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/structs.py
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from datetime import UTC, datetime
|
|
2
5
|
from uuid import UUID
|
|
3
6
|
|
|
4
7
|
import msgspec
|
|
5
8
|
import uuid7
|
|
6
9
|
|
|
10
|
+
from paskia import db
|
|
11
|
+
from paskia.util.hostutil import normalize_host
|
|
12
|
+
|
|
7
13
|
# Sentinel for uuid fields before they are set by create() or DB post init
|
|
8
14
|
_UUID_UNSET = UUID(int=0)
|
|
9
15
|
|
|
@@ -22,20 +28,30 @@ class Permission(msgspec.Struct, dict=True, omit_defaults=True):
|
|
|
22
28
|
orgs: dict[UUID, bool] = {} # org_uuid -> True (which orgs can grant this)
|
|
23
29
|
|
|
24
30
|
def __post_init__(self):
|
|
25
|
-
|
|
31
|
+
if not hasattr(self, "uuid"):
|
|
32
|
+
self.uuid: UUID = _UUID_UNSET
|
|
26
33
|
|
|
27
34
|
@property
|
|
28
35
|
def org_set(self) -> set[UUID]:
|
|
29
36
|
"""Get orgs that can grant this permission as a set."""
|
|
30
37
|
return set(self.orgs.keys())
|
|
31
38
|
|
|
39
|
+
@property
|
|
40
|
+
def orgs_list(self) -> list[Org]:
|
|
41
|
+
"""Get list of Org objects that can grant this permission."""
|
|
42
|
+
return [
|
|
43
|
+
db.data().orgs[org_uuid]
|
|
44
|
+
for org_uuid in self.orgs.keys()
|
|
45
|
+
if org_uuid in db.data().orgs
|
|
46
|
+
]
|
|
47
|
+
|
|
32
48
|
@classmethod
|
|
33
49
|
def create(
|
|
34
50
|
cls,
|
|
35
51
|
scope: str,
|
|
36
52
|
display_name: str,
|
|
37
53
|
domain: str | None = None,
|
|
38
|
-
) ->
|
|
54
|
+
) -> Permission:
|
|
39
55
|
"""Create a new Permission with auto-generated uuid7."""
|
|
40
56
|
perm = cls(
|
|
41
57
|
scope=scope,
|
|
@@ -46,36 +62,84 @@ class Permission(msgspec.Struct, dict=True, omit_defaults=True):
|
|
|
46
62
|
return perm
|
|
47
63
|
|
|
48
64
|
|
|
65
|
+
class Org(msgspec.Struct, dict=True):
|
|
66
|
+
"""Organization data structure."""
|
|
67
|
+
|
|
68
|
+
display_name: str
|
|
69
|
+
|
|
70
|
+
def __post_init__(self):
|
|
71
|
+
if not hasattr(self, "uuid"):
|
|
72
|
+
self.uuid: UUID = _UUID_UNSET
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def roles(self) -> list[Role]:
|
|
76
|
+
"""Get all roles that belong to this organization."""
|
|
77
|
+
return [r for r in db.data().roles.values() if r.org_uuid == self.uuid]
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def permissions(self) -> list[Permission]:
|
|
81
|
+
"""Get all permissions that this organization can grant."""
|
|
82
|
+
return [p for p in db.data().permissions.values() if self.uuid in p.orgs]
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def create(cls, display_name: str) -> Org:
|
|
86
|
+
"""Create a new Org with auto-generated uuid7."""
|
|
87
|
+
org = cls(display_name=display_name)
|
|
88
|
+
org.uuid = uuid7.create()
|
|
89
|
+
return org
|
|
90
|
+
|
|
91
|
+
|
|
49
92
|
class Role(msgspec.Struct, dict=True, omit_defaults=True):
|
|
50
93
|
"""Role data structure.
|
|
51
94
|
|
|
52
95
|
Mutable fields: display_name, permissions
|
|
53
|
-
Immutable fields:
|
|
96
|
+
Immutable fields: org_uuid (set at creation, never modified)
|
|
54
97
|
uuid is generated at creation.
|
|
55
98
|
"""
|
|
56
99
|
|
|
57
|
-
|
|
100
|
+
org_uuid: UUID = msgspec.field(name="org")
|
|
58
101
|
display_name: str
|
|
59
102
|
permissions: dict[UUID, bool] = {} # permission_uuid -> True
|
|
60
103
|
|
|
61
104
|
def __post_init__(self):
|
|
62
|
-
|
|
105
|
+
if not hasattr(self, "uuid"):
|
|
106
|
+
self.uuid: UUID = _UUID_UNSET
|
|
63
107
|
|
|
64
108
|
@property
|
|
65
109
|
def permission_set(self) -> set[UUID]:
|
|
66
110
|
"""Get permissions as a set of UUIDs."""
|
|
67
111
|
return set(self.permissions.keys())
|
|
68
112
|
|
|
113
|
+
@property
|
|
114
|
+
def permissions_list(self) -> list[Permission]:
|
|
115
|
+
"""Get list of Permission objects for this role."""
|
|
116
|
+
return [
|
|
117
|
+
db.data().permissions[perm_uuid]
|
|
118
|
+
for perm_uuid in self.permissions.keys()
|
|
119
|
+
if perm_uuid in db.data().permissions
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def org(self) -> Org:
|
|
124
|
+
"""Get the organization object this role belongs to."""
|
|
125
|
+
return db.data().orgs[self.org_uuid]
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def users(self) -> list[User]:
|
|
129
|
+
"""Get all users that have this role."""
|
|
130
|
+
return [u for u in db.data().users.values() if u.role_uuid == self.uuid]
|
|
131
|
+
|
|
69
132
|
@classmethod
|
|
70
133
|
def create(
|
|
71
134
|
cls,
|
|
72
|
-
org: UUID,
|
|
135
|
+
org: UUID | Org,
|
|
73
136
|
display_name: str,
|
|
74
137
|
permissions: set[UUID] | None = None,
|
|
75
|
-
) ->
|
|
138
|
+
) -> Role:
|
|
76
139
|
"""Create a new Role with auto-generated uuid7."""
|
|
140
|
+
org_uuid = org if isinstance(org, UUID) else org.uuid
|
|
77
141
|
role = cls(
|
|
78
|
-
|
|
142
|
+
org_uuid=org_uuid,
|
|
79
143
|
display_name=display_name,
|
|
80
144
|
permissions={p: True for p in (permissions or set())},
|
|
81
145
|
)
|
|
@@ -83,52 +147,62 @@ class Role(msgspec.Struct, dict=True, omit_defaults=True):
|
|
|
83
147
|
return role
|
|
84
148
|
|
|
85
149
|
|
|
86
|
-
class Org(msgspec.Struct, dict=True):
|
|
87
|
-
"""Organization data structure."""
|
|
88
|
-
|
|
89
|
-
display_name: str
|
|
90
|
-
|
|
91
|
-
def __post_init__(self):
|
|
92
|
-
self.uuid: UUID = _UUID_UNSET # Convenience field, not serialized
|
|
93
|
-
|
|
94
|
-
@classmethod
|
|
95
|
-
def create(cls, display_name: str) -> "Org":
|
|
96
|
-
"""Create a new Org with auto-generated uuid7."""
|
|
97
|
-
org = cls(display_name=display_name)
|
|
98
|
-
org.uuid = uuid7.create()
|
|
99
|
-
return org
|
|
100
|
-
|
|
101
|
-
|
|
102
150
|
class User(msgspec.Struct, dict=True):
|
|
103
151
|
"""User data structure.
|
|
104
152
|
|
|
105
|
-
Mutable fields: display_name,
|
|
153
|
+
Mutable fields: display_name, role_uuid, last_seen, visits
|
|
106
154
|
Immutable fields: created_at (set at creation, never modified)
|
|
107
155
|
uuid is derived from created_at using uuid7.
|
|
108
156
|
"""
|
|
109
157
|
|
|
110
158
|
display_name: str
|
|
111
|
-
|
|
159
|
+
role_uuid: UUID = msgspec.field(name="role")
|
|
112
160
|
created_at: datetime
|
|
113
161
|
last_seen: datetime | None = None
|
|
114
162
|
visits: int = 0
|
|
115
163
|
|
|
116
164
|
def __post_init__(self):
|
|
117
|
-
|
|
165
|
+
if not hasattr(self, "uuid"):
|
|
166
|
+
self.uuid: UUID = _UUID_UNSET
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def role(self) -> Role:
|
|
170
|
+
"""Get the role object this user has."""
|
|
171
|
+
return db.data().roles[self.role_uuid]
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def org(self) -> Org:
|
|
175
|
+
"""Get the organization this user belongs to (via role)."""
|
|
176
|
+
return self.role.org
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def credentials(self) -> list[Credential]:
|
|
180
|
+
"""Get all credentials for this user."""
|
|
181
|
+
return [c for c in db.data().credentials.values() if c.user_uuid == self.uuid]
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def sessions(self) -> list[Session]:
|
|
185
|
+
"""Get all sessions for this user."""
|
|
186
|
+
return [s for s in db.data().sessions.values() if s.user_uuid == self.uuid]
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def reset_tokens(self) -> list[ResetToken]:
|
|
190
|
+
"""Get all reset tokens for this user."""
|
|
191
|
+
return [t for t in db.data().reset_tokens.values() if t.user_uuid == self.uuid]
|
|
118
192
|
|
|
119
193
|
@classmethod
|
|
120
194
|
def create(
|
|
121
195
|
cls,
|
|
122
196
|
display_name: str,
|
|
123
|
-
role: UUID,
|
|
197
|
+
role: UUID | Role,
|
|
124
198
|
created_at: datetime | None = None,
|
|
125
|
-
) ->
|
|
199
|
+
) -> User:
|
|
126
200
|
"""Create a new User with auto-generated uuid7."""
|
|
127
|
-
|
|
201
|
+
role_uuid = role if isinstance(role, UUID) else role.uuid
|
|
128
202
|
user = cls(
|
|
129
203
|
display_name=display_name,
|
|
130
|
-
|
|
131
|
-
created_at=created_at or datetime.now(
|
|
204
|
+
role_uuid=role_uuid,
|
|
205
|
+
created_at=created_at or datetime.now(UTC),
|
|
132
206
|
)
|
|
133
207
|
user.uuid = uuid7.create(user.created_at)
|
|
134
208
|
return user
|
|
@@ -143,7 +217,7 @@ class Credential(msgspec.Struct, dict=True):
|
|
|
143
217
|
"""
|
|
144
218
|
|
|
145
219
|
credential_id: bytes # Long binary ID from the authenticator
|
|
146
|
-
|
|
220
|
+
user_uuid: UUID = msgspec.field(name="user")
|
|
147
221
|
aaguid: UUID
|
|
148
222
|
public_key: bytes
|
|
149
223
|
sign_count: int
|
|
@@ -152,23 +226,37 @@ class Credential(msgspec.Struct, dict=True):
|
|
|
152
226
|
last_verified: datetime | None = None
|
|
153
227
|
|
|
154
228
|
def __post_init__(self):
|
|
155
|
-
|
|
229
|
+
if not hasattr(self, "uuid"):
|
|
230
|
+
self.uuid: UUID = _UUID_UNSET
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def user(self) -> User:
|
|
234
|
+
"""Get the User object for this credential."""
|
|
235
|
+
return db.data().users[self.user_uuid]
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def sessions(self) -> list[Session]:
|
|
239
|
+
"""Get all sessions using this credential."""
|
|
240
|
+
return [
|
|
241
|
+
s for s in db.data().sessions.values() if s.credential_uuid == self.uuid
|
|
242
|
+
]
|
|
156
243
|
|
|
157
244
|
@classmethod
|
|
158
245
|
def create(
|
|
159
246
|
cls,
|
|
160
247
|
credential_id: bytes,
|
|
161
|
-
user: UUID,
|
|
248
|
+
user: UUID | User,
|
|
162
249
|
aaguid: UUID,
|
|
163
250
|
public_key: bytes,
|
|
164
251
|
sign_count: int,
|
|
165
252
|
created_at: datetime | None = None,
|
|
166
|
-
) ->
|
|
253
|
+
) -> Credential:
|
|
167
254
|
"""Create a new Credential with auto-generated uuid7."""
|
|
168
|
-
|
|
255
|
+
user_uuid = user if isinstance(user, UUID) else user.uuid
|
|
256
|
+
now = created_at or datetime.now(UTC)
|
|
169
257
|
cred = cls(
|
|
170
258
|
credential_id=credential_id,
|
|
171
|
-
|
|
259
|
+
user_uuid=user_uuid,
|
|
172
260
|
aaguid=aaguid,
|
|
173
261
|
public_key=public_key,
|
|
174
262
|
sign_count=sign_count,
|
|
@@ -184,19 +272,30 @@ class Session(msgspec.Struct, dict=True):
|
|
|
184
272
|
"""Session data structure.
|
|
185
273
|
|
|
186
274
|
Mutable fields: expiry (updated on session refresh)
|
|
187
|
-
Immutable fields:
|
|
275
|
+
Immutable fields: user_uuid, credential_uuid, host, ip, user_agent
|
|
188
276
|
key is stored in the dict key, not in the struct.
|
|
189
277
|
"""
|
|
190
278
|
|
|
191
|
-
|
|
192
|
-
|
|
279
|
+
user_uuid: UUID = msgspec.field(name="user")
|
|
280
|
+
credential_uuid: UUID = msgspec.field(name="credential")
|
|
193
281
|
host: str
|
|
194
282
|
ip: str
|
|
195
283
|
user_agent: str
|
|
196
284
|
expiry: datetime
|
|
197
285
|
|
|
198
286
|
def __post_init__(self):
|
|
199
|
-
|
|
287
|
+
if not hasattr(self, "key"):
|
|
288
|
+
self.key: str = ""
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def user(self) -> User:
|
|
292
|
+
"""Get the User object for this session."""
|
|
293
|
+
return db.data().users[self.user_uuid]
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def credential(self) -> Credential:
|
|
297
|
+
"""Get the Credential object for this session."""
|
|
298
|
+
return db.data().credentials[self.credential_uuid]
|
|
200
299
|
|
|
201
300
|
def metadata(self) -> dict:
|
|
202
301
|
"""Return session metadata for backwards compatibility."""
|
|
@@ -206,6 +305,32 @@ class Session(msgspec.Struct, dict=True):
|
|
|
206
305
|
"expiry": self.expiry.isoformat(),
|
|
207
306
|
}
|
|
208
307
|
|
|
308
|
+
@classmethod
|
|
309
|
+
def create(
|
|
310
|
+
cls,
|
|
311
|
+
user: UUID | User,
|
|
312
|
+
credential: UUID | Credential,
|
|
313
|
+
host: str,
|
|
314
|
+
ip: str,
|
|
315
|
+
user_agent: str,
|
|
316
|
+
expiry: datetime,
|
|
317
|
+
) -> Session:
|
|
318
|
+
"""Create a new Session with auto-generated key."""
|
|
319
|
+
user_uuid = user if isinstance(user, UUID) else user.uuid
|
|
320
|
+
credential_uuid = (
|
|
321
|
+
credential if isinstance(credential, UUID) else credential.uuid
|
|
322
|
+
)
|
|
323
|
+
session = cls(
|
|
324
|
+
user_uuid=user_uuid,
|
|
325
|
+
credential_uuid=credential_uuid,
|
|
326
|
+
host=host,
|
|
327
|
+
ip=ip,
|
|
328
|
+
user_agent=user_agent,
|
|
329
|
+
expiry=expiry,
|
|
330
|
+
)
|
|
331
|
+
session.key = secrets.token_urlsafe(12)
|
|
332
|
+
return session
|
|
333
|
+
|
|
209
334
|
|
|
210
335
|
class ResetToken(msgspec.Struct, dict=True):
|
|
211
336
|
"""Reset/device-addition token data structure.
|
|
@@ -214,12 +339,18 @@ class ResetToken(msgspec.Struct, dict=True):
|
|
|
214
339
|
key is stored in the dict key, not in the struct.
|
|
215
340
|
"""
|
|
216
341
|
|
|
217
|
-
|
|
342
|
+
user_uuid: UUID = msgspec.field(name="user")
|
|
218
343
|
expiry: datetime
|
|
219
344
|
token_type: str
|
|
220
345
|
|
|
221
346
|
def __post_init__(self):
|
|
222
|
-
|
|
347
|
+
if not hasattr(self, "key"):
|
|
348
|
+
self.key: bytes = b""
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def user(self) -> User:
|
|
352
|
+
"""Get the User object for this reset token."""
|
|
353
|
+
return db.data().users[self.user_uuid]
|
|
223
354
|
|
|
224
355
|
|
|
225
356
|
class SessionContext(msgspec.Struct):
|
|
@@ -246,7 +377,6 @@ class DB(msgspec.Struct, dict=True, omit_defaults=False):
|
|
|
246
377
|
credentials: dict[UUID, Credential] = {}
|
|
247
378
|
sessions: dict[str, Session] = {}
|
|
248
379
|
reset_tokens: dict[bytes, ResetToken] = {}
|
|
249
|
-
v: int = 0
|
|
250
380
|
|
|
251
381
|
def __post_init__(self):
|
|
252
382
|
# Store reference for persistence (not serialized)
|
|
@@ -270,3 +400,63 @@ class DB(msgspec.Struct, dict=True, omit_defaults=False):
|
|
|
270
400
|
def transaction(self, action, ctx=None, *, user=None):
|
|
271
401
|
"""Wrap writes in transaction. Delegates to JsonlStore."""
|
|
272
402
|
return self._store.transaction(action, ctx, user=user)
|
|
403
|
+
|
|
404
|
+
def session_ctx(
|
|
405
|
+
self, session_key: str, host: str | None = None
|
|
406
|
+
) -> SessionContext | None:
|
|
407
|
+
"""Get full session context with effective permissions.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
session_key: The session key string
|
|
411
|
+
host: Optional host for binding/validation and domain-scoped permissions
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
SessionContext if valid, None if session not found, expired, or host mismatch
|
|
415
|
+
"""
|
|
416
|
+
try:
|
|
417
|
+
s = self.sessions[session_key]
|
|
418
|
+
except KeyError:
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
# Validate host matches (sessions are always created with a host)
|
|
422
|
+
if s.host != host:
|
|
423
|
+
# Session bound to different host
|
|
424
|
+
return None
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
user = s.user
|
|
428
|
+
role = user.role
|
|
429
|
+
org = role.org
|
|
430
|
+
credential = s.credential
|
|
431
|
+
except KeyError:
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
# Effective permissions: role's permissions that the org can grant
|
|
435
|
+
# Also filter by domain if host is provided
|
|
436
|
+
org_perm_uuids = {p.uuid for p in org.permissions}
|
|
437
|
+
normalized_host = normalize_host(host)
|
|
438
|
+
host_without_port = (
|
|
439
|
+
normalized_host.rsplit(":", 1)[0] if normalized_host else None
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
effective_perms = []
|
|
443
|
+
for perm_uuid in role.permission_set:
|
|
444
|
+
if perm_uuid not in org_perm_uuids:
|
|
445
|
+
continue
|
|
446
|
+
try:
|
|
447
|
+
p = self.permissions[perm_uuid]
|
|
448
|
+
except KeyError:
|
|
449
|
+
continue
|
|
450
|
+
# Check domain restriction
|
|
451
|
+
if p.domain is not None and p.domain != host_without_port:
|
|
452
|
+
continue
|
|
453
|
+
effective_perms.append(p)
|
|
454
|
+
|
|
455
|
+
return SessionContext(
|
|
456
|
+
session=s,
|
|
457
|
+
user=user,
|
|
458
|
+
org=org,
|
|
459
|
+
role=role,
|
|
460
|
+
credential=credential,
|
|
461
|
+
permissions=effective_perms,
|
|
462
|
+
)
|
paskia/fastapi/__main__.py
CHANGED
|
@@ -7,12 +7,12 @@ from urllib.parse import urlparse
|
|
|
7
7
|
|
|
8
8
|
from fastapi_vue.hostutil import parse_endpoint
|
|
9
9
|
from uvicorn import Config, Server
|
|
10
|
+
from uvicorn import run as uvicorn_run
|
|
10
11
|
|
|
11
12
|
from paskia import globals as _globals
|
|
12
13
|
from paskia.bootstrap import bootstrap_if_needed
|
|
13
14
|
from paskia.config import PaskiaConfig
|
|
14
15
|
from paskia.db.background import flush
|
|
15
|
-
from paskia.fastapi import app as fastapi_app
|
|
16
16
|
from paskia.fastapi import reset as reset_cmd
|
|
17
17
|
from paskia.util import startupbox
|
|
18
18
|
from paskia.util.hostutil import normalize_origin
|
|
@@ -188,7 +188,8 @@ def main():
|
|
|
188
188
|
devmode = bool(os.environ.get("FASTAPI_VUE_FRONTEND_URL"))
|
|
189
189
|
|
|
190
190
|
run_kwargs: dict = {
|
|
191
|
-
"log_level": "
|
|
191
|
+
"log_level": "warning", # Suppress startup messages; we use custom logging
|
|
192
|
+
"access_log": False, # We use custom AccessLogMiddleware instead
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
if devmode:
|
|
@@ -198,8 +199,6 @@ def main():
|
|
|
198
199
|
raise SystemExit(f"Dev mode requires localhost:4402, got {host}:{port}")
|
|
199
200
|
run_kwargs["reload"] = True
|
|
200
201
|
run_kwargs["reload_dirs"] = ["paskia"]
|
|
201
|
-
# Suppress uvicorn startup messages in dev mode
|
|
202
|
-
run_kwargs["log_level"] = "warning"
|
|
203
202
|
|
|
204
203
|
async def async_main():
|
|
205
204
|
await _globals.init(
|
|
@@ -219,10 +218,18 @@ def main():
|
|
|
219
218
|
async with asyncio.TaskGroup() as tg:
|
|
220
219
|
for ep in endpoints:
|
|
221
220
|
tg.create_task(
|
|
222
|
-
Server(
|
|
221
|
+
Server(
|
|
222
|
+
Config(app="paskia.fastapi:app", **run_kwargs, **ep)
|
|
223
|
+
).serve()
|
|
223
224
|
)
|
|
225
|
+
elif devmode:
|
|
226
|
+
# Use uvicorn.run for proper reload support (it handles subprocess spawning)
|
|
227
|
+
ep = endpoints[0]
|
|
228
|
+
uvicorn_run("paskia.fastapi:app", **run_kwargs, **ep)
|
|
224
229
|
else:
|
|
225
|
-
server = Server(
|
|
230
|
+
server = Server(
|
|
231
|
+
Config(app="paskia.fastapi:app", **run_kwargs, **endpoints[0])
|
|
232
|
+
)
|
|
226
233
|
await server.serve()
|
|
227
234
|
|
|
228
235
|
try:
|