paskia 0.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- paskia/__init__.py +3 -0
- paskia/_version.py +34 -0
- paskia/aaguid/__init__.py +32 -0
- paskia/aaguid/combined_aaguid.json +1 -0
- paskia/authsession.py +112 -0
- paskia/bootstrap.py +190 -0
- paskia/config.py +25 -0
- paskia/db/__init__.py +415 -0
- paskia/db/sql.py +1424 -0
- paskia/fastapi/__init__.py +3 -0
- paskia/fastapi/__main__.py +335 -0
- paskia/fastapi/admin.py +850 -0
- paskia/fastapi/api.py +308 -0
- paskia/fastapi/auth_host.py +97 -0
- paskia/fastapi/authz.py +110 -0
- paskia/fastapi/mainapp.py +130 -0
- paskia/fastapi/remote.py +504 -0
- paskia/fastapi/reset.py +101 -0
- paskia/fastapi/session.py +52 -0
- paskia/fastapi/user.py +162 -0
- paskia/fastapi/ws.py +163 -0
- paskia/fastapi/wsutil.py +91 -0
- paskia/frontend-build/auth/admin/index.html +18 -0
- paskia/frontend-build/auth/assets/AccessDenied-Bc249ASC.css +1 -0
- paskia/frontend-build/auth/assets/AccessDenied-C-lL9vbN.js +8 -0
- paskia/frontend-build/auth/assets/RestrictedAuth-BLMK7-nL.js +1 -0
- paskia/frontend-build/auth/assets/RestrictedAuth-DgdJyscT.css +1 -0
- paskia/frontend-build/auth/assets/_plugin-vue_export-helper-BTzJAQlS.css +1 -0
- paskia/frontend-build/auth/assets/_plugin-vue_export-helper-rKFEraYH.js +2 -0
- paskia/frontend-build/auth/assets/admin-Cs6Mg773.css +1 -0
- paskia/frontend-build/auth/assets/admin-Df5_Damp.js +1 -0
- paskia/frontend-build/auth/assets/auth-BU_O38k2.css +1 -0
- paskia/frontend-build/auth/assets/auth-Df3pjeSS.js +1 -0
- paskia/frontend-build/auth/assets/forward-Dzg-aE1C.js +1 -0
- paskia/frontend-build/auth/assets/helpers-DzjFIx78.js +1 -0
- paskia/frontend-build/auth/assets/pow-2N9bxgAo.js +1 -0
- paskia/frontend-build/auth/assets/reset-BWF4cWKR.css +1 -0
- paskia/frontend-build/auth/assets/reset-C_Td1_jn.js +1 -0
- paskia/frontend-build/auth/assets/restricted-C0IQufuH.js +1 -0
- paskia/frontend-build/auth/index.html +19 -0
- paskia/frontend-build/auth/restricted/index.html +16 -0
- paskia/frontend-build/int/forward/index.html +18 -0
- paskia/frontend-build/int/reset/index.html +15 -0
- paskia/globals.py +71 -0
- paskia/remoteauth.py +359 -0
- paskia/sansio.py +263 -0
- paskia/util/frontend.py +75 -0
- paskia/util/hostutil.py +76 -0
- paskia/util/htmlutil.py +47 -0
- paskia/util/passphrase.py +20 -0
- paskia/util/permutil.py +32 -0
- paskia/util/pow.py +45 -0
- paskia/util/querysafe.py +11 -0
- paskia/util/sessionutil.py +37 -0
- paskia/util/startupbox.py +75 -0
- paskia/util/timeutil.py +47 -0
- paskia/util/tokens.py +44 -0
- paskia/util/useragent.py +10 -0
- paskia/util/userinfo.py +159 -0
- paskia/util/wordlist.py +54 -0
- paskia-0.7.1.dist-info/METADATA +22 -0
- paskia-0.7.1.dist-info/RECORD +64 -0
- paskia-0.7.1.dist-info/WHEEL +4 -0
- paskia-0.7.1.dist-info/entry_points.txt +2 -0
paskia/db/__init__.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database module for WebAuthn passkey authentication.
|
|
3
|
+
|
|
4
|
+
This module provides dataclasses and database abstractions for managing
|
|
5
|
+
users, credentials, and sessions in a WebAuthn authentication system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Permission:
|
|
16
|
+
id: str # String primary key (max 128 chars)
|
|
17
|
+
display_name: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Role:
|
|
22
|
+
uuid: UUID
|
|
23
|
+
org_uuid: UUID
|
|
24
|
+
display_name: str
|
|
25
|
+
# List of permission IDs this role grants to its members
|
|
26
|
+
permissions: list[str] = field(default_factory=list) # permission IDs
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Org:
|
|
31
|
+
uuid: UUID
|
|
32
|
+
display_name: str
|
|
33
|
+
# All permission IDs that the Org is allowed to grant to its roles
|
|
34
|
+
permissions: list[str] = field(default_factory=list) # permission IDs
|
|
35
|
+
# Roles belonging to this org
|
|
36
|
+
roles: list[Role] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class User:
|
|
41
|
+
uuid: UUID
|
|
42
|
+
display_name: str
|
|
43
|
+
role_uuid: UUID
|
|
44
|
+
created_at: datetime | None = None
|
|
45
|
+
last_seen: datetime | None = None
|
|
46
|
+
visits: int = 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Credential:
|
|
51
|
+
uuid: UUID
|
|
52
|
+
credential_id: bytes # Long binary ID passed from the authenticator
|
|
53
|
+
user_uuid: UUID
|
|
54
|
+
aaguid: UUID
|
|
55
|
+
public_key: bytes
|
|
56
|
+
sign_count: int
|
|
57
|
+
created_at: datetime
|
|
58
|
+
last_used: datetime | None = None
|
|
59
|
+
last_verified: datetime | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Session:
|
|
64
|
+
key: bytes
|
|
65
|
+
user_uuid: UUID
|
|
66
|
+
credential_uuid: UUID
|
|
67
|
+
host: str
|
|
68
|
+
ip: str
|
|
69
|
+
user_agent: str
|
|
70
|
+
renewed: datetime
|
|
71
|
+
|
|
72
|
+
def metadata(self) -> dict:
|
|
73
|
+
"""Return session metadata for backwards compatibility."""
|
|
74
|
+
return {
|
|
75
|
+
"ip": self.ip,
|
|
76
|
+
"user_agent": self.user_agent,
|
|
77
|
+
"renewed": self.renewed.isoformat(),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class ResetToken:
|
|
83
|
+
key: bytes
|
|
84
|
+
user_uuid: UUID
|
|
85
|
+
expiry: datetime
|
|
86
|
+
token_type: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class SessionContext:
|
|
91
|
+
session: Session
|
|
92
|
+
user: User
|
|
93
|
+
org: Org
|
|
94
|
+
role: Role
|
|
95
|
+
credential: Credential | None = None
|
|
96
|
+
permissions: list[Permission] | None = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class DatabaseInterface(ABC):
|
|
100
|
+
"""Abstract base class defining the database interface.
|
|
101
|
+
|
|
102
|
+
This class defines the public API that database implementations should provide.
|
|
103
|
+
Implementations may use decorators like @with_session that modify method signatures
|
|
104
|
+
at runtime, so this interface focuses on the logical operations rather than
|
|
105
|
+
exact parameter matching.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
async def init_db(self) -> None:
|
|
110
|
+
"""Initialize database tables."""
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
# User operations
|
|
114
|
+
@abstractmethod
|
|
115
|
+
async def get_user_by_uuid(self, user_uuid: UUID) -> User:
|
|
116
|
+
"""Get user record by WebAuthn user UUID."""
|
|
117
|
+
|
|
118
|
+
@abstractmethod
|
|
119
|
+
async def create_user(self, user: User) -> None:
|
|
120
|
+
"""Create a new user."""
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
async def update_user_display_name(
|
|
124
|
+
self, user_uuid: UUID, display_name: str
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Update a user's display name."""
|
|
127
|
+
|
|
128
|
+
# Role operations
|
|
129
|
+
@abstractmethod
|
|
130
|
+
async def create_role(self, role: Role) -> None:
|
|
131
|
+
"""Create new role."""
|
|
132
|
+
|
|
133
|
+
@abstractmethod
|
|
134
|
+
async def update_role(self, role: Role) -> None:
|
|
135
|
+
"""Update a role's display name and synchronize its permissions."""
|
|
136
|
+
|
|
137
|
+
@abstractmethod
|
|
138
|
+
async def delete_role(self, role_uuid: UUID) -> None:
|
|
139
|
+
"""Delete a role by UUID. Implementations may prevent deletion if users exist."""
|
|
140
|
+
|
|
141
|
+
# Credential operations
|
|
142
|
+
@abstractmethod
|
|
143
|
+
async def create_credential(self, credential: Credential) -> None:
|
|
144
|
+
"""Store a credential for a user."""
|
|
145
|
+
|
|
146
|
+
@abstractmethod
|
|
147
|
+
async def get_credential_by_id(self, credential_id: bytes) -> Credential:
|
|
148
|
+
"""Get credential by credential ID."""
|
|
149
|
+
|
|
150
|
+
@abstractmethod
|
|
151
|
+
async def get_credentials_by_user_uuid(self, user_uuid: UUID) -> list[bytes]:
|
|
152
|
+
"""Get all credential IDs for a user."""
|
|
153
|
+
|
|
154
|
+
@abstractmethod
|
|
155
|
+
async def update_credential(self, credential: Credential) -> None:
|
|
156
|
+
"""Update the sign count, created_at, last_used, and last_verified for a credential."""
|
|
157
|
+
|
|
158
|
+
@abstractmethod
|
|
159
|
+
async def delete_credential(self, uuid: UUID, user_uuid: UUID) -> None:
|
|
160
|
+
"""Delete a specific credential for a user."""
|
|
161
|
+
|
|
162
|
+
# Session operations
|
|
163
|
+
@abstractmethod
|
|
164
|
+
async def create_session(
|
|
165
|
+
self,
|
|
166
|
+
user_uuid: UUID,
|
|
167
|
+
key: bytes,
|
|
168
|
+
credential_uuid: UUID,
|
|
169
|
+
host: str,
|
|
170
|
+
ip: str,
|
|
171
|
+
user_agent: str,
|
|
172
|
+
renewed: datetime,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Create a new session."""
|
|
175
|
+
|
|
176
|
+
@abstractmethod
|
|
177
|
+
async def get_session(self, key: bytes) -> Session | None:
|
|
178
|
+
"""Get session by key."""
|
|
179
|
+
|
|
180
|
+
@abstractmethod
|
|
181
|
+
async def delete_session(self, key: bytes) -> None:
|
|
182
|
+
"""Delete session by key."""
|
|
183
|
+
|
|
184
|
+
@abstractmethod
|
|
185
|
+
async def update_session(
|
|
186
|
+
self,
|
|
187
|
+
key: bytes,
|
|
188
|
+
*,
|
|
189
|
+
ip: str,
|
|
190
|
+
user_agent: str,
|
|
191
|
+
renewed: datetime,
|
|
192
|
+
) -> Session | None:
|
|
193
|
+
"""Update session metadata and touch renewed timestamp."""
|
|
194
|
+
|
|
195
|
+
@abstractmethod
|
|
196
|
+
async def set_session_host(self, key: bytes, host: str) -> None:
|
|
197
|
+
"""Bind a session to a specific host if not already set."""
|
|
198
|
+
|
|
199
|
+
@abstractmethod
|
|
200
|
+
async def list_sessions_for_user(self, user_uuid: UUID) -> list[Session]:
|
|
201
|
+
"""Return all sessions for a user (including other hosts)."""
|
|
202
|
+
|
|
203
|
+
@abstractmethod
|
|
204
|
+
async def cleanup(self) -> None:
|
|
205
|
+
"""Called periodically to clean up expired records."""
|
|
206
|
+
|
|
207
|
+
@abstractmethod
|
|
208
|
+
async def delete_sessions_for_user(self, user_uuid: UUID) -> None:
|
|
209
|
+
"""Delete all sessions belonging to the provided user."""
|
|
210
|
+
|
|
211
|
+
# Reset token operations
|
|
212
|
+
@abstractmethod
|
|
213
|
+
async def create_reset_token(
|
|
214
|
+
self,
|
|
215
|
+
user_uuid: UUID,
|
|
216
|
+
key: bytes,
|
|
217
|
+
expiry: datetime,
|
|
218
|
+
token_type: str,
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Create a reset token for a user."""
|
|
221
|
+
|
|
222
|
+
@abstractmethod
|
|
223
|
+
async def get_reset_token(self, key: bytes) -> ResetToken | None:
|
|
224
|
+
"""Retrieve a reset token by key."""
|
|
225
|
+
|
|
226
|
+
@abstractmethod
|
|
227
|
+
async def delete_reset_token(self, key: bytes) -> None:
|
|
228
|
+
"""Delete a reset token by key."""
|
|
229
|
+
|
|
230
|
+
# Organization operations
|
|
231
|
+
@abstractmethod
|
|
232
|
+
async def create_organization(self, org: Org) -> None:
|
|
233
|
+
"""Add a new organization."""
|
|
234
|
+
|
|
235
|
+
@abstractmethod
|
|
236
|
+
async def get_organization(self, org_id: str) -> Org:
|
|
237
|
+
"""Get organization by ID, including its permission IDs and roles (with their permission IDs)."""
|
|
238
|
+
|
|
239
|
+
@abstractmethod
|
|
240
|
+
async def list_organizations(self) -> list[Org]:
|
|
241
|
+
"""List all organizations with their roles and permission IDs."""
|
|
242
|
+
|
|
243
|
+
@abstractmethod
|
|
244
|
+
async def update_organization(self, org: Org) -> None:
|
|
245
|
+
"""Update organization options."""
|
|
246
|
+
|
|
247
|
+
@abstractmethod
|
|
248
|
+
async def delete_organization(self, org_uuid: UUID) -> None:
|
|
249
|
+
"""Delete organization by ID."""
|
|
250
|
+
|
|
251
|
+
@abstractmethod
|
|
252
|
+
async def add_user_to_organization(
|
|
253
|
+
self, user_uuid: UUID, org_id: str, role: str
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Set a user's organization and role."""
|
|
256
|
+
|
|
257
|
+
@abstractmethod
|
|
258
|
+
async def transfer_user_to_organization(
|
|
259
|
+
self, user_uuid: UUID, new_org_id: str, new_role: str | None = None
|
|
260
|
+
) -> None:
|
|
261
|
+
"""Transfer a user to another organization with an optional role."""
|
|
262
|
+
|
|
263
|
+
@abstractmethod
|
|
264
|
+
async def get_user_organization(self, user_uuid: UUID) -> tuple[Org, str]:
|
|
265
|
+
"""Get the organization and role for a user."""
|
|
266
|
+
|
|
267
|
+
@abstractmethod
|
|
268
|
+
async def get_organization_users(self, org_id: str) -> list[tuple[User, str]]:
|
|
269
|
+
"""Get all users in an organization with their roles."""
|
|
270
|
+
|
|
271
|
+
@abstractmethod
|
|
272
|
+
async def get_roles_by_organization(self, org_id: str) -> list[Role]:
|
|
273
|
+
"""List roles belonging to an organization."""
|
|
274
|
+
|
|
275
|
+
@abstractmethod
|
|
276
|
+
async def get_user_role_in_organization(
|
|
277
|
+
self, user_uuid: UUID, org_id: str
|
|
278
|
+
) -> str | None:
|
|
279
|
+
"""Get a user's role in a specific organization."""
|
|
280
|
+
|
|
281
|
+
@abstractmethod
|
|
282
|
+
async def update_user_role_in_organization(
|
|
283
|
+
self, user_uuid: UUID, new_role: str
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Update a user's role in their organization."""
|
|
286
|
+
|
|
287
|
+
# Permission operations
|
|
288
|
+
@abstractmethod
|
|
289
|
+
async def create_permission(self, permission: Permission) -> None:
|
|
290
|
+
"""Create a new permission."""
|
|
291
|
+
|
|
292
|
+
@abstractmethod
|
|
293
|
+
async def get_permission(self, permission_id: str) -> Permission:
|
|
294
|
+
"""Get permission by ID."""
|
|
295
|
+
|
|
296
|
+
@abstractmethod
|
|
297
|
+
async def list_permissions(self) -> list[Permission]:
|
|
298
|
+
"""List all permissions."""
|
|
299
|
+
|
|
300
|
+
@abstractmethod
|
|
301
|
+
async def update_permission(self, permission: Permission) -> None:
|
|
302
|
+
"""Update permission details."""
|
|
303
|
+
|
|
304
|
+
@abstractmethod
|
|
305
|
+
async def delete_permission(self, permission_id: str) -> None:
|
|
306
|
+
"""Delete permission by ID."""
|
|
307
|
+
|
|
308
|
+
@abstractmethod
|
|
309
|
+
async def rename_permission(
|
|
310
|
+
self, old_id: str, new_id: str, display_name: str
|
|
311
|
+
) -> None:
|
|
312
|
+
"""Rename a permission's ID (and display name) updating all references.
|
|
313
|
+
|
|
314
|
+
This must update:
|
|
315
|
+
- permissions.id (primary key)
|
|
316
|
+
- org_permissions.permission_id
|
|
317
|
+
- role_permissions.permission_id
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
@abstractmethod
|
|
321
|
+
async def add_permission_to_organization(
|
|
322
|
+
self, org_id: str, permission_id: str
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Add a permission to an organization."""
|
|
325
|
+
|
|
326
|
+
@abstractmethod
|
|
327
|
+
async def remove_permission_from_organization(
|
|
328
|
+
self, org_id: str, permission_id: str
|
|
329
|
+
) -> None:
|
|
330
|
+
"""Remove a permission from an organization."""
|
|
331
|
+
|
|
332
|
+
@abstractmethod
|
|
333
|
+
async def get_organization_permissions(self, org_id: str) -> list[Permission]:
|
|
334
|
+
"""Get all permissions assigned to an organization."""
|
|
335
|
+
|
|
336
|
+
@abstractmethod
|
|
337
|
+
async def get_permission_organizations(self, permission_id: str) -> list[Org]:
|
|
338
|
+
"""Get all organizations that have a specific permission."""
|
|
339
|
+
|
|
340
|
+
# Role-permission operations
|
|
341
|
+
@abstractmethod
|
|
342
|
+
async def add_permission_to_role(self, role_uuid: UUID, permission_id: str) -> None:
|
|
343
|
+
"""Add a permission to a role."""
|
|
344
|
+
|
|
345
|
+
@abstractmethod
|
|
346
|
+
async def remove_permission_from_role(
|
|
347
|
+
self, role_uuid: UUID, permission_id: str
|
|
348
|
+
) -> None:
|
|
349
|
+
"""Remove a permission from a role."""
|
|
350
|
+
|
|
351
|
+
@abstractmethod
|
|
352
|
+
async def get_role_permissions(self, role_uuid: UUID) -> list[Permission]:
|
|
353
|
+
"""List all permissions granted to a role."""
|
|
354
|
+
|
|
355
|
+
@abstractmethod
|
|
356
|
+
async def get_permission_roles(self, permission_id: str) -> list[Role]:
|
|
357
|
+
"""List all roles that grant a permission."""
|
|
358
|
+
|
|
359
|
+
@abstractmethod
|
|
360
|
+
async def get_role(self, role_uuid: UUID) -> Role:
|
|
361
|
+
"""Get a role by UUID, including its permission IDs."""
|
|
362
|
+
|
|
363
|
+
# Combined operations
|
|
364
|
+
@abstractmethod
|
|
365
|
+
async def login(self, user_uuid: UUID, credential: Credential) -> None:
|
|
366
|
+
"""Update user and credential timestamps after successful login."""
|
|
367
|
+
|
|
368
|
+
@abstractmethod
|
|
369
|
+
async def create_user_and_credential(
|
|
370
|
+
self, user: User, credential: Credential
|
|
371
|
+
) -> None:
|
|
372
|
+
"""Create a new user and their first credential in a transaction."""
|
|
373
|
+
|
|
374
|
+
@abstractmethod
|
|
375
|
+
async def get_session_context(
|
|
376
|
+
self, session_key: bytes, host: str | None = None
|
|
377
|
+
) -> SessionContext | None:
|
|
378
|
+
"""Get complete session context including user, organization, role, and permissions."""
|
|
379
|
+
|
|
380
|
+
# Combined atomic operations
|
|
381
|
+
@abstractmethod
|
|
382
|
+
async def create_credential_session(
|
|
383
|
+
self,
|
|
384
|
+
user_uuid: UUID,
|
|
385
|
+
credential: Credential,
|
|
386
|
+
reset_key: bytes | None,
|
|
387
|
+
session_key: bytes,
|
|
388
|
+
*,
|
|
389
|
+
display_name: str | None = None,
|
|
390
|
+
host: str | None = None,
|
|
391
|
+
ip: str | None = None,
|
|
392
|
+
user_agent: str | None = None,
|
|
393
|
+
) -> None:
|
|
394
|
+
"""Atomically add a credential and create a session.
|
|
395
|
+
|
|
396
|
+
Steps (single transaction):
|
|
397
|
+
1. Insert credential
|
|
398
|
+
2. Optionally delete old reset token if provided
|
|
399
|
+
3. Optionally update user's display name
|
|
400
|
+
4. Insert new session referencing the credential
|
|
401
|
+
5. Update user's last_seen and increment visits (treat as a login)
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
__all__ = [
|
|
406
|
+
"User",
|
|
407
|
+
"Credential",
|
|
408
|
+
"Session",
|
|
409
|
+
"ResetToken",
|
|
410
|
+
"SessionContext",
|
|
411
|
+
"Org",
|
|
412
|
+
"Role",
|
|
413
|
+
"Permission",
|
|
414
|
+
"DatabaseInterface",
|
|
415
|
+
]
|