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.
Files changed (64) hide show
  1. paskia/__init__.py +3 -0
  2. paskia/_version.py +34 -0
  3. paskia/aaguid/__init__.py +32 -0
  4. paskia/aaguid/combined_aaguid.json +1 -0
  5. paskia/authsession.py +112 -0
  6. paskia/bootstrap.py +190 -0
  7. paskia/config.py +25 -0
  8. paskia/db/__init__.py +415 -0
  9. paskia/db/sql.py +1424 -0
  10. paskia/fastapi/__init__.py +3 -0
  11. paskia/fastapi/__main__.py +335 -0
  12. paskia/fastapi/admin.py +850 -0
  13. paskia/fastapi/api.py +308 -0
  14. paskia/fastapi/auth_host.py +97 -0
  15. paskia/fastapi/authz.py +110 -0
  16. paskia/fastapi/mainapp.py +130 -0
  17. paskia/fastapi/remote.py +504 -0
  18. paskia/fastapi/reset.py +101 -0
  19. paskia/fastapi/session.py +52 -0
  20. paskia/fastapi/user.py +162 -0
  21. paskia/fastapi/ws.py +163 -0
  22. paskia/fastapi/wsutil.py +91 -0
  23. paskia/frontend-build/auth/admin/index.html +18 -0
  24. paskia/frontend-build/auth/assets/AccessDenied-Bc249ASC.css +1 -0
  25. paskia/frontend-build/auth/assets/AccessDenied-C-lL9vbN.js +8 -0
  26. paskia/frontend-build/auth/assets/RestrictedAuth-BLMK7-nL.js +1 -0
  27. paskia/frontend-build/auth/assets/RestrictedAuth-DgdJyscT.css +1 -0
  28. paskia/frontend-build/auth/assets/_plugin-vue_export-helper-BTzJAQlS.css +1 -0
  29. paskia/frontend-build/auth/assets/_plugin-vue_export-helper-rKFEraYH.js +2 -0
  30. paskia/frontend-build/auth/assets/admin-Cs6Mg773.css +1 -0
  31. paskia/frontend-build/auth/assets/admin-Df5_Damp.js +1 -0
  32. paskia/frontend-build/auth/assets/auth-BU_O38k2.css +1 -0
  33. paskia/frontend-build/auth/assets/auth-Df3pjeSS.js +1 -0
  34. paskia/frontend-build/auth/assets/forward-Dzg-aE1C.js +1 -0
  35. paskia/frontend-build/auth/assets/helpers-DzjFIx78.js +1 -0
  36. paskia/frontend-build/auth/assets/pow-2N9bxgAo.js +1 -0
  37. paskia/frontend-build/auth/assets/reset-BWF4cWKR.css +1 -0
  38. paskia/frontend-build/auth/assets/reset-C_Td1_jn.js +1 -0
  39. paskia/frontend-build/auth/assets/restricted-C0IQufuH.js +1 -0
  40. paskia/frontend-build/auth/index.html +19 -0
  41. paskia/frontend-build/auth/restricted/index.html +16 -0
  42. paskia/frontend-build/int/forward/index.html +18 -0
  43. paskia/frontend-build/int/reset/index.html +15 -0
  44. paskia/globals.py +71 -0
  45. paskia/remoteauth.py +359 -0
  46. paskia/sansio.py +263 -0
  47. paskia/util/frontend.py +75 -0
  48. paskia/util/hostutil.py +76 -0
  49. paskia/util/htmlutil.py +47 -0
  50. paskia/util/passphrase.py +20 -0
  51. paskia/util/permutil.py +32 -0
  52. paskia/util/pow.py +45 -0
  53. paskia/util/querysafe.py +11 -0
  54. paskia/util/sessionutil.py +37 -0
  55. paskia/util/startupbox.py +75 -0
  56. paskia/util/timeutil.py +47 -0
  57. paskia/util/tokens.py +44 -0
  58. paskia/util/useragent.py +10 -0
  59. paskia/util/userinfo.py +159 -0
  60. paskia/util/wordlist.py +54 -0
  61. paskia-0.7.1.dist-info/METADATA +22 -0
  62. paskia-0.7.1.dist-info/RECORD +64 -0
  63. paskia-0.7.1.dist-info/WHEEL +4 -0
  64. 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
+ ]