paskia 0.8.0__py3-none-any.whl → 0.9.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/authsession.py +14 -27
- paskia/bootstrap.py +31 -103
- paskia/config.py +0 -1
- paskia/db/__init__.py +26 -51
- paskia/db/background.py +17 -37
- paskia/db/jsonl.py +168 -6
- paskia/db/migrations.py +34 -0
- paskia/db/operations.py +400 -723
- paskia/db/structs.py +214 -90
- paskia/fastapi/__main__.py +89 -189
- paskia/fastapi/admin.py +103 -162
- paskia/fastapi/api.py +49 -85
- paskia/fastapi/mainapp.py +30 -19
- paskia/fastapi/remote.py +16 -39
- paskia/fastapi/reset.py +27 -17
- paskia/fastapi/session.py +2 -2
- paskia/fastapi/user.py +21 -27
- paskia/fastapi/ws.py +27 -62
- paskia/fastapi/wschat.py +62 -0
- paskia/frontend-build/auth/admin/index.html +5 -5
- paskia/frontend-build/auth/assets/{AccessDenied-Bc249ASC.css → AccessDenied-DPkUS8LZ.css} +1 -1
- paskia/frontend-build/auth/assets/AccessDenied-Fmeb6EtF.js +8 -0
- paskia/frontend-build/auth/assets/{RestrictedAuth-DgdJyscT.css → RestrictedAuth-CvR33_Z0.css} +1 -1
- paskia/frontend-build/auth/assets/RestrictedAuth-DsJXicIw.js +1 -0
- paskia/frontend-build/auth/assets/{_plugin-vue_export-helper-rKFEraYH.js → _plugin-vue_export-helper-nhjnO_bd.js} +1 -1
- paskia/frontend-build/auth/assets/admin-CPE1pLMm.js +1 -0
- paskia/frontend-build/auth/assets/{admin-BeNu48FR.css → admin-DzzjSg72.css} +1 -1
- paskia/frontend-build/auth/assets/{auth-BKX7shEe.css → auth-C7k64Wad.css} +1 -1
- paskia/frontend-build/auth/assets/auth-YIZvPlW_.js +1 -0
- paskia/frontend-build/auth/assets/{forward-Dzg-aE1C.js → forward-DmqVHZ7e.js} +1 -1
- paskia/frontend-build/auth/assets/reset-Chtv69AT.css +1 -0
- paskia/frontend-build/auth/assets/reset-s20PATTN.js +1 -0
- paskia/frontend-build/auth/assets/{restricted-C0IQufuH.js → restricted-D3AJx3_6.js} +1 -1
- paskia/frontend-build/auth/index.html +5 -5
- paskia/frontend-build/auth/restricted/index.html +4 -4
- paskia/frontend-build/int/forward/index.html +4 -4
- paskia/frontend-build/int/reset/index.html +3 -3
- paskia/globals.py +2 -2
- paskia/migrate/__init__.py +62 -55
- paskia/migrate/sql.py +72 -22
- paskia/remoteauth.py +1 -2
- paskia/sansio.py +6 -12
- {paskia-0.8.0.dist-info → paskia-0.9.0.dist-info}/METADATA +3 -2
- paskia-0.9.0.dist-info/RECORD +57 -0
- paskia/frontend-build/auth/assets/AccessDenied-aTdCvz9k.js +0 -8
- paskia/frontend-build/auth/assets/RestrictedAuth-BLMK7-nL.js +0 -1
- paskia/frontend-build/auth/assets/admin-tVs8oyLv.js +0 -1
- paskia/frontend-build/auth/assets/auth-Dk3q4pNS.js +0 -1
- paskia/frontend-build/auth/assets/reset-BWF4cWKR.css +0 -1
- paskia/frontend-build/auth/assets/reset-C_Td1_jn.js +0 -1
- paskia/util/frontend.py +0 -75
- paskia/util/hostutil.py +0 -76
- paskia/util/htmlutil.py +0 -47
- paskia/util/passphrase.py +0 -20
- paskia/util/permutil.py +0 -43
- paskia/util/pow.py +0 -45
- paskia/util/querysafe.py +0 -11
- paskia/util/sessionutil.py +0 -38
- paskia/util/startupbox.py +0 -75
- paskia/util/timeutil.py +0 -47
- paskia/util/useragent.py +0 -10
- paskia/util/userinfo.py +0 -145
- paskia/util/wordlist.py +0 -54
- paskia-0.8.0.dist-info/RECORD +0 -68
- {paskia-0.8.0.dist-info → paskia-0.9.0.dist-info}/WHEEL +0 -0
- {paskia-0.8.0.dist-info → paskia-0.9.0.dist-info}/entry_points.txt +0 -0
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<script type="module" crossorigin src="/auth/assets/restricted-
|
|
7
|
-
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-
|
|
6
|
+
<script type="module" crossorigin src="/auth/assets/restricted-D3AJx3_6.js"></script>
|
|
7
|
+
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-nhjnO_bd.js">
|
|
8
8
|
<link rel="modulepreload" crossorigin href="/auth/assets/pow-2N9bxgAo.js">
|
|
9
|
-
<link rel="modulepreload" crossorigin href="/auth/assets/RestrictedAuth-
|
|
9
|
+
<link rel="modulepreload" crossorigin href="/auth/assets/RestrictedAuth-DsJXicIw.js">
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-BTzJAQlS.css">
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/auth/assets/RestrictedAuth-
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/auth/assets/RestrictedAuth-CvR33_Z0.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="app"></div>
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Access Restricted</title>
|
|
7
|
-
<script type="module" crossorigin src="/auth/assets/forward-
|
|
8
|
-
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-
|
|
7
|
+
<script type="module" crossorigin src="/auth/assets/forward-DmqVHZ7e.js"></script>
|
|
8
|
+
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-nhjnO_bd.js">
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/auth/assets/pow-2N9bxgAo.js">
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/auth/assets/RestrictedAuth-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/auth/assets/RestrictedAuth-DsJXicIw.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/auth/assets/helpers-DzjFIx78.js">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-BTzJAQlS.css">
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/auth/assets/RestrictedAuth-
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/auth/assets/RestrictedAuth-CvR33_Z0.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="app"></div>
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Complete Passkey Setup</title>
|
|
7
|
-
<script type="module" crossorigin src="/auth/assets/reset-
|
|
8
|
-
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-
|
|
7
|
+
<script type="module" crossorigin src="/auth/assets/reset-s20PATTN.js"></script>
|
|
8
|
+
<link rel="modulepreload" crossorigin href="/auth/assets/_plugin-vue_export-helper-nhjnO_bd.js">
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/auth/assets/_plugin-vue_export-helper-BTzJAQlS.css">
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/auth/assets/reset-
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/auth/assets/reset-Chtv69AT.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="app"></div>
|
paskia/globals.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from typing import Generic, TypeVar
|
|
2
2
|
|
|
3
|
+
from paskia import db, remoteauth
|
|
4
|
+
from paskia.bootstrap import bootstrap_if_needed
|
|
3
5
|
from paskia.sansio import Passkey
|
|
4
6
|
|
|
5
7
|
T = TypeVar("T")
|
|
@@ -42,7 +44,6 @@ async def init(
|
|
|
42
44
|
Set PASKIA_DB environment variable to specify the JSONL database file path.
|
|
43
45
|
Default: paskia.jsonl
|
|
44
46
|
"""
|
|
45
|
-
from . import db, remoteauth
|
|
46
47
|
|
|
47
48
|
# Initialize passkey instance with provided parameters
|
|
48
49
|
passkey.instance = Passkey(
|
|
@@ -59,7 +60,6 @@ async def init(
|
|
|
59
60
|
|
|
60
61
|
if bootstrap:
|
|
61
62
|
# Bootstrap system if needed
|
|
62
|
-
from .bootstrap import bootstrap_if_needed
|
|
63
63
|
|
|
64
64
|
await bootstrap_if_needed()
|
|
65
65
|
|
paskia/migrate/__init__.py
CHANGED
|
@@ -11,13 +11,28 @@ Or via the CLI entry point (if installed):
|
|
|
11
11
|
paskia-migrate --sql sqlite+aiosqlite:///paskia.sqlite --json paskia.jsonl
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
import argparse
|
|
14
15
|
import asyncio
|
|
16
|
+
import re
|
|
15
17
|
from datetime import datetime, timezone
|
|
16
18
|
from uuid import UUID
|
|
17
19
|
|
|
18
20
|
import base64url
|
|
21
|
+
import uuid7
|
|
22
|
+
from sqlalchemy import select
|
|
19
23
|
|
|
20
24
|
from paskia.authsession import EXPIRES
|
|
25
|
+
from paskia.db.jsonl import JsonlStore
|
|
26
|
+
from paskia.db.structs import (
|
|
27
|
+
DB,
|
|
28
|
+
Credential,
|
|
29
|
+
Org,
|
|
30
|
+
Permission,
|
|
31
|
+
ResetToken,
|
|
32
|
+
Role,
|
|
33
|
+
Session,
|
|
34
|
+
User,
|
|
35
|
+
)
|
|
21
36
|
|
|
22
37
|
from .sql import (
|
|
23
38
|
DB as SQLDB,
|
|
@@ -47,30 +62,14 @@ async def migrate_from_sql(
|
|
|
47
62
|
sql_db_path: SQLAlchemy connection string for the source SQL database
|
|
48
63
|
json_db_path: Path for the destination JSONL file
|
|
49
64
|
"""
|
|
50
|
-
# Import here to avoid circular imports and to not require JSON db at import time
|
|
51
|
-
import re
|
|
52
|
-
|
|
53
|
-
import uuid7
|
|
54
|
-
from sqlalchemy import select
|
|
55
|
-
|
|
56
|
-
from paskia.db.operations import DB as JSONDB
|
|
57
|
-
from paskia.db.structs import (
|
|
58
|
-
_CredentialData,
|
|
59
|
-
_OrgData,
|
|
60
|
-
_PermissionData,
|
|
61
|
-
_ResetTokenData,
|
|
62
|
-
_RoleData,
|
|
63
|
-
_SessionData,
|
|
64
|
-
_UserData,
|
|
65
|
-
)
|
|
66
|
-
|
|
67
65
|
# Initialize source SQL database
|
|
68
66
|
sql_db = SQLDB(sql_db_path)
|
|
69
67
|
await sql_db.init_db()
|
|
70
68
|
|
|
71
69
|
# Initialize destination JSON database (fresh, don't load existing)
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
db = DB()
|
|
71
|
+
store = JsonlStore(db, json_db_path)
|
|
72
|
+
db._store = store
|
|
74
73
|
|
|
75
74
|
print(f"Migrating from {sql_db_path} to {json_db_path}...")
|
|
76
75
|
|
|
@@ -90,11 +89,13 @@ async def migrate_from_sql(
|
|
|
90
89
|
# Migrate permissions with UUID keys and scope field
|
|
91
90
|
# Always create exactly one common auth:org:admin permission for all org admin needs
|
|
92
91
|
org_admin_perm_uuid: UUID = uuid7.create()
|
|
93
|
-
|
|
92
|
+
org_admin_perm = Permission(
|
|
94
93
|
scope="auth:org:admin",
|
|
95
94
|
display_name="Org Admin",
|
|
96
95
|
orgs={},
|
|
97
96
|
)
|
|
97
|
+
org_admin_perm.uuid = org_admin_perm_uuid
|
|
98
|
+
db.permissions[org_admin_perm_uuid] = org_admin_perm
|
|
98
99
|
|
|
99
100
|
# Mapping from old permission ID to new permission UUID
|
|
100
101
|
perm_id_to_uuid: dict[str, UUID] = {}
|
|
@@ -113,11 +114,13 @@ async def migrate_from_sql(
|
|
|
113
114
|
|
|
114
115
|
# Regular permission - create with UUID key
|
|
115
116
|
perm_uuid: UUID = uuid7.create()
|
|
116
|
-
|
|
117
|
+
new_perm = Permission(
|
|
117
118
|
scope=perm.id, # Old ID becomes the scope
|
|
118
119
|
display_name=perm.display_name,
|
|
119
120
|
orgs={},
|
|
120
121
|
)
|
|
122
|
+
new_perm.uuid = perm_uuid
|
|
123
|
+
db.permissions[perm_uuid] = new_perm
|
|
121
124
|
perm_id_to_uuid[perm.id] = perm_uuid
|
|
122
125
|
print(
|
|
123
126
|
f" Migrated {len(permissions)} permissions (with {len(org_admin_uuids)} org-specific admins consolidated to auth:org:admin)"
|
|
@@ -127,16 +130,16 @@ async def migrate_from_sql(
|
|
|
127
130
|
orgs = await sql_db.list_organizations()
|
|
128
131
|
for org in orgs:
|
|
129
132
|
org_key: UUID = org.uuid
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
new_org = Org(display_name=org.display_name)
|
|
134
|
+
new_org.uuid = org_key
|
|
135
|
+
db.orgs[org_key] = new_org
|
|
133
136
|
# Update permissions to allow this org to grant them (by UUID)
|
|
134
137
|
for old_perm_id in org.permissions:
|
|
135
138
|
perm_uuid = perm_id_to_uuid.get(old_perm_id)
|
|
136
|
-
if perm_uuid and perm_uuid in
|
|
137
|
-
|
|
139
|
+
if perm_uuid and perm_uuid in db.permissions:
|
|
140
|
+
db.permissions[perm_uuid].orgs[org_key] = True
|
|
138
141
|
# Ensure every org can grant auth:org:admin
|
|
139
|
-
|
|
142
|
+
db.permissions[org_admin_perm_uuid].orgs[org_key] = True
|
|
140
143
|
print(f" Migrated {len(orgs)} organizations")
|
|
141
144
|
|
|
142
145
|
# Migrate roles - convert old permission IDs to UUIDs
|
|
@@ -150,11 +153,13 @@ async def migrate_from_sql(
|
|
|
150
153
|
perm_uuid = perm_id_to_uuid.get(old_perm_id)
|
|
151
154
|
if perm_uuid:
|
|
152
155
|
new_permissions[perm_uuid] = True
|
|
153
|
-
|
|
156
|
+
new_role = Role(
|
|
154
157
|
org=role.org_uuid,
|
|
155
158
|
display_name=role.display_name,
|
|
156
159
|
permissions=new_permissions,
|
|
157
160
|
)
|
|
161
|
+
new_role.uuid = role_key
|
|
162
|
+
db.roles[role_key] = new_role
|
|
158
163
|
role_count += 1
|
|
159
164
|
print(f" Migrated {role_count} roles")
|
|
160
165
|
|
|
@@ -163,15 +168,17 @@ async def migrate_from_sql(
|
|
|
163
168
|
result = await session.execute(select(UserModel))
|
|
164
169
|
user_models = result.scalars().all()
|
|
165
170
|
for um in user_models:
|
|
166
|
-
|
|
167
|
-
user_key: UUID =
|
|
168
|
-
|
|
169
|
-
display_name=
|
|
170
|
-
role=
|
|
171
|
-
created_at=
|
|
172
|
-
last_seen=
|
|
173
|
-
visits=
|
|
171
|
+
legacy_user = um.as_dataclass()
|
|
172
|
+
user_key: UUID = legacy_user.uuid
|
|
173
|
+
new_user = User(
|
|
174
|
+
display_name=legacy_user.display_name,
|
|
175
|
+
role=legacy_user.role_uuid,
|
|
176
|
+
created_at=legacy_user.created_at or datetime.now(timezone.utc),
|
|
177
|
+
last_seen=legacy_user.last_seen,
|
|
178
|
+
visits=legacy_user.visits,
|
|
174
179
|
)
|
|
180
|
+
new_user.uuid = user_key
|
|
181
|
+
db.users[user_key] = new_user
|
|
175
182
|
print(f" Migrated {len(user_models)} users")
|
|
176
183
|
|
|
177
184
|
# Migrate credentials
|
|
@@ -179,18 +186,20 @@ async def migrate_from_sql(
|
|
|
179
186
|
result = await session.execute(select(CredentialModel))
|
|
180
187
|
cred_models = result.scalars().all()
|
|
181
188
|
for cm in cred_models:
|
|
182
|
-
|
|
183
|
-
cred_key: UUID =
|
|
184
|
-
|
|
185
|
-
credential_id=
|
|
186
|
-
user=
|
|
187
|
-
aaguid=
|
|
188
|
-
public_key=
|
|
189
|
-
sign_count=
|
|
190
|
-
created_at=
|
|
191
|
-
last_used=
|
|
192
|
-
last_verified=
|
|
189
|
+
legacy_cred = cm.as_dataclass()
|
|
190
|
+
cred_key: UUID = legacy_cred.uuid
|
|
191
|
+
new_cred = Credential(
|
|
192
|
+
credential_id=legacy_cred.credential_id,
|
|
193
|
+
user=legacy_cred.user_uuid,
|
|
194
|
+
aaguid=legacy_cred.aaguid,
|
|
195
|
+
public_key=legacy_cred.public_key,
|
|
196
|
+
sign_count=legacy_cred.sign_count,
|
|
197
|
+
created_at=legacy_cred.created_at,
|
|
198
|
+
last_used=legacy_cred.last_used,
|
|
199
|
+
last_verified=legacy_cred.last_verified,
|
|
193
200
|
)
|
|
201
|
+
new_cred.uuid = cred_key
|
|
202
|
+
db.credentials[cred_key] = new_cred
|
|
194
203
|
print(f" Migrated {len(cred_models)} credentials")
|
|
195
204
|
|
|
196
205
|
# Migrate sessions
|
|
@@ -207,7 +216,7 @@ async def migrate_from_sql(
|
|
|
207
216
|
else:
|
|
208
217
|
# Already in new format or unknown - try to use as-is
|
|
209
218
|
session_key = base64url.enc(old_key[:12])
|
|
210
|
-
|
|
219
|
+
db.sessions[session_key] = Session(
|
|
211
220
|
user=sess.user_uuid,
|
|
212
221
|
credential=sess.credential_uuid,
|
|
213
222
|
host=sess.host,
|
|
@@ -231,26 +240,24 @@ async def migrate_from_sql(
|
|
|
231
240
|
else:
|
|
232
241
|
# Already in new format or unknown - truncate to 9 bytes
|
|
233
242
|
token_key = old_key[:9]
|
|
234
|
-
|
|
243
|
+
db.reset_tokens[token_key] = ResetToken(
|
|
235
244
|
user=token.user_uuid,
|
|
236
245
|
expiry=token.expiry,
|
|
237
246
|
token_type=token.token_type,
|
|
238
247
|
)
|
|
239
248
|
print(f" Migrated {len(token_models)} reset tokens")
|
|
240
249
|
|
|
241
|
-
# Queue and flush all changes
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
from paskia.db.jsonl import flush_changes
|
|
250
|
+
# Queue and flush all changes using the transaction mechanism
|
|
251
|
+
with db.transaction("migrate"):
|
|
252
|
+
pass # All data already added to _data, transaction commits on exit
|
|
245
253
|
|
|
246
|
-
await
|
|
254
|
+
await store.flush()
|
|
247
255
|
|
|
248
256
|
print("Migration complete!")
|
|
249
257
|
|
|
250
258
|
|
|
251
259
|
def main():
|
|
252
260
|
"""CLI entry point for migration."""
|
|
253
|
-
import argparse
|
|
254
261
|
|
|
255
262
|
parser = argparse.ArgumentParser(
|
|
256
263
|
description="Migrate Paskia database from SQL to JSON"
|
paskia/migrate/sql.py
CHANGED
|
@@ -26,30 +26,55 @@ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
|
26
26
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
27
27
|
|
|
28
28
|
from paskia.db import (
|
|
29
|
-
Credential,
|
|
30
29
|
Org,
|
|
31
|
-
ResetToken,
|
|
32
30
|
Role,
|
|
33
|
-
User,
|
|
34
31
|
)
|
|
35
32
|
|
|
36
33
|
|
|
37
|
-
#
|
|
34
|
+
# Legacy User class for SQL schema (uses 'role_uuid' not 'role')
|
|
38
35
|
@dataclass
|
|
39
|
-
class
|
|
40
|
-
"""
|
|
36
|
+
class _LegacyUser:
|
|
37
|
+
"""User as stored in the old SQL schema with role_uuid field."""
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
uuid: UUID
|
|
43
40
|
display_name: str
|
|
41
|
+
role_uuid: UUID
|
|
42
|
+
created_at: datetime | None = None
|
|
43
|
+
last_seen: datetime | None = None
|
|
44
|
+
visits: int = 0
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
# Legacy Credential class for SQL schema (uses 'user_uuid' not 'user')
|
|
48
|
+
@dataclass
|
|
49
|
+
class _LegacyCredential:
|
|
50
|
+
"""Credential as stored in the old SQL schema with user_uuid field."""
|
|
51
|
+
|
|
52
|
+
uuid: UUID
|
|
53
|
+
credential_id: bytes
|
|
54
|
+
user_uuid: UUID
|
|
55
|
+
aaguid: UUID
|
|
56
|
+
public_key: bytes
|
|
57
|
+
sign_count: int
|
|
58
|
+
created_at: datetime
|
|
59
|
+
last_used: datetime | None = None
|
|
60
|
+
last_verified: datetime | None = None
|
|
47
61
|
|
|
48
62
|
|
|
49
|
-
#
|
|
63
|
+
# Legacy Role class for SQL schema (uses 'org_uuid' not 'org')
|
|
50
64
|
@dataclass
|
|
51
|
-
class
|
|
52
|
-
"""
|
|
65
|
+
class _LegacyRole:
|
|
66
|
+
"""Role as stored in the old SQL schema with org_uuid field."""
|
|
67
|
+
|
|
68
|
+
uuid: UUID
|
|
69
|
+
org_uuid: UUID
|
|
70
|
+
display_name: str
|
|
71
|
+
permissions: list[str] | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Legacy Session class for SQL schema (uses 'key' as field, 'user_uuid', 'credential_uuid')
|
|
75
|
+
@dataclass
|
|
76
|
+
class _LegacySession:
|
|
77
|
+
"""Session as stored in the old SQL schema."""
|
|
53
78
|
|
|
54
79
|
key: bytes
|
|
55
80
|
user_uuid: UUID
|
|
@@ -60,6 +85,29 @@ class _SqlSession:
|
|
|
60
85
|
renewed: datetime
|
|
61
86
|
|
|
62
87
|
|
|
88
|
+
# Legacy ResetToken class for SQL schema (uses 'key' as field, 'user_uuid')
|
|
89
|
+
@dataclass
|
|
90
|
+
class _LegacyResetToken:
|
|
91
|
+
"""ResetToken as stored in the old SQL schema."""
|
|
92
|
+
|
|
93
|
+
key: bytes
|
|
94
|
+
user_uuid: UUID
|
|
95
|
+
token_type: str
|
|
96
|
+
expiry: datetime
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Local Permission class for SQL schema (uses 'id' not 'uuid' + 'scope')
|
|
100
|
+
@dataclass
|
|
101
|
+
class SqlPermission:
|
|
102
|
+
"""Permission as stored in the old SQL schema with id field."""
|
|
103
|
+
|
|
104
|
+
id: str
|
|
105
|
+
display_name: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
DB_PATH_DEFAULT = "sqlite+aiosqlite:///paskia.sqlite"
|
|
109
|
+
|
|
110
|
+
|
|
63
111
|
def _normalize_dt(value: datetime | None) -> datetime | None:
|
|
64
112
|
if value is None:
|
|
65
113
|
return None
|
|
@@ -80,7 +128,9 @@ class OrgModel(Base):
|
|
|
80
128
|
|
|
81
129
|
def as_dataclass(self):
|
|
82
130
|
# Base Org without permissions/roles (filled by data accessors)
|
|
83
|
-
|
|
131
|
+
org = Org(display_name=self.display_name)
|
|
132
|
+
org.uuid = UUID(bytes=self.uuid)
|
|
133
|
+
return org
|
|
84
134
|
|
|
85
135
|
@staticmethod
|
|
86
136
|
def from_dataclass(org: Org):
|
|
@@ -98,14 +148,14 @@ class RoleModel(Base):
|
|
|
98
148
|
|
|
99
149
|
def as_dataclass(self):
|
|
100
150
|
# Base Role without permissions (filled by data accessors)
|
|
101
|
-
return
|
|
151
|
+
return _LegacyRole(
|
|
102
152
|
uuid=UUID(bytes=self.uuid),
|
|
103
153
|
org_uuid=UUID(bytes=self.org_uuid),
|
|
104
154
|
display_name=self.display_name,
|
|
105
155
|
)
|
|
106
156
|
|
|
107
157
|
@staticmethod
|
|
108
|
-
def from_dataclass(role:
|
|
158
|
+
def from_dataclass(role: _LegacyRole):
|
|
109
159
|
return RoleModel(
|
|
110
160
|
uuid=role.uuid.bytes,
|
|
111
161
|
org_uuid=role.org_uuid.bytes,
|
|
@@ -129,8 +179,8 @@ class UserModel(Base):
|
|
|
129
179
|
)
|
|
130
180
|
visits: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
131
181
|
|
|
132
|
-
def as_dataclass(self) ->
|
|
133
|
-
return
|
|
182
|
+
def as_dataclass(self) -> "_LegacyUser":
|
|
183
|
+
return _LegacyUser(
|
|
134
184
|
uuid=UUID(bytes=self.uuid),
|
|
135
185
|
display_name=self.display_name,
|
|
136
186
|
role_uuid=UUID(bytes=self.role_uuid),
|
|
@@ -140,7 +190,7 @@ class UserModel(Base):
|
|
|
140
190
|
)
|
|
141
191
|
|
|
142
192
|
@staticmethod
|
|
143
|
-
def from_dataclass(user:
|
|
193
|
+
def from_dataclass(user: "_LegacyUser"):
|
|
144
194
|
return UserModel(
|
|
145
195
|
uuid=user.uuid.bytes,
|
|
146
196
|
display_name=user.display_name,
|
|
@@ -175,7 +225,7 @@ class CredentialModel(Base):
|
|
|
175
225
|
)
|
|
176
226
|
|
|
177
227
|
def as_dataclass(self):
|
|
178
|
-
return
|
|
228
|
+
return _LegacyCredential(
|
|
179
229
|
uuid=UUID(bytes=self.uuid),
|
|
180
230
|
credential_id=self.credential_id,
|
|
181
231
|
user_uuid=UUID(bytes=self.user_uuid),
|
|
@@ -210,7 +260,7 @@ class SessionModel(Base):
|
|
|
210
260
|
)
|
|
211
261
|
|
|
212
262
|
def as_dataclass(self):
|
|
213
|
-
return
|
|
263
|
+
return _LegacySession(
|
|
214
264
|
key=self.key,
|
|
215
265
|
user_uuid=UUID(bytes=self.user_uuid),
|
|
216
266
|
credential_uuid=UUID(bytes=self.credential_uuid),
|
|
@@ -221,7 +271,7 @@ class SessionModel(Base):
|
|
|
221
271
|
)
|
|
222
272
|
|
|
223
273
|
@staticmethod
|
|
224
|
-
def from_dataclass(session:
|
|
274
|
+
def from_dataclass(session: _LegacySession):
|
|
225
275
|
return SessionModel(
|
|
226
276
|
key=session.key,
|
|
227
277
|
user_uuid=session.user_uuid.bytes,
|
|
@@ -243,8 +293,8 @@ class ResetTokenModel(Base):
|
|
|
243
293
|
token_type: Mapped[str] = mapped_column(String, nullable=False)
|
|
244
294
|
expiry: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
245
295
|
|
|
246
|
-
def as_dataclass(self) ->
|
|
247
|
-
return
|
|
296
|
+
def as_dataclass(self) -> _LegacyResetToken:
|
|
297
|
+
return _LegacyResetToken(
|
|
248
298
|
key=self.key,
|
|
249
299
|
user_uuid=UUID(bytes=self.user_uuid),
|
|
250
300
|
token_type=self.token_type,
|
paskia/remoteauth.py
CHANGED
|
@@ -24,7 +24,7 @@ from datetime import datetime, timedelta, timezone
|
|
|
24
24
|
from typing import Callable
|
|
25
25
|
from uuid import UUID
|
|
26
26
|
|
|
27
|
-
from paskia.util import passphrase
|
|
27
|
+
from paskia.util import passphrase, pow
|
|
28
28
|
|
|
29
29
|
# Remote auth requests expire after this duration
|
|
30
30
|
REMOTE_AUTH_LIFETIME = timedelta(minutes=5)
|
|
@@ -319,7 +319,6 @@ class RemoteAuthManager:
|
|
|
319
319
|
Returns:
|
|
320
320
|
PoW work units (pow.NORMAL or pow.HARD)
|
|
321
321
|
"""
|
|
322
|
-
from paskia.util import pow
|
|
323
322
|
|
|
324
323
|
count = self.get_connection_count()
|
|
325
324
|
return pow.HARD if count >= 10 else pow.NORMAL
|
paskia/sansio.py
CHANGED
|
@@ -8,11 +8,9 @@ This module provides a unified interface for WebAuthn operations including:
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
-
from datetime import datetime, timezone
|
|
12
11
|
from urllib.parse import urlparse
|
|
13
12
|
from uuid import UUID
|
|
14
13
|
|
|
15
|
-
import uuid7
|
|
16
14
|
from webauthn import (
|
|
17
15
|
generate_authentication_options,
|
|
18
16
|
generate_registration_options,
|
|
@@ -176,14 +174,12 @@ class Passkey:
|
|
|
176
174
|
expected_origin=origin,
|
|
177
175
|
expected_rp_id=self.rp_id,
|
|
178
176
|
)
|
|
179
|
-
return Credential(
|
|
180
|
-
uuid=uuid7.create(),
|
|
177
|
+
return Credential.create(
|
|
181
178
|
credential_id=credential.raw_id,
|
|
182
|
-
|
|
179
|
+
user=user_uuid,
|
|
183
180
|
aaguid=UUID(registration.aaguid),
|
|
184
181
|
public_key=registration.credential_public_key,
|
|
185
182
|
sign_count=registration.sign_count,
|
|
186
|
-
created_at=datetime.now(timezone.utc),
|
|
187
183
|
)
|
|
188
184
|
|
|
189
185
|
### Authentication Methods ###
|
|
@@ -234,8 +230,11 @@ class Passkey:
|
|
|
234
230
|
Args:
|
|
235
231
|
credential: The authentication credential response from the client
|
|
236
232
|
expected_challenge: The earlier generated challenge bytes
|
|
237
|
-
stored_cred: The server stored credential record (modified
|
|
233
|
+
stored_cred: The server stored credential record (NOT modified)
|
|
238
234
|
origin: The origin URL (required, must be pre-validated)
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
VerifiedAuthentication with new_sign_count and user_verified status
|
|
239
238
|
"""
|
|
240
239
|
# Verify the authentication response
|
|
241
240
|
verification = verify_authentication_response(
|
|
@@ -246,11 +245,6 @@ class Passkey:
|
|
|
246
245
|
credential_public_key=stored_cred.public_key,
|
|
247
246
|
credential_current_sign_count=stored_cred.sign_count,
|
|
248
247
|
)
|
|
249
|
-
stored_cred.sign_count = verification.new_sign_count
|
|
250
|
-
now = datetime.now(timezone.utc)
|
|
251
|
-
stored_cred.last_used = now
|
|
252
|
-
if verification.user_verified:
|
|
253
|
-
stored_cred.last_verified = now
|
|
254
248
|
return verification
|
|
255
249
|
|
|
256
250
|
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: paskia
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Passkey Auth made easy: all sites and APIs can be guarded even without any changes on the protected site.
|
|
5
5
|
Project-URL: Homepage, https://git.zi.fi/LeoVasanko/paskia
|
|
6
6
|
Project-URL: Repository, https://github.com/LeoVasanko/paskia
|
|
7
7
|
Author: Leo Vasanko
|
|
8
8
|
Keywords: FastAPI,auth_request,forward_auth
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
10
|
Requires-Dist: aiofiles>=25.1.0
|
|
11
11
|
Requires-Dist: base64url>=1.0.0
|
|
12
|
+
Requires-Dist: fastapi-vue>=0.3.0
|
|
12
13
|
Requires-Dist: fastapi[standard]>=0.104.1
|
|
13
14
|
Requires-Dist: jsondiff>=2.2.1
|
|
14
15
|
Requires-Dist: msgspec>=0.20.0
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
paskia/__init__.py,sha256=6eopO87IOFA2zfOuqt8Jj8Tdtp93HBMOgUBtTzMRweM,57
|
|
2
|
+
paskia/_version.py,sha256=TvxBYkx8Rz_Q1S3JFp831BRT8Wo0Yxt6TJMtgZKenTo,704
|
|
3
|
+
paskia/authsession.py,sha256=WtHC0iurDfNbjCxbKzZXZL-hklYorLFLsXM2w9JaUu0,1741
|
|
4
|
+
paskia/bootstrap.py,sha256=FShAMvLmLxQ4HfE9yz1ZB6EO9fC-lQroesU4dC58VO8,3875
|
|
5
|
+
paskia/config.py,sha256=BdGzQ3Ja1enSTHmkDkBDGQk_JluT3VaK3Y7AqB5xMlk,723
|
|
6
|
+
paskia/globals.py,sha256=ip03kLoS_27cNIgXTVcXNoeQDjTAC_IILuXaHKShTws,1712
|
|
7
|
+
paskia/remoteauth.py,sha256=0lDH6mQg7YGxTdj4OHSJFJzAahtK8M8nrCK6k0iLk20,12450
|
|
8
|
+
paskia/sansio.py,sha256=LQRdV1kW_aGwDWC8fhyEvqWPwKZVx_8qzQv65et6utg,9727
|
|
9
|
+
paskia/aaguid/__init__.py,sha256=Moy67wiJSYulL_whgEEBBKabKEOvlR-NRLyXprDdBO0,1007
|
|
10
|
+
paskia/aaguid/combined_aaguid.json,sha256=CaZ96AiwdAjBnyVZnJ1eolAHxUQMB2H6mDgZkorYg_A,4124722
|
|
11
|
+
paskia/db/__init__.py,sha256=5ZUTqpwY939DvR-4ejodwXep8dPtml8k2Z_2ecPJoTM,3274
|
|
12
|
+
paskia/db/background.py,sha256=6vBLFh0HPIZQy9fSbSX5C3MmnkkxyOaJ3V4MKPz0aaQ,3569
|
|
13
|
+
paskia/db/jsonl.py,sha256=9fX31DmQL3YiTIcwwOaao6Z6grGZGrMyw-U-M5apXNQ,10240
|
|
14
|
+
paskia/db/migrations.py,sha256=fwdVbIf2ybXra8qO2th3G26ocpUCftA4lP4s6zRhjoY,969
|
|
15
|
+
paskia/db/operations.py,sha256=mva4hMsi8080AisjgXXuo5PGJ55zWPzmcl4v_3CLseM,29826
|
|
16
|
+
paskia/db/structs.py,sha256=y6X2W0t64qvzDnin6HRoiMFHV-KhovaPe4fPnV1tF_g,7838
|
|
17
|
+
paskia/fastapi/__init__.py,sha256=NFsTX1qytoyZKiur7RDTa2fxiOWHrop5CAAx8rqK9E0,58
|
|
18
|
+
paskia/fastapi/__main__.py,sha256=qxApOdjY51pQquDsmoMNkXWEBTr2edQo-p2AKB8NbWc,7701
|
|
19
|
+
paskia/fastapi/admin.py,sha256=51Gm3Mf_laHpXJNfR6d6-zPy0L__zQ7L95XJ-TDwS40,34960
|
|
20
|
+
paskia/fastapi/api.py,sha256=zUM7x3w05STphaVAAmd7e02-D8_ETtz-zR4vyiFZBiY,9377
|
|
21
|
+
paskia/fastapi/auth_host.py,sha256=Y5w9Mz6jyq0hj7SX8LfwebaesUOLGcWzGW9lsmw5WOo,3242
|
|
22
|
+
paskia/fastapi/authz.py,sha256=6s2TGkb3C7qWTXHOaMj613NiqMfztqM5QENuSb2IjO8,3529
|
|
23
|
+
paskia/fastapi/mainapp.py,sha256=SgPuXbgh82S0zlCfdlxmZajicwE4w7yh67PsSw0pdFI,4622
|
|
24
|
+
paskia/fastapi/remote.py,sha256=bbE9U90wo7cpfXxMlBFm7s-eR0ALq7ImiKKUXeynA1s,18835
|
|
25
|
+
paskia/fastapi/reset.py,sha256=k5UEVc3yWKwc3tHk7IuKAAdmSsbX3Zq9J6twXpaU9so,3609
|
|
26
|
+
paskia/fastapi/session.py,sha256=BRnlgR8pTY7o0f7qFnkdyepS2fKEAgqwT9Hj951sZJM,1479
|
|
27
|
+
paskia/fastapi/user.py,sha256=2mB4OU6BxeO9KQBvd1usgzSvTW5rsY37UdPp_OcGjr4,4620
|
|
28
|
+
paskia/fastapi/ws.py,sha256=p0rHGbvglWw7S2ti89iTy_FlEScxjYqL1oJxkoWcPDY,4479
|
|
29
|
+
paskia/fastapi/wschat.py,sha256=eUtVTGyxVIG2DOZIpjeYGgJRBpU5LnWfTT66vIsKaOA,1818
|
|
30
|
+
paskia/fastapi/wsutil.py,sha256=CJZOyy4e9jazgyvj6CQ_kXVlGtpHNwfjGOdmHL02Nec,2620
|
|
31
|
+
paskia/frontend-build/auth/index.html,sha256=PhNRnHQSaQCIyiSl8fZqidC-PIGMe_KWLJokw0N8628,936
|
|
32
|
+
paskia/frontend-build/auth/admin/index.html,sha256=jiCkfwVKDRT-nmz1OEwWzZBw8tNMXGnNjIfaIBXJ4fE,862
|
|
33
|
+
paskia/frontend-build/auth/assets/AccessDenied-DPkUS8LZ.css,sha256=s7BvCBUV8YZQA1riEuXFvBGcm8dRr6QNZ1VJMBWoKPA,7941
|
|
34
|
+
paskia/frontend-build/auth/assets/AccessDenied-Fmeb6EtF.js,sha256=6ZgY9gSDDLtQUq0k4N5YWlpqpGyQW9r_X9ocWgSHuVM,51702
|
|
35
|
+
paskia/frontend-build/auth/assets/RestrictedAuth-CvR33_Z0.css,sha256=jOnmoy1hon9QwzoY9tHmhd-0zUye9v9-bv5zPku82GY,5397
|
|
36
|
+
paskia/frontend-build/auth/assets/RestrictedAuth-DsJXicIw.js,sha256=YctOv9_npORcB8kdudZs2iEf2yKhWJkLPTZTkvu-qB8,9761
|
|
37
|
+
paskia/frontend-build/auth/assets/_plugin-vue_export-helper-BTzJAQlS.css,sha256=GpCu32ZoXHmL_8wVVa0Yja8dtKpaJSRyfitYAURx4Yc,12796
|
|
38
|
+
paskia/frontend-build/auth/assets/_plugin-vue_export-helper-nhjnO_bd.js,sha256=6UQZRGDUU0m_cRZd_QKtt-4C1-3H2MtocoRR89is2AI,84746
|
|
39
|
+
paskia/frontend-build/auth/assets/admin-CPE1pLMm.js,sha256=EgB9vPz_8Zey0klCLnAaQgPANnLrAXWnar64oZFZiqM,41005
|
|
40
|
+
paskia/frontend-build/auth/assets/admin-DzzjSg72.css,sha256=X8K-ycZwZ3gXNdB8YFMxdGpRD8uS_Qeq0_awx9eJ-VY,7599
|
|
41
|
+
paskia/frontend-build/auth/assets/auth-C7k64Wad.css,sha256=x1CKaX0MmuunxZvew4ArCusqTYNrNjLv4JW2bnvZhDg,4333
|
|
42
|
+
paskia/frontend-build/auth/assets/auth-YIZvPlW_.js,sha256=pSJnxtX9ALwKNAu0rL7LAfr0zZIxy_Y--Qn5UllyMng,25209
|
|
43
|
+
paskia/frontend-build/auth/assets/forward-DmqVHZ7e.js,sha256=rfiUg6N9ez9WuZA-pWf0CViV7j4-UoWqH3riFDr81xE,782
|
|
44
|
+
paskia/frontend-build/auth/assets/helpers-DzjFIx78.js,sha256=w_IsCBn3QwidsuwQhVRycd8Fa53lvbgRGGojTBXVlUc,940
|
|
45
|
+
paskia/frontend-build/auth/assets/pow-2N9bxgAo.js,sha256=7AfzW5lcTefPI6YGXrYao1b56L7v5Bon9Y9N40yHsaE,9447
|
|
46
|
+
paskia/frontend-build/auth/assets/reset-Chtv69AT.css,sha256=1iFB1F8va7Ktdn7YxAMyNU8keFcaalmo25mCCM5QQys,238
|
|
47
|
+
paskia/frontend-build/auth/assets/reset-s20PATTN.js,sha256=Fo4u2Kz58spVVJ2BmV3Q_HPG8x5jysL5_0KDYQbVRac,3976
|
|
48
|
+
paskia/frontend-build/auth/assets/restricted-D3AJx3_6.js,sha256=H7L5F3nW6btinOZPgi8U_yLkxBO93L_rTeDMUWx7-Ow,1023
|
|
49
|
+
paskia/frontend-build/auth/restricted/index.html,sha256=6RPwe3edRPjM2-TLp87S9C170NM0gGYufboL_sp7q-k,785
|
|
50
|
+
paskia/frontend-build/int/forward/index.html,sha256=iA5lgiEgLxszNEdvTi7jMawKkhSTWydU8idRs0LLKlw,870
|
|
51
|
+
paskia/frontend-build/int/reset/index.html,sha256=E6ylRX8-OupR3eBUPSzFRH1_rXVft93esAY0zaoVFBQ,612
|
|
52
|
+
paskia/migrate/__init__.py,sha256=CuJxt4w7ZvnL4JJ6FxiBKXTSRvCx-rPxKe9CbMGagD4,9896
|
|
53
|
+
paskia/migrate/sql.py,sha256=MK6KfmvoD3U65IN6EV2au3mhdLdicFIrV83mpQq8Bp4,14096
|
|
54
|
+
paskia-0.9.0.dist-info/METADATA,sha256=fGtkP1RbaVeNZ2B-mcaX7hCgZn9Nc0QS1gfPg5av4l8,4261
|
|
55
|
+
paskia-0.9.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
56
|
+
paskia-0.9.0.dist-info/entry_points.txt,sha256=vvx6RYetgd61I2ODqQPHqrKHgCfuo08w_T35yDlHenE,93
|
|
57
|
+
paskia-0.9.0.dist-info/RECORD,,
|