paskia 0.9.0__py3-none-any.whl → 0.9.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/_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 +100 -112
- paskia/db/logging.py +233 -0
- paskia/db/migrations.py +19 -20
- paskia/db/operations.py +99 -192
- paskia/db/structs.py +236 -46
- paskia/fastapi/__main__.py +1 -0
- paskia/fastapi/admin.py +70 -193
- paskia/fastapi/api.py +49 -55
- paskia/fastapi/logging.py +218 -0
- paskia/fastapi/mainapp.py +12 -2
- paskia/fastapi/remote.py +4 -4
- paskia/fastapi/reset.py +0 -2
- paskia/fastapi/response.py +22 -0
- paskia/fastapi/user.py +7 -7
- paskia/fastapi/ws.py +6 -6
- paskia/fastapi/wsutil.py +15 -2
- 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.9.1.dist-info}/METADATA +1 -1
- {paskia-0.9.0.dist-info → paskia-0.9.1.dist-info}/RECORD +28 -25
- {paskia-0.9.0.dist-info → paskia-0.9.1.dist-info}/WHEEL +0 -0
- {paskia-0.9.0.dist-info → paskia-0.9.1.dist-info}/entry_points.txt +0 -0
paskia/db/migrations.py
CHANGED
|
@@ -5,30 +5,29 @@ Migrations are applied during database load based on the version field.
|
|
|
5
5
|
Each migration should be idempotent and only run when needed.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
9
|
|
|
10
|
-
_logger = logging.getLogger(__name__)
|
|
11
10
|
|
|
11
|
+
def migrate_v1(d: dict) -> None:
|
|
12
|
+
"""Remove Org.created_at fields."""
|
|
13
|
+
for org_data in d["orgs"].values():
|
|
14
|
+
org_data.pop("created_at", None)
|
|
12
15
|
|
|
13
|
-
def apply_migrations(data_dict: dict) -> bool:
|
|
14
|
-
"""Apply any pending schema migrations to the database dictionary.
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
migrations = sorted(
|
|
18
|
+
[f for n, f in globals().items() if n.startswith("migrate_v")],
|
|
19
|
+
key=lambda f: int(f.__name__.removeprefix("migrate_v")),
|
|
20
|
+
)
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
True if any migrations were applied, False otherwise
|
|
21
|
-
"""
|
|
22
|
-
db_version = data_dict.get("v", 0)
|
|
23
|
-
migrated = False
|
|
22
|
+
DBVER = len(migrations) # Used by bootstrap and migrate:sql to set initial version
|
|
24
23
|
|
|
25
|
-
if db_version == 0:
|
|
26
|
-
# Migration v0 -> v1: Remove created_at from orgs (field removed from schema)
|
|
27
|
-
if "orgs" in data_dict:
|
|
28
|
-
for org_data in data_dict["orgs"].values():
|
|
29
|
-
org_data.pop("created_at", None)
|
|
30
|
-
data_dict["v"] = 1
|
|
31
|
-
migrated = True
|
|
32
|
-
_logger.info("Applied schema migration: v0 -> v1 (removed org.created_at)")
|
|
33
24
|
|
|
34
|
-
|
|
25
|
+
async def apply_all_migrations(
|
|
26
|
+
data_dict: dict,
|
|
27
|
+
current_version: int,
|
|
28
|
+
persist: Callable[[str, int, dict], Awaitable[None]],
|
|
29
|
+
) -> None:
|
|
30
|
+
while current_version < DBVER:
|
|
31
|
+
migrations[current_version](data_dict)
|
|
32
|
+
current_version += 1
|
|
33
|
+
await persist(f"migrate:v{current_version}", current_version, data_dict)
|
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(
|
|
@@ -634,10 +539,12 @@ def delete_sessions_for_user(
|
|
|
634
539
|
For user logout-all, pass ctx of the user's session.
|
|
635
540
|
For admin bulk termination, pass admin's ctx.
|
|
636
541
|
"""
|
|
542
|
+
user = _db.users.get(user_uuid)
|
|
543
|
+
if not user:
|
|
544
|
+
return
|
|
637
545
|
with _db.transaction("admin:delete_sessions_for_user", ctx):
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
del _db.sessions[k]
|
|
546
|
+
for sess in user.sessions:
|
|
547
|
+
del _db.sessions[sess.key]
|
|
641
548
|
|
|
642
549
|
|
|
643
550
|
def create_reset_token(
|
|
@@ -662,7 +569,7 @@ def create_reset_token(
|
|
|
662
569
|
raise ValueError(f"User {user_uuid} not found")
|
|
663
570
|
with _db.transaction("create_reset_token", ctx):
|
|
664
571
|
_db.reset_tokens[key] = ResetToken(
|
|
665
|
-
|
|
572
|
+
user_uuid=user_uuid, expiry=expiry, token_type=token_type
|
|
666
573
|
)
|
|
667
574
|
|
|
668
575
|
|
|
@@ -681,7 +588,7 @@ def delete_reset_token(key: bytes, *, ctx: SessionContext | None = None) -> None
|
|
|
681
588
|
|
|
682
589
|
def cleanup_expired() -> int:
|
|
683
590
|
"""Remove expired sessions and reset tokens. Returns count removed."""
|
|
684
|
-
now = datetime.now(
|
|
591
|
+
now = datetime.now(UTC)
|
|
685
592
|
count = 0
|
|
686
593
|
with _db.transaction("expiry"):
|
|
687
594
|
expired_sessions = [k for k, s in _db.sessions.items() if s.expiry < now]
|
|
@@ -726,13 +633,20 @@ def login(
|
|
|
726
633
|
"""
|
|
727
634
|
if isinstance(user_uuid, str):
|
|
728
635
|
user_uuid = UUID(user_uuid)
|
|
729
|
-
now = datetime.now(
|
|
636
|
+
now = datetime.now(UTC)
|
|
730
637
|
if user_uuid not in _db.users:
|
|
731
638
|
raise ValueError(f"User {user_uuid} not found")
|
|
732
639
|
if credential_uuid not in _db.credentials:
|
|
733
640
|
raise ValueError(f"Credential {credential_uuid} not found")
|
|
734
641
|
|
|
735
|
-
|
|
642
|
+
session = Session.create(
|
|
643
|
+
user=user_uuid,
|
|
644
|
+
credential=credential_uuid,
|
|
645
|
+
host=host,
|
|
646
|
+
ip=ip,
|
|
647
|
+
user_agent=user_agent,
|
|
648
|
+
expiry=expiry,
|
|
649
|
+
)
|
|
736
650
|
user_str = str(user_uuid)
|
|
737
651
|
with _db.transaction("login", user=user_str):
|
|
738
652
|
# Update user
|
|
@@ -742,15 +656,8 @@ def login(
|
|
|
742
656
|
_db.credentials[credential_uuid].sign_count = sign_count
|
|
743
657
|
_db.credentials[credential_uuid].last_used = now
|
|
744
658
|
# 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
|
|
659
|
+
_db.sessions[session.key] = session
|
|
660
|
+
return session.key
|
|
754
661
|
|
|
755
662
|
|
|
756
663
|
def create_credential_session(
|
|
@@ -773,13 +680,20 @@ def create_credential_session(
|
|
|
773
680
|
Returns the generated session token.
|
|
774
681
|
"""
|
|
775
682
|
|
|
776
|
-
now = datetime.now(
|
|
683
|
+
now = datetime.now(UTC)
|
|
777
684
|
expiry = now + SESSION_LIFETIME
|
|
778
|
-
session_key = _create_token()
|
|
779
685
|
|
|
780
686
|
if user_uuid not in _db.users:
|
|
781
687
|
raise ValueError(f"User {user_uuid} not found")
|
|
782
688
|
|
|
689
|
+
session = Session.create(
|
|
690
|
+
user=user_uuid,
|
|
691
|
+
credential=credential.uuid,
|
|
692
|
+
host=host,
|
|
693
|
+
ip=ip,
|
|
694
|
+
user_agent=user_agent,
|
|
695
|
+
expiry=expiry,
|
|
696
|
+
)
|
|
783
697
|
user_str = str(user_uuid)
|
|
784
698
|
with _db.transaction("create_credential_session", user=user_str):
|
|
785
699
|
# Update display name if provided
|
|
@@ -790,20 +704,13 @@ def create_credential_session(
|
|
|
790
704
|
_db.credentials[credential.uuid] = credential
|
|
791
705
|
|
|
792
706
|
# 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
|
-
)
|
|
707
|
+
_db.sessions[session.key] = session
|
|
801
708
|
|
|
802
709
|
# Delete reset token if provided
|
|
803
710
|
if reset_key:
|
|
804
711
|
if reset_key in _db.reset_tokens:
|
|
805
712
|
del _db.reset_tokens[reset_key]
|
|
806
|
-
return
|
|
713
|
+
return session.key
|
|
807
714
|
|
|
808
715
|
|
|
809
716
|
# -------------------------------------------------------------------------
|
|
@@ -862,7 +769,7 @@ def bootstrap(
|
|
|
862
769
|
reset_expiry = reset_expires()
|
|
863
770
|
reset_key = _reset_key(reset_passphrase)
|
|
864
771
|
|
|
865
|
-
now = datetime.now(
|
|
772
|
+
now = datetime.now(UTC)
|
|
866
773
|
|
|
867
774
|
with _db.transaction("bootstrap"):
|
|
868
775
|
# Create auth:admin permission
|
|
@@ -884,13 +791,13 @@ def bootstrap(
|
|
|
884
791
|
_db.permissions[perm_org_admin_uuid] = perm_org_admin
|
|
885
792
|
|
|
886
793
|
# Create organization
|
|
887
|
-
new_org = Org(display_name=org_name)
|
|
794
|
+
new_org = Org.create(display_name=org_name)
|
|
888
795
|
new_org.uuid = org_uuid
|
|
889
796
|
_db.orgs[org_uuid] = new_org
|
|
890
797
|
|
|
891
798
|
# Create Administration role with both permissions
|
|
892
799
|
admin_role = Role(
|
|
893
|
-
|
|
800
|
+
org_uuid=org_uuid,
|
|
894
801
|
display_name="Administration",
|
|
895
802
|
permissions={perm_admin_uuid: True, perm_org_admin_uuid: True},
|
|
896
803
|
)
|
|
@@ -900,7 +807,7 @@ def bootstrap(
|
|
|
900
807
|
# Create admin user
|
|
901
808
|
admin_user = User(
|
|
902
809
|
display_name=admin_name,
|
|
903
|
-
|
|
810
|
+
role_uuid=role_uuid,
|
|
904
811
|
created_at=now,
|
|
905
812
|
last_seen=None,
|
|
906
813
|
visits=0,
|
|
@@ -910,7 +817,7 @@ def bootstrap(
|
|
|
910
817
|
|
|
911
818
|
# Create reset token
|
|
912
819
|
_db.reset_tokens[reset_key] = ResetToken(
|
|
913
|
-
|
|
820
|
+
user_uuid=user_uuid,
|
|
914
821
|
expiry=reset_expiry,
|
|
915
822
|
token_type="admin bootstrap",
|
|
916
823
|
)
|