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/fastapi/admin.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from datetime import timezone
|
|
3
2
|
from uuid import UUID
|
|
4
3
|
|
|
5
4
|
from fastapi import Body, FastAPI, HTTPException, Query, Request, Response
|
|
@@ -13,6 +12,7 @@ from paskia.db import Permission as PermDC
|
|
|
13
12
|
from paskia.db import Role as RoleDC
|
|
14
13
|
from paskia.db import User as UserDC
|
|
15
14
|
from paskia.fastapi import authz
|
|
15
|
+
from paskia.fastapi.response import MsgspecResponse
|
|
16
16
|
from paskia.fastapi.session import AUTH_COOKIE
|
|
17
17
|
from paskia.globals import passkey
|
|
18
18
|
from paskia.util import (
|
|
@@ -20,46 +20,26 @@ from paskia.util import (
|
|
|
20
20
|
passphrase,
|
|
21
21
|
permutil,
|
|
22
22
|
querysafe,
|
|
23
|
-
useragent,
|
|
24
23
|
vitedev,
|
|
25
24
|
)
|
|
25
|
+
from paskia.util.apistructs import ApiPermission, ApiSession, format_datetime
|
|
26
26
|
from paskia.util.hostutil import normalize_host
|
|
27
27
|
|
|
28
28
|
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def
|
|
32
|
-
""
|
|
33
|
-
effective_scopes = (
|
|
34
|
-
{p.scope for p in (ctx.permissions or [])}
|
|
35
|
-
if ctx.permissions
|
|
36
|
-
else set(ctx.role.permissions or [])
|
|
37
|
-
)
|
|
38
|
-
return "auth:admin" in effective_scopes
|
|
39
|
-
|
|
31
|
+
def master_admin(ctx) -> bool:
|
|
32
|
+
return any(p.scope == "auth:admin" for p in ctx.permissions)
|
|
40
33
|
|
|
41
|
-
def is_org_admin(ctx, org_uuid: UUID | None = None) -> bool:
|
|
42
|
-
"""Check if user has org admin permission.
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
effective_scopes = (
|
|
48
|
-
{p.scope for p in (ctx.permissions or [])}
|
|
49
|
-
if ctx.permissions
|
|
50
|
-
else set(ctx.role.permissions or [])
|
|
35
|
+
def org_admin(ctx, org_uuid: UUID) -> bool:
|
|
36
|
+
return ctx.org.uuid == org_uuid and any(
|
|
37
|
+
p.scope == "auth:org:admin" for p in ctx.permissions
|
|
51
38
|
)
|
|
52
|
-
if "auth:org:admin" not in effective_scopes:
|
|
53
|
-
return False
|
|
54
|
-
if org_uuid is None:
|
|
55
|
-
return True
|
|
56
|
-
# User must belong to the target org (via their role)
|
|
57
|
-
return ctx.org.uuid == org_uuid
|
|
58
39
|
|
|
59
40
|
|
|
60
41
|
def can_manage_org(ctx, org_uuid: UUID) -> bool:
|
|
61
|
-
|
|
62
|
-
return is_global_admin(ctx) or is_org_admin(ctx, org_uuid)
|
|
42
|
+
return master_admin(ctx) or org_admin(ctx, org_uuid)
|
|
63
43
|
|
|
64
44
|
|
|
65
45
|
@app.exception_handler(ValueError)
|
|
@@ -99,42 +79,38 @@ async def admin_list_orgs(request: Request, auth=AUTH_COOKIE):
|
|
|
99
79
|
host=request.headers.get("host"),
|
|
100
80
|
)
|
|
101
81
|
orgs = list(db.data().orgs.values())
|
|
102
|
-
if not
|
|
82
|
+
if not master_admin(ctx):
|
|
103
83
|
# Org admins can only see their own organization
|
|
104
84
|
orgs = [o for o in orgs if o.uuid == ctx.org.uuid]
|
|
105
85
|
|
|
106
|
-
def
|
|
107
|
-
return {
|
|
108
|
-
"uuid": str(r.uuid),
|
|
109
|
-
"org": str(r.org),
|
|
110
|
-
"display_name": r.display_name,
|
|
111
|
-
"permissions": list(r.permissions.keys()),
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async def org_to_dict(o):
|
|
86
|
+
def org_to_dict(o):
|
|
115
87
|
users = db.get_organization_users(o.uuid)
|
|
116
88
|
return {
|
|
117
|
-
"uuid":
|
|
89
|
+
"uuid": o.uuid,
|
|
118
90
|
"display_name": o.display_name,
|
|
119
|
-
"permissions": {
|
|
120
|
-
pid for pid, p in db.data().permissions.items() if o.uuid in p.orgs
|
|
121
|
-
},
|
|
91
|
+
"permissions": {p.uuid for p in o.permissions},
|
|
122
92
|
"roles": [
|
|
123
|
-
|
|
93
|
+
{
|
|
94
|
+
"uuid": r.uuid,
|
|
95
|
+
"org": r.org_uuid,
|
|
96
|
+
"display_name": r.display_name,
|
|
97
|
+
"permissions": list(r.permissions.keys()),
|
|
98
|
+
}
|
|
99
|
+
for r in o.roles
|
|
124
100
|
],
|
|
125
101
|
"users": [
|
|
126
102
|
{
|
|
127
|
-
"uuid":
|
|
103
|
+
"uuid": u.uuid,
|
|
128
104
|
"display_name": u.display_name,
|
|
129
105
|
"role": role_name,
|
|
130
106
|
"visits": u.visits,
|
|
131
|
-
"last_seen": u.last_seen
|
|
107
|
+
"last_seen": u.last_seen,
|
|
132
108
|
}
|
|
133
109
|
for (u, role_name) in users
|
|
134
110
|
],
|
|
135
111
|
}
|
|
136
112
|
|
|
137
|
-
return [
|
|
113
|
+
return MsgspecResponse([org_to_dict(o) for o in orgs])
|
|
138
114
|
|
|
139
115
|
|
|
140
116
|
@app.post("/orgs")
|
|
@@ -283,7 +259,8 @@ async def admin_create_role(
|
|
|
283
259
|
perms = payload.get("permissions") or []
|
|
284
260
|
if org_uuid not in db.data().orgs:
|
|
285
261
|
raise HTTPException(status_code=404, detail="Organization not found")
|
|
286
|
-
|
|
262
|
+
org = db.data().orgs[org_uuid]
|
|
263
|
+
grantable = {p.uuid for p in org.permissions}
|
|
287
264
|
|
|
288
265
|
# Normalize permission IDs to UUIDs
|
|
289
266
|
permission_uuids: set[UUID] = set()
|
|
@@ -324,7 +301,7 @@ async def admin_update_role_name(
|
|
|
324
301
|
status_code=403, detail="Insufficient permissions", mode="forbidden"
|
|
325
302
|
)
|
|
326
303
|
role = db.data().roles.get(role_uuid)
|
|
327
|
-
if not role or role.
|
|
304
|
+
if not role or role.org_uuid != org_uuid:
|
|
328
305
|
raise HTTPException(status_code=404, detail="Role not found in organization")
|
|
329
306
|
|
|
330
307
|
display_name = payload.get("display_name")
|
|
@@ -356,7 +333,7 @@ async def admin_add_role_permission(
|
|
|
356
333
|
)
|
|
357
334
|
|
|
358
335
|
role = db.data().roles.get(role_uuid)
|
|
359
|
-
if not role or role.
|
|
336
|
+
if not role or role.org_uuid != org_uuid:
|
|
360
337
|
raise HTTPException(status_code=404, detail="Role not found in organization")
|
|
361
338
|
|
|
362
339
|
# Verify permission exists and org can grant it
|
|
@@ -391,7 +368,7 @@ async def admin_remove_role_permission(
|
|
|
391
368
|
)
|
|
392
369
|
|
|
393
370
|
role = db.data().roles.get(role_uuid)
|
|
394
|
-
if not role or role.
|
|
371
|
+
if not role or role.org_uuid != org_uuid:
|
|
395
372
|
raise HTTPException(status_code=404, detail="Role not found in organization")
|
|
396
373
|
|
|
397
374
|
# Sanity check: prevent admin from removing their own access
|
|
@@ -432,7 +409,7 @@ async def admin_delete_role(
|
|
|
432
409
|
status_code=403, detail="Insufficient permissions", mode="forbidden"
|
|
433
410
|
)
|
|
434
411
|
role = db.data().roles.get(role_uuid)
|
|
435
|
-
if not role or role.
|
|
412
|
+
if not role or role.org_uuid != org_uuid:
|
|
436
413
|
raise HTTPException(status_code=404, detail="Role not found in organization")
|
|
437
414
|
|
|
438
415
|
# Sanity check: prevent admin from deleting their own role
|
|
@@ -468,8 +445,11 @@ async def admin_create_user(
|
|
|
468
445
|
if not display_name or not role_name:
|
|
469
446
|
raise ValueError("display_name and role are required")
|
|
470
447
|
|
|
471
|
-
|
|
472
|
-
role_obj = next(
|
|
448
|
+
org = db.data().orgs[org_uuid]
|
|
449
|
+
role_obj = next(
|
|
450
|
+
(r for r in org.roles if r.display_name == role_name),
|
|
451
|
+
None,
|
|
452
|
+
)
|
|
473
453
|
if not role_obj:
|
|
474
454
|
raise ValueError("Role not found in organization")
|
|
475
455
|
user = UserDC.create(
|
|
@@ -507,7 +487,7 @@ async def admin_update_user_role(
|
|
|
507
487
|
raise ValueError("User not found")
|
|
508
488
|
if user_org.uuid != org_uuid:
|
|
509
489
|
raise ValueError("User does not belong to this organization")
|
|
510
|
-
roles =
|
|
490
|
+
roles = user_org.roles
|
|
511
491
|
if not any(r.display_name == new_role for r in roles):
|
|
512
492
|
raise ValueError("Role not found in organization")
|
|
513
493
|
|
|
@@ -572,11 +552,7 @@ async def admin_create_user_registration_link(
|
|
|
572
552
|
url = hostutil.reset_link_url(token)
|
|
573
553
|
return {
|
|
574
554
|
"url": url,
|
|
575
|
-
"expires": (
|
|
576
|
-
expiry.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
577
|
-
if expiry.tzinfo
|
|
578
|
-
else expiry.replace(tzinfo=timezone.utc).isoformat().replace("+00:00", "Z")
|
|
579
|
-
),
|
|
555
|
+
"expires": format_datetime(expiry),
|
|
580
556
|
}
|
|
581
557
|
|
|
582
558
|
|
|
@@ -604,120 +580,39 @@ async def admin_get_user_detail(
|
|
|
604
580
|
status_code=403, detail="Insufficient permissions", mode="forbidden"
|
|
605
581
|
)
|
|
606
582
|
user = db.data().users.get(user_uuid)
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
.
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
.
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if c.last_used
|
|
636
|
-
else None
|
|
637
|
-
)
|
|
638
|
-
),
|
|
639
|
-
"last_verified": (
|
|
640
|
-
c.last_verified.astimezone(timezone.utc)
|
|
641
|
-
.isoformat()
|
|
642
|
-
.replace("+00:00", "Z")
|
|
643
|
-
if c.last_verified and c.last_verified.tzinfo
|
|
644
|
-
else (
|
|
645
|
-
c.last_verified.replace(tzinfo=timezone.utc)
|
|
646
|
-
.isoformat()
|
|
647
|
-
.replace("+00:00", "Z")
|
|
648
|
-
if c.last_verified
|
|
649
|
-
else None
|
|
650
|
-
)
|
|
583
|
+
normalized_host = hostutil.normalize_host(request.headers.get("host"))
|
|
584
|
+
|
|
585
|
+
return MsgspecResponse(
|
|
586
|
+
{
|
|
587
|
+
"display_name": user.display_name,
|
|
588
|
+
"org": {"display_name": user_org.display_name},
|
|
589
|
+
"role": role_name,
|
|
590
|
+
"visits": user.visits,
|
|
591
|
+
"created_at": user.created_at,
|
|
592
|
+
"last_seen": user.last_seen,
|
|
593
|
+
"credentials": [
|
|
594
|
+
{
|
|
595
|
+
"credential": c.uuid,
|
|
596
|
+
"aaguid": c.aaguid,
|
|
597
|
+
"created_at": c.created_at,
|
|
598
|
+
"last_used": c.last_used,
|
|
599
|
+
"last_verified": c.last_verified,
|
|
600
|
+
"sign_count": c.sign_count,
|
|
601
|
+
}
|
|
602
|
+
for c in user.credentials
|
|
603
|
+
],
|
|
604
|
+
"aaguid_info": aaguid_mod.filter(c.aaguid for c in user.credentials),
|
|
605
|
+
"sessions": [
|
|
606
|
+
ApiSession.from_db(
|
|
607
|
+
s,
|
|
608
|
+
current_key=auth,
|
|
609
|
+
normalized_host=normalized_host,
|
|
610
|
+
expires_delta=EXPIRES,
|
|
651
611
|
)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
)
|
|
657
|
-
|
|
658
|
-
aaguid_info = aaguid_mod.filter(aaguids)
|
|
659
|
-
|
|
660
|
-
# Get sessions for the user
|
|
661
|
-
normalized_request_host = hostutil.normalize_host(request.headers.get("host"))
|
|
662
|
-
session_records = [s for s in db.data().sessions.values() if s.user == user_uuid]
|
|
663
|
-
current_session_key = auth
|
|
664
|
-
sessions_payload: list[dict] = []
|
|
665
|
-
for entry in session_records:
|
|
666
|
-
renewed = entry.expiry - EXPIRES
|
|
667
|
-
sessions_payload.append(
|
|
668
|
-
{
|
|
669
|
-
"id": entry.key,
|
|
670
|
-
"credential": str(entry.credential),
|
|
671
|
-
"host": entry.host,
|
|
672
|
-
"ip": entry.ip,
|
|
673
|
-
"user_agent": useragent.compact_user_agent(entry.user_agent),
|
|
674
|
-
"last_renewed": (
|
|
675
|
-
renewed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
676
|
-
if renewed.tzinfo
|
|
677
|
-
else renewed.replace(tzinfo=timezone.utc)
|
|
678
|
-
.isoformat()
|
|
679
|
-
.replace("+00:00", "Z")
|
|
680
|
-
),
|
|
681
|
-
"is_current": entry.key == current_session_key,
|
|
682
|
-
"is_current_host": bool(
|
|
683
|
-
normalized_request_host
|
|
684
|
-
and entry.host
|
|
685
|
-
and entry.host == normalized_request_host
|
|
686
|
-
),
|
|
687
|
-
}
|
|
688
|
-
)
|
|
689
|
-
|
|
690
|
-
return {
|
|
691
|
-
"display_name": user.display_name,
|
|
692
|
-
"org": {"display_name": user_org.display_name},
|
|
693
|
-
"role": role_name,
|
|
694
|
-
"visits": user.visits,
|
|
695
|
-
"created_at": (
|
|
696
|
-
user.created_at.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
697
|
-
if user.created_at and user.created_at.tzinfo
|
|
698
|
-
else (
|
|
699
|
-
user.created_at.replace(tzinfo=timezone.utc)
|
|
700
|
-
.isoformat()
|
|
701
|
-
.replace("+00:00", "Z")
|
|
702
|
-
if user.created_at
|
|
703
|
-
else None
|
|
704
|
-
)
|
|
705
|
-
),
|
|
706
|
-
"last_seen": (
|
|
707
|
-
user.last_seen.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
708
|
-
if user.last_seen and user.last_seen.tzinfo
|
|
709
|
-
else (
|
|
710
|
-
user.last_seen.replace(tzinfo=timezone.utc)
|
|
711
|
-
.isoformat()
|
|
712
|
-
.replace("+00:00", "Z")
|
|
713
|
-
if user.last_seen
|
|
714
|
-
else None
|
|
715
|
-
)
|
|
716
|
-
),
|
|
717
|
-
"credentials": creds,
|
|
718
|
-
"aaguid_info": aaguid_info,
|
|
719
|
-
"sessions": sessions_payload,
|
|
720
|
-
}
|
|
612
|
+
for s in user.sessions
|
|
613
|
+
],
|
|
614
|
+
}
|
|
615
|
+
)
|
|
721
616
|
|
|
722
617
|
|
|
723
618
|
@app.patch("/orgs/{org_uuid}/users/{user_uuid}/display-name")
|
|
@@ -808,7 +703,7 @@ async def admin_delete_user_session(
|
|
|
808
703
|
)
|
|
809
704
|
|
|
810
705
|
target_session = db.data().sessions.get(session_id)
|
|
811
|
-
if not target_session or target_session.
|
|
706
|
+
if not target_session or target_session.user_uuid != user_uuid:
|
|
812
707
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
813
708
|
|
|
814
709
|
db.delete_session(session_id, ctx=ctx)
|
|
@@ -821,14 +716,6 @@ async def admin_delete_user_session(
|
|
|
821
716
|
# -------------------- Permissions (global) --------------------
|
|
822
717
|
|
|
823
718
|
|
|
824
|
-
def _perm_to_dict(p):
|
|
825
|
-
"""Convert Permission to dict, omitting domain if None."""
|
|
826
|
-
d = {"uuid": str(p.uuid), "scope": p.scope, "display_name": p.display_name}
|
|
827
|
-
if p.domain is not None:
|
|
828
|
-
d["domain"] = p.domain
|
|
829
|
-
return d
|
|
830
|
-
|
|
831
|
-
|
|
832
719
|
def _validate_permission_domain(domain: str | None) -> None:
|
|
833
720
|
"""Validate that domain is rp_id or a subdomain of it."""
|
|
834
721
|
if domain is None:
|
|
@@ -919,18 +806,8 @@ async def admin_list_permissions(request: Request, auth=AUTH_COOKIE):
|
|
|
919
806
|
match=permutil.has_any,
|
|
920
807
|
host=request.headers.get("host"),
|
|
921
808
|
)
|
|
922
|
-
perms =
|
|
923
|
-
|
|
924
|
-
# Global admins see all permissions
|
|
925
|
-
if is_global_admin(ctx):
|
|
926
|
-
return [_perm_to_dict(p) for p in perms]
|
|
927
|
-
|
|
928
|
-
# Org admins only see permissions their org can grant (by UUID)
|
|
929
|
-
grantable = {
|
|
930
|
-
pid for pid, p in db.data().permissions.items() if ctx.org.uuid in p.orgs
|
|
931
|
-
}
|
|
932
|
-
filtered_perms = [p for p in perms if p.uuid in grantable]
|
|
933
|
-
return [_perm_to_dict(p) for p in filtered_perms]
|
|
809
|
+
perms = db.data().permissions.values() if master_admin(ctx) else ctx.org.permissions
|
|
810
|
+
return MsgspecResponse([ApiPermission.from_db(p) for p in perms])
|
|
934
811
|
|
|
935
812
|
|
|
936
813
|
@app.post("/permissions")
|
paskia/fastapi/api.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from contextlib import suppress
|
|
3
|
-
from datetime import datetime, timedelta
|
|
3
|
+
from datetime import UTC, datetime, timedelta
|
|
4
4
|
|
|
5
5
|
from fastapi import (
|
|
6
6
|
Depends,
|
|
@@ -14,12 +14,9 @@ from fastapi.responses import JSONResponse
|
|
|
14
14
|
from fastapi.security import HTTPBearer
|
|
15
15
|
|
|
16
16
|
from paskia import db
|
|
17
|
-
from paskia.authsession import
|
|
18
|
-
EXPIRES,
|
|
19
|
-
get_reset,
|
|
20
|
-
refresh_session_token,
|
|
21
|
-
)
|
|
17
|
+
from paskia.authsession import EXPIRES, expires, get_reset
|
|
22
18
|
from paskia.fastapi import authz, session, user
|
|
19
|
+
from paskia.fastapi.response import MsgspecResponse
|
|
23
20
|
from paskia.fastapi.session import AUTH_COOKIE, AUTH_COOKIE_NAME
|
|
24
21
|
from paskia.globals import passkey as global_passkey
|
|
25
22
|
from paskia.util import hostutil, htmlutil, passphrase, userinfo, vitedev
|
|
@@ -90,44 +87,23 @@ async def validate_token(
|
|
|
90
87
|
raise
|
|
91
88
|
renewed = False
|
|
92
89
|
if auth:
|
|
93
|
-
consumed = EXPIRES - (ctx.session.expiry - datetime.now(
|
|
90
|
+
consumed = EXPIRES - (ctx.session.expiry - datetime.now(UTC))
|
|
94
91
|
if not timedelta(0) < consumed < _REFRESH_INTERVAL:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"renewed": renewed,
|
|
111
|
-
"ctx": userinfo.format_session_context(ctx),
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
@app.get("/token-info")
|
|
116
|
-
async def token_info(credentials=Depends(bearer_auth)):
|
|
117
|
-
"""Get reset/device-add token info. Pass token via Bearer header."""
|
|
118
|
-
token = credentials.credentials
|
|
119
|
-
if not passphrase.is_well_formed(token):
|
|
120
|
-
raise HTTPException(400, "Invalid token format")
|
|
121
|
-
try:
|
|
122
|
-
reset_token = get_reset(token)
|
|
123
|
-
except ValueError as e:
|
|
124
|
-
raise HTTPException(401, str(e))
|
|
125
|
-
|
|
126
|
-
u = db.data().users.get(reset_token.user)
|
|
127
|
-
return {
|
|
128
|
-
"token_type": reset_token.token_type,
|
|
129
|
-
"display_name": u.display_name,
|
|
130
|
-
}
|
|
92
|
+
db.update_session(
|
|
93
|
+
auth,
|
|
94
|
+
ip=request.client.host if request.client else "",
|
|
95
|
+
user_agent=request.headers.get("user-agent") or "",
|
|
96
|
+
expiry=expires(),
|
|
97
|
+
)
|
|
98
|
+
session.set_session_cookie(response, auth)
|
|
99
|
+
renewed = True
|
|
100
|
+
return MsgspecResponse(
|
|
101
|
+
{
|
|
102
|
+
"valid": True,
|
|
103
|
+
"renewed": renewed,
|
|
104
|
+
"ctx": userinfo.build_session_context(ctx),
|
|
105
|
+
}
|
|
106
|
+
)
|
|
131
107
|
|
|
132
108
|
|
|
133
109
|
@app.get("/forward")
|
|
@@ -170,11 +146,9 @@ async def forward_authentication(
|
|
|
170
146
|
"Remote-Role": str(ctx.role.uuid),
|
|
171
147
|
"Remote-Role-Name": ctx.role.display_name,
|
|
172
148
|
"Remote-Session-Expires": (
|
|
173
|
-
ctx.session.expiry.astimezone(
|
|
174
|
-
.isoformat()
|
|
175
|
-
.replace("+00:00", "Z")
|
|
149
|
+
ctx.session.expiry.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
|
176
150
|
if ctx.session.expiry.tzinfo
|
|
177
|
-
else ctx.session.expiry.replace(tzinfo=
|
|
151
|
+
else ctx.session.expiry.replace(tzinfo=UTC)
|
|
178
152
|
.isoformat()
|
|
179
153
|
.replace("+00:00", "Z")
|
|
180
154
|
),
|
|
@@ -233,24 +207,44 @@ async def api_user_info(
|
|
|
233
207
|
detail="Authentication required",
|
|
234
208
|
mode="login",
|
|
235
209
|
)
|
|
236
|
-
ctx = db.
|
|
210
|
+
ctx = db.data().session_ctx(auth, request.headers.get("host"))
|
|
237
211
|
if not ctx:
|
|
238
212
|
raise HTTPException(401, "Session expired")
|
|
239
213
|
|
|
240
|
-
return
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
214
|
+
return MsgspecResponse(
|
|
215
|
+
await userinfo.build_user_info(
|
|
216
|
+
user_uuid=ctx.user.uuid,
|
|
217
|
+
auth=auth,
|
|
218
|
+
session_record=ctx.session,
|
|
219
|
+
request_host=request.headers.get("host"),
|
|
220
|
+
)
|
|
245
221
|
)
|
|
246
222
|
|
|
247
223
|
|
|
224
|
+
@app.get("/token-info")
|
|
225
|
+
async def token_info(credentials=Depends(bearer_auth)):
|
|
226
|
+
"""Get reset/device-add token info. Pass token via Bearer header."""
|
|
227
|
+
token = credentials.credentials
|
|
228
|
+
if not passphrase.is_well_formed(token):
|
|
229
|
+
raise HTTPException(400, "Invalid token format")
|
|
230
|
+
try:
|
|
231
|
+
reset_token = get_reset(token)
|
|
232
|
+
except ValueError as e:
|
|
233
|
+
raise HTTPException(401, str(e))
|
|
234
|
+
|
|
235
|
+
u = reset_token.user
|
|
236
|
+
return {
|
|
237
|
+
"token_type": reset_token.token_type,
|
|
238
|
+
"display_name": u.display_name,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
248
242
|
@app.post("/logout")
|
|
249
243
|
async def api_logout(request: Request, response: Response, auth=AUTH_COOKIE):
|
|
250
244
|
if not auth:
|
|
251
245
|
return {"message": "Already logged out"}
|
|
252
246
|
host = request.headers.get("host")
|
|
253
|
-
ctx = db.
|
|
247
|
+
ctx = db.data().session_ctx(auth, host)
|
|
254
248
|
if not ctx:
|
|
255
249
|
return {"message": "Already logged out"}
|
|
256
250
|
with suppress(Exception):
|
|
@@ -263,7 +257,7 @@ async def api_logout(request: Request, response: Response, auth=AUTH_COOKIE):
|
|
|
263
257
|
async def api_set_session(
|
|
264
258
|
request: Request, response: Response, auth=Depends(bearer_auth)
|
|
265
259
|
):
|
|
266
|
-
ctx = db.
|
|
260
|
+
ctx = db.data().session_ctx(auth.credentials, request.headers.get("host"))
|
|
267
261
|
if not ctx:
|
|
268
262
|
raise HTTPException(401, "Session expired")
|
|
269
263
|
session.set_session_cookie(response, auth.credentials)
|