paskia 0.9.0__py3-none-any.whl → 0.10.0__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/_version.py +2 -2
- paskia/aaguid/__init__.py +5 -4
- paskia/authsession.py +4 -19
- paskia/db/__init__.py +2 -4
- paskia/db/background.py +3 -3
- paskia/db/jsonl.py +99 -111
- paskia/db/logging.py +318 -0
- paskia/db/migrations.py +19 -20
- paskia/db/operations.py +107 -196
- paskia/db/structs.py +236 -46
- paskia/fastapi/__main__.py +13 -6
- paskia/fastapi/admin.py +72 -195
- paskia/fastapi/api.py +56 -58
- paskia/fastapi/authz.py +3 -8
- paskia/fastapi/logging.py +261 -0
- paskia/fastapi/mainapp.py +14 -3
- paskia/fastapi/remote.py +11 -37
- paskia/fastapi/reset.py +0 -2
- paskia/fastapi/response.py +22 -0
- paskia/fastapi/user.py +7 -7
- paskia/fastapi/ws.py +14 -37
- paskia/fastapi/wschat.py +55 -2
- paskia/fastapi/wsutil.py +10 -2
- paskia/frontend-build/auth/admin/index.html +6 -6
- paskia/frontend-build/auth/assets/AccessDenied-C29NZI95.css +1 -0
- paskia/frontend-build/auth/assets/AccessDenied-DAdzg_MJ.js +12 -0
- paskia/frontend-build/auth/assets/{RestrictedAuth-CvR33_Z0.css → RestrictedAuth-BOdNrlQB.css} +1 -1
- paskia/frontend-build/auth/assets/{RestrictedAuth-DsJXicIw.js → RestrictedAuth-BSusdAfp.js} +1 -1
- paskia/frontend-build/auth/assets/_plugin-vue_export-helper-D2l53SUz.js +49 -0
- paskia/frontend-build/auth/assets/_plugin-vue_export-helper-DYJ24FZK.css +1 -0
- paskia/frontend-build/auth/assets/admin-BeFvGyD6.js +1 -0
- paskia/frontend-build/auth/assets/{admin-DzzjSg72.css → admin-CmNtuH3s.css} +1 -1
- paskia/frontend-build/auth/assets/{auth-C7k64Wad.css → auth-BKq4T2K2.css} +1 -1
- paskia/frontend-build/auth/assets/auth-DvHf8hgy.js +1 -0
- paskia/frontend-build/auth/assets/{forward-DmqVHZ7e.js → forward-C86Jm_Uq.js} +1 -1
- paskia/frontend-build/auth/assets/reset-B8PlNXuP.css +1 -0
- paskia/frontend-build/auth/assets/reset-D71FG0VL.js +1 -0
- paskia/frontend-build/auth/assets/{restricted-D3AJx3_6.js → restricted-CW0drE_k.js} +1 -1
- paskia/frontend-build/auth/index.html +6 -6
- paskia/frontend-build/auth/restricted/index.html +5 -5
- paskia/frontend-build/int/forward/index.html +5 -5
- paskia/frontend-build/int/reset/index.html +4 -4
- paskia/migrate/__init__.py +9 -9
- paskia/migrate/sql.py +26 -19
- paskia/remoteauth.py +6 -6
- {paskia-0.9.0.dist-info → paskia-0.10.0.dist-info}/METADATA +1 -1
- paskia-0.10.0.dist-info/RECORD +60 -0
- paskia/frontend-build/auth/assets/AccessDenied-DPkUS8LZ.css +0 -1
- paskia/frontend-build/auth/assets/AccessDenied-Fmeb6EtF.js +0 -8
- paskia/frontend-build/auth/assets/_plugin-vue_export-helper-BTzJAQlS.css +0 -1
- paskia/frontend-build/auth/assets/_plugin-vue_export-helper-nhjnO_bd.js +0 -2
- paskia/frontend-build/auth/assets/admin-CPE1pLMm.js +0 -1
- paskia/frontend-build/auth/assets/auth-YIZvPlW_.js +0 -1
- paskia/frontend-build/auth/assets/reset-Chtv69AT.css +0 -1
- paskia/frontend-build/auth/assets/reset-s20PATTN.js +0 -1
- paskia-0.9.0.dist-info/RECORD +0 -57
- {paskia-0.9.0.dist-info → paskia-0.10.0.dist-info}/WHEEL +0 -0
- {paskia-0.9.0.dist-info → paskia-0.10.0.dist-info}/entry_points.txt +0 -0
paskia/db/operations.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Database for WebAuthn passkey authentication.
|
|
3
3
|
|
|
4
4
|
Read operations: Access _db directly, use build_* helpers to get public structs.
|
|
5
|
-
Context lookup:
|
|
5
|
+
Context lookup: _db.session_ctx() returns full SessionContext with effective permissions.
|
|
6
6
|
Write operations: Functions that validate and commit, or raise ValueError.
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ import hashlib
|
|
|
10
10
|
import logging
|
|
11
11
|
import os
|
|
12
12
|
import secrets
|
|
13
|
-
from datetime import
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
14
|
from uuid import UUID
|
|
15
15
|
|
|
16
16
|
import uuid7
|
|
@@ -31,7 +31,6 @@ from paskia.db.structs import (
|
|
|
31
31
|
SessionContext,
|
|
32
32
|
User,
|
|
33
33
|
)
|
|
34
|
-
from paskia.util.hostutil import normalize_host
|
|
35
34
|
from paskia.util.passphrase import generate as generate_passphrase
|
|
36
35
|
from paskia.util.passphrase import is_well_formed as _is_passphrase
|
|
37
36
|
|
|
@@ -69,21 +68,18 @@ def get_user_organization(user_uuid: UUID) -> tuple[Org, str]:
|
|
|
69
68
|
Raises ValueError if user not found.
|
|
70
69
|
|
|
71
70
|
Call sites:
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
71
|
+
- update_user_role_in_organization: org only
|
|
72
|
+
- admin_create_user_registration_link: org only
|
|
73
|
+
- admin_get_user_detail: org and role
|
|
74
|
+
- admin_update_user_display_name: org only
|
|
75
|
+
- admin_delete_user_credential: org only
|
|
76
|
+
- admin_delete_user_session: org only
|
|
78
77
|
"""
|
|
79
78
|
if user_uuid not in _db.users:
|
|
80
79
|
raise ValueError(f"User {user_uuid} not found")
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
role_data = _db.roles[role_uuid]
|
|
85
|
-
org_uuid = role_data.org
|
|
86
|
-
return _db.orgs[org_uuid], role_data.display_name
|
|
80
|
+
user = _db.users[user_uuid]
|
|
81
|
+
role = user.role
|
|
82
|
+
return role.org, role.display_name
|
|
87
83
|
|
|
88
84
|
|
|
89
85
|
def get_organization_users(org_uuid: UUID) -> list[tuple[User, str]]:
|
|
@@ -91,10 +87,8 @@ def get_organization_users(org_uuid: UUID) -> list[tuple[User, str]]:
|
|
|
91
87
|
|
|
92
88
|
Returns list of (User, role_display_name) tuples.
|
|
93
89
|
"""
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
return [(u, role_map[u.role]) for u in _db.users.values() if u.role in role_map]
|
|
90
|
+
org = _db.orgs[org_uuid]
|
|
91
|
+
return [(u, u.role.display_name) for role in org.roles for u in role.users]
|
|
98
92
|
|
|
99
93
|
|
|
100
94
|
def get_user_credential_ids(user_uuid: UUID) -> list[bytes]:
|
|
@@ -102,7 +96,8 @@ def get_user_credential_ids(user_uuid: UUID) -> list[bytes]:
|
|
|
102
96
|
|
|
103
97
|
Returns empty list if user has no credentials.
|
|
104
98
|
"""
|
|
105
|
-
|
|
99
|
+
assert user_uuid
|
|
100
|
+
return [c.credential_id for c in _db.users[user_uuid].credentials]
|
|
106
101
|
|
|
107
102
|
|
|
108
103
|
def _reset_key(passphrase: str) -> bytes:
|
|
@@ -126,92 +121,6 @@ def get_reset_token(passphrase: str) -> ResetToken | None:
|
|
|
126
121
|
return _db.reset_tokens.get(key)
|
|
127
122
|
|
|
128
123
|
|
|
129
|
-
# -------------------------------------------------------------------------
|
|
130
|
-
# Context lookup
|
|
131
|
-
# -------------------------------------------------------------------------
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def get_session_context(
|
|
135
|
-
session_key: str, host: str | None = None
|
|
136
|
-
) -> SessionContext | None:
|
|
137
|
-
"""Get full session context with effective permissions.
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
session_key: The session key string
|
|
141
|
-
host: Optional host for binding/validation and domain-scoped permissions
|
|
142
|
-
|
|
143
|
-
Returns:
|
|
144
|
-
SessionContext if valid, None if session not found, expired, or host mismatch
|
|
145
|
-
|
|
146
|
-
Call sites:
|
|
147
|
-
- Example usage in docstring (db/__init__.py:16)
|
|
148
|
-
- Get session context from auth token (util/permutil.py:43)
|
|
149
|
-
"""
|
|
150
|
-
|
|
151
|
-
if session_key not in _db.sessions:
|
|
152
|
-
return None
|
|
153
|
-
|
|
154
|
-
s = _db.sessions[session_key]
|
|
155
|
-
if s.expiry < datetime.now(timezone.utc):
|
|
156
|
-
return None
|
|
157
|
-
|
|
158
|
-
# Validate host matches (sessions are always created with a host)
|
|
159
|
-
if host is not None and s.host != host:
|
|
160
|
-
# Session bound to different host
|
|
161
|
-
return None
|
|
162
|
-
|
|
163
|
-
# Validate user exists
|
|
164
|
-
if s.user not in _db.users:
|
|
165
|
-
return None
|
|
166
|
-
|
|
167
|
-
# Validate role exists
|
|
168
|
-
role_uuid = _db.users[s.user].role
|
|
169
|
-
if role_uuid not in _db.roles:
|
|
170
|
-
return None
|
|
171
|
-
|
|
172
|
-
# Validate org exists
|
|
173
|
-
org_uuid = _db.roles[role_uuid].org
|
|
174
|
-
if org_uuid not in _db.orgs:
|
|
175
|
-
return None
|
|
176
|
-
|
|
177
|
-
session = _db.sessions[session_key]
|
|
178
|
-
user = _db.users[s.user]
|
|
179
|
-
role = _db.roles[role_uuid]
|
|
180
|
-
org = _db.orgs[org_uuid]
|
|
181
|
-
|
|
182
|
-
# Credential must exist (sessions are cascade-deleted when credential is deleted)
|
|
183
|
-
if s.credential not in _db.credentials:
|
|
184
|
-
return None
|
|
185
|
-
credential = _db.credentials[s.credential]
|
|
186
|
-
|
|
187
|
-
# Effective permissions: role's permissions that the org can grant
|
|
188
|
-
# Also filter by domain if host is provided
|
|
189
|
-
org_perm_uuids = {pid for pid, p in _db.permissions.items() if org_uuid in p.orgs}
|
|
190
|
-
normalized_host = normalize_host(host)
|
|
191
|
-
host_without_port = normalized_host.rsplit(":", 1)[0] if normalized_host else None
|
|
192
|
-
|
|
193
|
-
effective_perms = []
|
|
194
|
-
for perm_uuid in role.permission_set:
|
|
195
|
-
if perm_uuid not in org_perm_uuids:
|
|
196
|
-
continue
|
|
197
|
-
if perm_uuid not in _db.permissions:
|
|
198
|
-
continue
|
|
199
|
-
p = _db.permissions[perm_uuid]
|
|
200
|
-
# Check domain restriction
|
|
201
|
-
if p.domain is not None and p.domain != host_without_port:
|
|
202
|
-
continue
|
|
203
|
-
effective_perms.append(_db.permissions[perm_uuid])
|
|
204
|
-
|
|
205
|
-
return SessionContext(
|
|
206
|
-
session=session,
|
|
207
|
-
user=user,
|
|
208
|
-
org=org,
|
|
209
|
-
role=role,
|
|
210
|
-
credential=credential,
|
|
211
|
-
permissions=effective_perms,
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
|
|
215
124
|
# -------------------------------------------------------------------------
|
|
216
125
|
# Write operations (validate, modify, commit or raise ValueError)
|
|
217
126
|
# -------------------------------------------------------------------------
|
|
@@ -264,9 +173,9 @@ def create_org(org: Org, *, ctx: SessionContext | None = None) -> None:
|
|
|
264
173
|
if org.uuid in _db.orgs:
|
|
265
174
|
raise ValueError(f"Organization {org.uuid} already exists")
|
|
266
175
|
with _db.transaction("admin:create_org", ctx):
|
|
267
|
-
new_org = Org(display_name=org.display_name)
|
|
268
|
-
_db.orgs[org.uuid] = new_org
|
|
176
|
+
new_org = Org.create(display_name=org.display_name)
|
|
269
177
|
new_org.uuid = org.uuid
|
|
178
|
+
_db.orgs[org.uuid] = new_org
|
|
270
179
|
# Create Administration role with org admin permission
|
|
271
180
|
|
|
272
181
|
admin_role_uuid = uuid7.create()
|
|
@@ -278,7 +187,7 @@ def create_org(org: Org, *, ctx: SessionContext | None = None) -> None:
|
|
|
278
187
|
break
|
|
279
188
|
role_permissions = {org_admin_perm_uuid: True} if org_admin_perm_uuid else {}
|
|
280
189
|
admin_role = Role(
|
|
281
|
-
|
|
190
|
+
org_uuid=org.uuid,
|
|
282
191
|
display_name="Administration",
|
|
283
192
|
permissions=role_permissions,
|
|
284
193
|
)
|
|
@@ -304,17 +213,15 @@ def delete_org(uuid: UUID, *, ctx: SessionContext | None = None) -> None:
|
|
|
304
213
|
if uuid not in _db.orgs:
|
|
305
214
|
raise ValueError(f"Organization {uuid} not found")
|
|
306
215
|
with _db.transaction("admin:delete_org", ctx):
|
|
216
|
+
org = _db.orgs[uuid]
|
|
307
217
|
# Remove org from all permissions
|
|
308
218
|
for p in _db.permissions.values():
|
|
309
219
|
p.orgs.pop(uuid, None)
|
|
310
|
-
# Delete roles in this org
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
user_uuids = [uid for uid, u in _db.users.items() if u.role in role_uuids]
|
|
316
|
-
for uid in user_uuids:
|
|
317
|
-
del _db.users[uid]
|
|
220
|
+
# Delete roles in this org and their users
|
|
221
|
+
for role in org.roles:
|
|
222
|
+
for user in role.users:
|
|
223
|
+
del _db.users[user.uuid]
|
|
224
|
+
del _db.roles[role.uuid]
|
|
318
225
|
del _db.orgs[uuid]
|
|
319
226
|
|
|
320
227
|
|
|
@@ -356,8 +263,8 @@ def create_role(role: Role, *, ctx: SessionContext | None = None) -> None:
|
|
|
356
263
|
"""Create a new role."""
|
|
357
264
|
if role.uuid in _db.roles:
|
|
358
265
|
raise ValueError(f"Role {role.uuid} already exists")
|
|
359
|
-
if role.
|
|
360
|
-
raise ValueError(f"Organization {role.
|
|
266
|
+
if role.org_uuid not in _db.orgs:
|
|
267
|
+
raise ValueError(f"Organization {role.org_uuid} not found")
|
|
361
268
|
with _db.transaction("admin:create_role", ctx):
|
|
362
269
|
_db.roles[role.uuid] = role
|
|
363
270
|
|
|
@@ -408,7 +315,8 @@ def delete_role(uuid: UUID, *, ctx: SessionContext | None = None) -> None:
|
|
|
408
315
|
if uuid not in _db.roles:
|
|
409
316
|
raise ValueError(f"Role {uuid} not found")
|
|
410
317
|
# Check no users have this role
|
|
411
|
-
|
|
318
|
+
role = _db.roles[uuid]
|
|
319
|
+
if role.users:
|
|
412
320
|
raise ValueError(f"Cannot delete role {uuid}: users still assigned")
|
|
413
321
|
with _db.transaction("admin:delete_role", ctx):
|
|
414
322
|
del _db.roles[uuid]
|
|
@@ -418,8 +326,8 @@ def create_user(new_user: User, *, ctx: SessionContext | None = None) -> None:
|
|
|
418
326
|
"""Create a new user."""
|
|
419
327
|
if new_user.uuid in _db.users:
|
|
420
328
|
raise ValueError(f"User {new_user.uuid} already exists")
|
|
421
|
-
if new_user.
|
|
422
|
-
raise ValueError(f"Role {new_user.
|
|
329
|
+
if new_user.role_uuid not in _db.roles:
|
|
330
|
+
raise ValueError(f"Role {new_user.role_uuid} not found")
|
|
423
331
|
with _db.transaction("admin:create_user", ctx):
|
|
424
332
|
_db.users[new_user.uuid] = new_user
|
|
425
333
|
|
|
@@ -456,7 +364,7 @@ def update_user_role(
|
|
|
456
364
|
if role_uuid not in _db.roles:
|
|
457
365
|
raise ValueError(f"Role {role_uuid} not found")
|
|
458
366
|
with _db.transaction("admin:update_user_role", ctx):
|
|
459
|
-
_db.users[uuid].
|
|
367
|
+
_db.users[uuid].role_uuid = role_uuid
|
|
460
368
|
|
|
461
369
|
|
|
462
370
|
def update_user_role_in_organization(
|
|
@@ -468,39 +376,35 @@ def update_user_role_in_organization(
|
|
|
468
376
|
"""Update user's role by role name within their current organization."""
|
|
469
377
|
if user_uuid not in _db.users:
|
|
470
378
|
raise ValueError(f"User {user_uuid} not found")
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
raise ValueError("Current role not found")
|
|
474
|
-
org_uuid = _db.roles[current_role_uuid].org
|
|
379
|
+
user = _db.users[user_uuid]
|
|
380
|
+
org = user.org
|
|
475
381
|
# Find role by name in the same org
|
|
476
382
|
new_role_uuid = None
|
|
477
|
-
for
|
|
478
|
-
if r.
|
|
479
|
-
new_role_uuid =
|
|
383
|
+
for r in org.roles:
|
|
384
|
+
if r.display_name == role_name:
|
|
385
|
+
new_role_uuid = r.uuid
|
|
480
386
|
break
|
|
481
387
|
if new_role_uuid is None:
|
|
482
388
|
raise ValueError(f"Role '{role_name}' not found in organization")
|
|
483
389
|
with _db.transaction("admin:update_user_role", ctx):
|
|
484
|
-
_db.users[user_uuid].
|
|
390
|
+
_db.users[user_uuid].role_uuid = new_role_uuid
|
|
485
391
|
|
|
486
392
|
|
|
487
393
|
def delete_user(uuid: UUID, *, ctx: SessionContext | None = None) -> None:
|
|
488
394
|
"""Delete user and their credentials/sessions."""
|
|
489
395
|
if uuid not in _db.users:
|
|
490
396
|
raise ValueError(f"User {uuid} not found")
|
|
397
|
+
user = _db.users[uuid]
|
|
491
398
|
with _db.transaction("admin:delete_user", ctx):
|
|
492
399
|
# Delete credentials
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
del _db.credentials[cid]
|
|
400
|
+
for cred in user.credentials:
|
|
401
|
+
del _db.credentials[cred.uuid]
|
|
496
402
|
# Delete sessions
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
del _db.sessions[k]
|
|
403
|
+
for sess in user.sessions:
|
|
404
|
+
del _db.sessions[sess.key]
|
|
500
405
|
# Delete reset tokens
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
del _db.reset_tokens[k]
|
|
406
|
+
for token in user.reset_tokens:
|
|
407
|
+
del _db.reset_tokens[token.key]
|
|
504
408
|
del _db.users[uuid]
|
|
505
409
|
|
|
506
410
|
|
|
@@ -508,8 +412,8 @@ def create_credential(cred: Credential, *, ctx: SessionContext | None = None) ->
|
|
|
508
412
|
"""Create a new credential."""
|
|
509
413
|
if cred.uuid in _db.credentials:
|
|
510
414
|
raise ValueError(f"Credential {cred.uuid} already exists")
|
|
511
|
-
if cred.
|
|
512
|
-
raise ValueError(f"User {cred.
|
|
415
|
+
if cred.user_uuid not in _db.users:
|
|
416
|
+
raise ValueError(f"User {cred.user_uuid} not found")
|
|
513
417
|
with _db.transaction("create_credential", ctx):
|
|
514
418
|
_db.credentials[cred.uuid] = cred
|
|
515
419
|
|
|
@@ -542,20 +446,19 @@ def delete_credential(
|
|
|
542
446
|
"""
|
|
543
447
|
if uuid not in _db.credentials:
|
|
544
448
|
raise ValueError(f"Credential {uuid} not found")
|
|
449
|
+
cred = _db.credentials[uuid]
|
|
545
450
|
if user_uuid is not None:
|
|
546
|
-
|
|
547
|
-
if cred_user != user_uuid:
|
|
451
|
+
if cred.user_uuid != user_uuid:
|
|
548
452
|
raise ValueError(f"Credential {uuid} does not belong to user {user_uuid}")
|
|
549
453
|
with _db.transaction("delete_credential", ctx):
|
|
550
454
|
# Delete all sessions using this credential
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
del _db.sessions[
|
|
455
|
+
for sess in cred.sessions:
|
|
456
|
+
print(sess, repr(sess.key))
|
|
457
|
+
del _db.sessions[sess.key]
|
|
554
458
|
del _db.credentials[uuid]
|
|
555
459
|
|
|
556
460
|
|
|
557
461
|
def create_session(
|
|
558
|
-
key: str,
|
|
559
462
|
user_uuid: UUID,
|
|
560
463
|
credential_uuid: UUID,
|
|
561
464
|
host: str,
|
|
@@ -564,23 +467,25 @@ def create_session(
|
|
|
564
467
|
expiry: datetime,
|
|
565
468
|
*,
|
|
566
469
|
ctx: SessionContext | None = None,
|
|
567
|
-
) ->
|
|
568
|
-
"""Create a new session."""
|
|
569
|
-
if key in _db.sessions:
|
|
570
|
-
raise ValueError("Session already exists")
|
|
470
|
+
) -> str:
|
|
471
|
+
"""Create a new session. Returns the session key."""
|
|
571
472
|
if user_uuid not in _db.users:
|
|
572
473
|
raise ValueError(f"User {user_uuid} not found")
|
|
573
474
|
if credential_uuid not in _db.credentials:
|
|
574
475
|
raise ValueError(f"Credential {credential_uuid} not found")
|
|
476
|
+
session = Session.create(
|
|
477
|
+
user=user_uuid,
|
|
478
|
+
credential=credential_uuid,
|
|
479
|
+
host=host,
|
|
480
|
+
ip=ip,
|
|
481
|
+
user_agent=user_agent,
|
|
482
|
+
expiry=expiry,
|
|
483
|
+
)
|
|
484
|
+
if session.key in _db.sessions:
|
|
485
|
+
raise ValueError("Session already exists")
|
|
575
486
|
with _db.transaction("create_session", ctx):
|
|
576
|
-
_db.sessions[key] =
|
|
577
|
-
|
|
578
|
-
credential=credential_uuid,
|
|
579
|
-
host=host,
|
|
580
|
-
ip=ip,
|
|
581
|
-
user_agent=user_agent,
|
|
582
|
-
expiry=expiry,
|
|
583
|
-
)
|
|
487
|
+
_db.sessions[session.key] = session
|
|
488
|
+
return session.key
|
|
584
489
|
|
|
585
490
|
|
|
586
491
|
def update_session(
|
|
@@ -612,16 +517,18 @@ def set_session_host(key: str, host: str, *, ctx: SessionContext | None = None)
|
|
|
612
517
|
update_session(key, host=host, ctx=ctx)
|
|
613
518
|
|
|
614
519
|
|
|
615
|
-
def delete_session(
|
|
520
|
+
def delete_session(
|
|
521
|
+
key: str, *, ctx: SessionContext | None = None, action: str = "delete_session"
|
|
522
|
+
) -> None:
|
|
616
523
|
"""Delete a session.
|
|
617
524
|
|
|
618
525
|
The acting user should be logged via ctx.
|
|
619
|
-
For user logout, pass ctx of the user's session.
|
|
526
|
+
For user logout, pass ctx of the user's session and action="logout".
|
|
620
527
|
For admin terminating a session, pass admin's ctx.
|
|
621
528
|
"""
|
|
622
529
|
if key not in _db.sessions:
|
|
623
530
|
raise ValueError("Session not found")
|
|
624
|
-
with _db.transaction(
|
|
531
|
+
with _db.transaction(action, ctx):
|
|
625
532
|
del _db.sessions[key]
|
|
626
533
|
|
|
627
534
|
|
|
@@ -634,10 +541,12 @@ def delete_sessions_for_user(
|
|
|
634
541
|
For user logout-all, pass ctx of the user's session.
|
|
635
542
|
For admin bulk termination, pass admin's ctx.
|
|
636
543
|
"""
|
|
544
|
+
user = _db.users.get(user_uuid)
|
|
545
|
+
if not user:
|
|
546
|
+
return
|
|
637
547
|
with _db.transaction("admin:delete_sessions_for_user", ctx):
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
del _db.sessions[k]
|
|
548
|
+
for sess in user.sessions:
|
|
549
|
+
del _db.sessions[sess.key]
|
|
641
550
|
|
|
642
551
|
|
|
643
552
|
def create_reset_token(
|
|
@@ -647,6 +556,7 @@ def create_reset_token(
|
|
|
647
556
|
token_type: str,
|
|
648
557
|
*,
|
|
649
558
|
ctx: SessionContext | None = None,
|
|
559
|
+
user: str | None = None,
|
|
650
560
|
) -> None:
|
|
651
561
|
"""Create a reset token from a passphrase.
|
|
652
562
|
|
|
@@ -654,15 +564,16 @@ def create_reset_token(
|
|
|
654
564
|
For self-service (user creating own recovery link), pass user's ctx.
|
|
655
565
|
For admin operations, pass admin's ctx.
|
|
656
566
|
For system operations (bootstrap), pass neither to log no user.
|
|
567
|
+
For API operations where ctx is not available but user is known, pass user.
|
|
657
568
|
"""
|
|
658
569
|
key = _reset_key(passphrase)
|
|
659
570
|
if key in _db.reset_tokens:
|
|
660
571
|
raise ValueError("Reset token already exists")
|
|
661
572
|
if user_uuid not in _db.users:
|
|
662
573
|
raise ValueError(f"User {user_uuid} not found")
|
|
663
|
-
with _db.transaction("create_reset_token", ctx):
|
|
574
|
+
with _db.transaction("create_reset_token", ctx, user=user):
|
|
664
575
|
_db.reset_tokens[key] = ResetToken(
|
|
665
|
-
|
|
576
|
+
user_uuid=user_uuid, expiry=expiry, token_type=token_type
|
|
666
577
|
)
|
|
667
578
|
|
|
668
579
|
|
|
@@ -681,7 +592,7 @@ def delete_reset_token(key: bytes, *, ctx: SessionContext | None = None) -> None
|
|
|
681
592
|
|
|
682
593
|
def cleanup_expired() -> int:
|
|
683
594
|
"""Remove expired sessions and reset tokens. Returns count removed."""
|
|
684
|
-
now = datetime.now(
|
|
595
|
+
now = datetime.now(UTC)
|
|
685
596
|
count = 0
|
|
686
597
|
with _db.transaction("expiry"):
|
|
687
598
|
expired_sessions = [k for k, s in _db.sessions.items() if s.expiry < now]
|
|
@@ -726,13 +637,20 @@ def login(
|
|
|
726
637
|
"""
|
|
727
638
|
if isinstance(user_uuid, str):
|
|
728
639
|
user_uuid = UUID(user_uuid)
|
|
729
|
-
now = datetime.now(
|
|
640
|
+
now = datetime.now(UTC)
|
|
730
641
|
if user_uuid not in _db.users:
|
|
731
642
|
raise ValueError(f"User {user_uuid} not found")
|
|
732
643
|
if credential_uuid not in _db.credentials:
|
|
733
644
|
raise ValueError(f"Credential {credential_uuid} not found")
|
|
734
645
|
|
|
735
|
-
|
|
646
|
+
session = Session.create(
|
|
647
|
+
user=user_uuid,
|
|
648
|
+
credential=credential_uuid,
|
|
649
|
+
host=host,
|
|
650
|
+
ip=ip,
|
|
651
|
+
user_agent=user_agent,
|
|
652
|
+
expiry=expiry,
|
|
653
|
+
)
|
|
736
654
|
user_str = str(user_uuid)
|
|
737
655
|
with _db.transaction("login", user=user_str):
|
|
738
656
|
# Update user
|
|
@@ -742,15 +660,8 @@ def login(
|
|
|
742
660
|
_db.credentials[credential_uuid].sign_count = sign_count
|
|
743
661
|
_db.credentials[credential_uuid].last_used = now
|
|
744
662
|
# Create session
|
|
745
|
-
_db.sessions[
|
|
746
|
-
|
|
747
|
-
credential=credential_uuid,
|
|
748
|
-
host=host,
|
|
749
|
-
ip=ip,
|
|
750
|
-
user_agent=user_agent,
|
|
751
|
-
expiry=expiry,
|
|
752
|
-
)
|
|
753
|
-
return session_key
|
|
663
|
+
_db.sessions[session.key] = session
|
|
664
|
+
return session.key
|
|
754
665
|
|
|
755
666
|
|
|
756
667
|
def create_credential_session(
|
|
@@ -773,13 +684,20 @@ def create_credential_session(
|
|
|
773
684
|
Returns the generated session token.
|
|
774
685
|
"""
|
|
775
686
|
|
|
776
|
-
now = datetime.now(
|
|
687
|
+
now = datetime.now(UTC)
|
|
777
688
|
expiry = now + SESSION_LIFETIME
|
|
778
|
-
session_key = _create_token()
|
|
779
689
|
|
|
780
690
|
if user_uuid not in _db.users:
|
|
781
691
|
raise ValueError(f"User {user_uuid} not found")
|
|
782
692
|
|
|
693
|
+
session = Session.create(
|
|
694
|
+
user=user_uuid,
|
|
695
|
+
credential=credential.uuid,
|
|
696
|
+
host=host,
|
|
697
|
+
ip=ip,
|
|
698
|
+
user_agent=user_agent,
|
|
699
|
+
expiry=expiry,
|
|
700
|
+
)
|
|
783
701
|
user_str = str(user_uuid)
|
|
784
702
|
with _db.transaction("create_credential_session", user=user_str):
|
|
785
703
|
# Update display name if provided
|
|
@@ -790,20 +708,13 @@ def create_credential_session(
|
|
|
790
708
|
_db.credentials[credential.uuid] = credential
|
|
791
709
|
|
|
792
710
|
# Create session
|
|
793
|
-
_db.sessions[
|
|
794
|
-
user=user_uuid,
|
|
795
|
-
credential=credential.uuid,
|
|
796
|
-
host=host,
|
|
797
|
-
ip=ip,
|
|
798
|
-
user_agent=user_agent,
|
|
799
|
-
expiry=expiry,
|
|
800
|
-
)
|
|
711
|
+
_db.sessions[session.key] = session
|
|
801
712
|
|
|
802
713
|
# Delete reset token if provided
|
|
803
714
|
if reset_key:
|
|
804
715
|
if reset_key in _db.reset_tokens:
|
|
805
716
|
del _db.reset_tokens[reset_key]
|
|
806
|
-
return
|
|
717
|
+
return session.key
|
|
807
718
|
|
|
808
719
|
|
|
809
720
|
# -------------------------------------------------------------------------
|
|
@@ -862,7 +773,7 @@ def bootstrap(
|
|
|
862
773
|
reset_expiry = reset_expires()
|
|
863
774
|
reset_key = _reset_key(reset_passphrase)
|
|
864
775
|
|
|
865
|
-
now = datetime.now(
|
|
776
|
+
now = datetime.now(UTC)
|
|
866
777
|
|
|
867
778
|
with _db.transaction("bootstrap"):
|
|
868
779
|
# Create auth:admin permission
|
|
@@ -884,13 +795,13 @@ def bootstrap(
|
|
|
884
795
|
_db.permissions[perm_org_admin_uuid] = perm_org_admin
|
|
885
796
|
|
|
886
797
|
# Create organization
|
|
887
|
-
new_org = Org(display_name=org_name)
|
|
798
|
+
new_org = Org.create(display_name=org_name)
|
|
888
799
|
new_org.uuid = org_uuid
|
|
889
800
|
_db.orgs[org_uuid] = new_org
|
|
890
801
|
|
|
891
802
|
# Create Administration role with both permissions
|
|
892
803
|
admin_role = Role(
|
|
893
|
-
|
|
804
|
+
org_uuid=org_uuid,
|
|
894
805
|
display_name="Administration",
|
|
895
806
|
permissions={perm_admin_uuid: True, perm_org_admin_uuid: True},
|
|
896
807
|
)
|
|
@@ -900,7 +811,7 @@ def bootstrap(
|
|
|
900
811
|
# Create admin user
|
|
901
812
|
admin_user = User(
|
|
902
813
|
display_name=admin_name,
|
|
903
|
-
|
|
814
|
+
role_uuid=role_uuid,
|
|
904
815
|
created_at=now,
|
|
905
816
|
last_seen=None,
|
|
906
817
|
visits=0,
|
|
@@ -910,7 +821,7 @@ def bootstrap(
|
|
|
910
821
|
|
|
911
822
|
# Create reset token
|
|
912
823
|
_db.reset_tokens[reset_key] = ResetToken(
|
|
913
|
-
|
|
824
|
+
user_uuid=user_uuid,
|
|
914
825
|
expiry=reset_expiry,
|
|
915
826
|
token_type="admin bootstrap",
|
|
916
827
|
)
|