Database cleanup: make it synchronous because we work with in-memory data. Defer writes to disk and cleanup to background task. Tests passing.
This commit is contained in:
@@ -55,7 +55,7 @@ async def create_session(
|
|||||||
raise ValueError(f"Host must be the same as or a subdomain of {rp_id}")
|
raise ValueError(f"Host must be the same as or a subdomain of {rp_id}")
|
||||||
token = create_token()
|
token = create_token()
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
await db.create_session(
|
db.create_session(
|
||||||
user_uuid=user_uuid,
|
user_uuid=user_uuid,
|
||||||
credential_uuid=credential_uuid,
|
credential_uuid=credential_uuid,
|
||||||
key=session_key(token),
|
key=session_key(token),
|
||||||
@@ -69,7 +69,7 @@ async def create_session(
|
|||||||
|
|
||||||
async def get_reset(token: str) -> ResetToken:
|
async def get_reset(token: str) -> ResetToken:
|
||||||
"""Validate a credential reset token. Returns None if the token is not well formed (i.e. it is another type of token)."""
|
"""Validate a credential reset token. Returns None if the token is not well formed (i.e. it is another type of token)."""
|
||||||
record = await db.get_reset_token(reset_key(token))
|
record = db.get_reset_token(reset_key(token))
|
||||||
if record and record.expiry >= datetime.now(timezone.utc):
|
if record and record.expiry >= datetime.now(timezone.utc):
|
||||||
return record
|
return record
|
||||||
raise ValueError("This authentication link is no longer valid.")
|
raise ValueError("This authentication link is no longer valid.")
|
||||||
@@ -80,11 +80,11 @@ async def get_session(token: str, host: str | None = None) -> Session:
|
|||||||
host = hostutil.normalize_host(host)
|
host = hostutil.normalize_host(host)
|
||||||
if not host:
|
if not host:
|
||||||
raise ValueError("Invalid host")
|
raise ValueError("Invalid host")
|
||||||
session = await db.get_session(session_key(token))
|
session = db.get_session(session_key(token))
|
||||||
if session and session_expiry(session) >= datetime.now(timezone.utc):
|
if session and session_expiry(session) >= datetime.now(timezone.utc):
|
||||||
if session.host is None:
|
if session.host is None:
|
||||||
# First time binding: store exact host:port (or IPv6 form) now.
|
# First time binding: store exact host:port (or IPv6 form) now.
|
||||||
await db.set_session_host(session.key, host)
|
db.set_session_host(session.key, host)
|
||||||
session.host = host
|
session.host = host
|
||||||
elif session.host != host:
|
elif session.host != host:
|
||||||
raise ValueError("Session host mismatch")
|
raise ValueError("Session host mismatch")
|
||||||
@@ -94,10 +94,10 @@ async def get_session(token: str, host: str | None = None) -> Session:
|
|||||||
|
|
||||||
async def refresh_session_token(token: str, *, ip: str, user_agent: str):
|
async def refresh_session_token(token: str, *, ip: str, user_agent: str):
|
||||||
"""Refresh a session extending its expiry."""
|
"""Refresh a session extending its expiry."""
|
||||||
session_record = await db.get_session(session_key(token))
|
session_record = db.get_session(session_key(token))
|
||||||
if not session_record:
|
if not session_record:
|
||||||
raise ValueError("Session not found or expired")
|
raise ValueError("Session not found or expired")
|
||||||
updated = await db.update_session(
|
updated = db.update_session(
|
||||||
session_key(token),
|
session_key(token),
|
||||||
ip=ip,
|
ip=ip,
|
||||||
user_agent=user_agent,
|
user_agent=user_agent,
|
||||||
@@ -110,4 +110,4 @@ async def refresh_session_token(token: str, *, ip: str, user_agent: str):
|
|||||||
async def delete_credential(credential_uuid: UUID, auth: str, host: str | None = None):
|
async def delete_credential(credential_uuid: UUID, auth: str, host: str | None = None):
|
||||||
"""Delete a specific credential for the current user."""
|
"""Delete a specific credential for the current user."""
|
||||||
s = await get_session(auth, host=host)
|
s = await get_session(auth, host=host)
|
||||||
await db.delete_credential(credential_uuid, s.user_uuid)
|
db.delete_credential(credential_uuid, s.user_uuid)
|
||||||
|
|||||||
+10
-10
@@ -42,7 +42,7 @@ async def _create_and_log_admin_reset_link(user_uuid, message, session_type) ->
|
|||||||
"""Create an admin reset link and log it with the provided message."""
|
"""Create an admin reset link and log it with the provided message."""
|
||||||
token = passphrase.generate()
|
token = passphrase.generate()
|
||||||
expiry = authsession.reset_expires()
|
expiry = authsession.reset_expires()
|
||||||
await db.create_reset_token(
|
db.create_reset_token(
|
||||||
user_uuid=user_uuid,
|
user_uuid=user_uuid,
|
||||||
key=tokens.reset_key(token),
|
key=tokens.reset_key(token),
|
||||||
expiry=expiry,
|
expiry=expiry,
|
||||||
@@ -62,14 +62,14 @@ async def bootstrap_system() -> dict:
|
|||||||
"""
|
"""
|
||||||
# Create permission first - will fail if already exists
|
# Create permission first - will fail if already exists
|
||||||
perm0 = Permission(id="auth:admin", display_name="Master Admin")
|
perm0 = Permission(id="auth:admin", display_name="Master Admin")
|
||||||
await db.create_permission(perm0)
|
db.create_permission(perm0)
|
||||||
|
|
||||||
org = Org(uuid7.create(), "Organization")
|
org = Org(uuid7.create(), "Organization")
|
||||||
await db.create_organization(org)
|
db.create_organization(org)
|
||||||
|
|
||||||
# After creation, org.permissions now includes the auto-created org admin permission
|
# After creation, org.permissions now includes the auto-created org admin permission
|
||||||
# Allow this org to grant global admin explicitly
|
# Allow this org to grant global admin explicitly
|
||||||
await db.add_permission_to_organization(str(org.uuid), perm0.id)
|
db.add_permission_to_organization(str(org.uuid), perm0.id)
|
||||||
|
|
||||||
# Create an Administration role granting both org and global admin
|
# Create an Administration role granting both org and global admin
|
||||||
# Compose permissions for Administration role: global admin + org admin auto-perm
|
# Compose permissions for Administration role: global admin + org admin auto-perm
|
||||||
@@ -79,7 +79,7 @@ async def bootstrap_system() -> dict:
|
|||||||
"Administration",
|
"Administration",
|
||||||
permissions=[perm0.id, *org.permissions],
|
permissions=[perm0.id, *org.permissions],
|
||||||
)
|
)
|
||||||
await db.create_role(role)
|
db.create_role(role)
|
||||||
|
|
||||||
user = User(
|
user = User(
|
||||||
uuid=uuid7.create(),
|
uuid=uuid7.create(),
|
||||||
@@ -88,7 +88,7 @@ async def bootstrap_system() -> dict:
|
|||||||
created_at=datetime.now(timezone.utc),
|
created_at=datetime.now(timezone.utc),
|
||||||
visits=0,
|
visits=0,
|
||||||
)
|
)
|
||||||
await db.create_user(user)
|
db.create_user(user)
|
||||||
|
|
||||||
# Generate reset link and log it
|
# Generate reset link and log it
|
||||||
reset_link = await _create_and_log_admin_reset_link(
|
reset_link = await _create_and_log_admin_reset_link(
|
||||||
@@ -116,7 +116,7 @@ async def check_admin_credentials() -> bool:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Get permission organizations to find admin users
|
# Get permission organizations to find admin users
|
||||||
permission_orgs = await db.get_permission_organizations(
|
permission_orgs = db.get_permission_organizations(
|
||||||
"auth:admin"
|
"auth:admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ async def check_admin_credentials() -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Get users from the first organization with admin permission
|
# Get users from the first organization with admin permission
|
||||||
org_users = await db.get_organization_users(
|
org_users = db.get_organization_users(
|
||||||
str(permission_orgs[0].uuid)
|
str(permission_orgs[0].uuid)
|
||||||
)
|
)
|
||||||
admin_users = [user for user, role in org_users if role == "Administration"]
|
admin_users = [user for user, role in org_users if role == "Administration"]
|
||||||
@@ -134,7 +134,7 @@ async def check_admin_credentials() -> bool:
|
|||||||
|
|
||||||
# Check first admin user for credentials
|
# Check first admin user for credentials
|
||||||
admin_user = admin_users[0]
|
admin_user = admin_users[0]
|
||||||
credentials = await db.get_credentials_by_user_uuid(
|
credentials = db.get_credentials_by_user_uuid(
|
||||||
admin_user.uuid
|
admin_user.uuid
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ async def bootstrap_if_needed() -> bool:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Check if the admin permission exists - if it does, system is already bootstrapped
|
# Check if the admin permission exists - if it does, system is already bootstrapped
|
||||||
await db.get_permission("auth:admin")
|
db.get_permission("auth:admin")
|
||||||
# Permission exists, system is already bootstrapped
|
# Permission exists, system is already bootstrapped
|
||||||
# Check if admin needs credentials (only for already-bootstrapped systems)
|
# Check if admin needs credentials (only for already-bootstrapped systems)
|
||||||
await check_admin_credentials()
|
await check_admin_credentials()
|
||||||
|
|||||||
+11
-3
@@ -3,13 +3,14 @@ Database module for WebAuthn passkey authentication.
|
|||||||
|
|
||||||
This module re-exports the JSONL database types and implementation.
|
This module re-exports the JSONL database types and implementation.
|
||||||
All data types are msgspec Structs for efficient serialization.
|
All data types are msgspec Structs for efficient serialization.
|
||||||
|
Database methods are synchronous (no await needed).
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from paskia import db
|
from paskia import db
|
||||||
|
|
||||||
# Access the database instance (after init)
|
# Access the database instance (after init)
|
||||||
await db.create_session(...)
|
db.create_session(...)
|
||||||
user = await db.get_user_by_uuid(uuid)
|
user = db.get_user_by_uuid(uuid)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from paskia.db.json import (
|
from paskia.db.json import (
|
||||||
@@ -23,8 +24,11 @@ from paskia.db.json import (
|
|||||||
SessionContext,
|
SessionContext,
|
||||||
User,
|
User,
|
||||||
init,
|
init,
|
||||||
|
start_background,
|
||||||
|
stop_background,
|
||||||
|
start_cleanup,
|
||||||
|
stop_cleanup,
|
||||||
)
|
)
|
||||||
from paskia.db.json import _db as _json_db
|
|
||||||
import paskia.db.json as _json_module
|
import paskia.db.json as _json_module
|
||||||
|
|
||||||
|
|
||||||
@@ -63,4 +67,8 @@ __all__ = [
|
|||||||
"SessionContext",
|
"SessionContext",
|
||||||
"User",
|
"User",
|
||||||
"init",
|
"init",
|
||||||
|
"start_background",
|
||||||
|
"stop_background",
|
||||||
|
"start_cleanup",
|
||||||
|
"stop_cleanup",
|
||||||
]
|
]
|
||||||
|
|||||||
+407
-426
File diff suppressed because it is too large
Load Diff
+46
-46
@@ -59,7 +59,7 @@ async def admin_list_orgs(request: Request, auth=AUTH_COOKIE):
|
|||||||
match=permutil.has_any,
|
match=permutil.has_any,
|
||||||
host=request.headers.get("host"),
|
host=request.headers.get("host"),
|
||||||
)
|
)
|
||||||
orgs = await db.list_organizations()
|
orgs = db.list_organizations()
|
||||||
if "auth:admin" not in ctx.role.permissions:
|
if "auth:admin" not in ctx.role.permissions:
|
||||||
orgs = [o for o in orgs if f"auth:org:{o.uuid}" in ctx.role.permissions]
|
orgs = [o for o in orgs if f"auth:org:{o.uuid}" in ctx.role.permissions]
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ async def admin_list_orgs(request: Request, auth=AUTH_COOKIE):
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def org_to_dict(o):
|
async def org_to_dict(o):
|
||||||
users = await db.get_organization_users(str(o.uuid))
|
users = db.get_organization_users(str(o.uuid))
|
||||||
return {
|
return {
|
||||||
"uuid": str(o.uuid),
|
"uuid": str(o.uuid),
|
||||||
"display_name": o.display_name,
|
"display_name": o.display_name,
|
||||||
@@ -107,7 +107,7 @@ async def admin_create_org(
|
|||||||
display_name = payload.get("display_name") or "New Organization"
|
display_name = payload.get("display_name") or "New Organization"
|
||||||
permissions = payload.get("permissions") or []
|
permissions = payload.get("permissions") or []
|
||||||
org = OrgDC(uuid=org_uuid, display_name=display_name, permissions=permissions)
|
org = OrgDC(uuid=org_uuid, display_name=display_name, permissions=permissions)
|
||||||
await db.create_organization(org)
|
db.create_organization(org)
|
||||||
|
|
||||||
# Automatically create Administration role with org admin permission
|
# Automatically create Administration role with org admin permission
|
||||||
role_uuid = uuid4()
|
role_uuid = uuid4()
|
||||||
@@ -117,7 +117,7 @@ async def admin_create_org(
|
|||||||
display_name="Administration",
|
display_name="Administration",
|
||||||
permissions=[f"auth:org:{org_uuid}"],
|
permissions=[f"auth:org:{org_uuid}"],
|
||||||
)
|
)
|
||||||
await db.create_role(admin_role)
|
db.create_role(admin_role)
|
||||||
|
|
||||||
return {"uuid": str(org_uuid)}
|
return {"uuid": str(org_uuid)}
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ async def admin_update_org(
|
|||||||
)
|
)
|
||||||
from ..db import Org as OrgDC # local import to avoid cycles
|
from ..db import Org as OrgDC # local import to avoid cycles
|
||||||
|
|
||||||
current = await db.get_organization(str(org_uuid))
|
current = db.get_organization(str(org_uuid))
|
||||||
display_name = payload.get("display_name") or current.display_name
|
display_name = payload.get("display_name") or current.display_name
|
||||||
permissions = payload.get("permissions")
|
permissions = payload.get("permissions")
|
||||||
if permissions is None:
|
if permissions is None:
|
||||||
@@ -157,7 +157,7 @@ async def admin_update_org(
|
|||||||
)
|
)
|
||||||
|
|
||||||
org = OrgDC(uuid=org_uuid, display_name=display_name, permissions=permissions)
|
org = OrgDC(uuid=org_uuid, display_name=display_name, permissions=permissions)
|
||||||
await db.update_organization(org)
|
db.update_organization(org)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ async def admin_delete_org(org_uuid: UUID, request: Request, auth=AUTH_COOKIE):
|
|||||||
|
|
||||||
# Delete organization-specific permissions
|
# Delete organization-specific permissions
|
||||||
org_perm_pattern = f"org:{str(org_uuid).lower()}"
|
org_perm_pattern = f"org:{str(org_uuid).lower()}"
|
||||||
all_permissions = await db.list_permissions()
|
all_permissions = db.list_permissions()
|
||||||
for perm in all_permissions:
|
for perm in all_permissions:
|
||||||
perm_id_lower = perm.id.lower()
|
perm_id_lower = perm.id.lower()
|
||||||
# Check if permission contains "org:{uuid}" separated by colons or at boundaries
|
# Check if permission contains "org:{uuid}" separated by colons or at boundaries
|
||||||
@@ -185,9 +185,9 @@ async def admin_delete_org(org_uuid: UUID, request: Request, auth=AUTH_COOKIE):
|
|||||||
or perm_id_lower.endswith(f":{org_perm_pattern}")
|
or perm_id_lower.endswith(f":{org_perm_pattern}")
|
||||||
or perm_id_lower == org_perm_pattern
|
or perm_id_lower == org_perm_pattern
|
||||||
):
|
):
|
||||||
await db.delete_permission(perm.id)
|
db.delete_permission(perm.id)
|
||||||
|
|
||||||
await db.delete_organization(org_uuid)
|
db.delete_organization(org_uuid)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ async def admin_add_org_permission(
|
|||||||
await authz.verify(
|
await authz.verify(
|
||||||
auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all
|
auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all
|
||||||
)
|
)
|
||||||
await db.add_permission_to_organization(str(org_uuid), permission_id)
|
db.add_permission_to_organization(str(org_uuid), permission_id)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ async def admin_remove_org_permission(
|
|||||||
await authz.verify(
|
await authz.verify(
|
||||||
auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all
|
auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all
|
||||||
)
|
)
|
||||||
await db.remove_permission_from_organization(str(org_uuid), permission_id)
|
db.remove_permission_from_organization(str(org_uuid), permission_id)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@@ -240,10 +240,10 @@ async def admin_create_role(
|
|||||||
role_uuid = uuid4()
|
role_uuid = uuid4()
|
||||||
display_name = payload.get("display_name") or "New Role"
|
display_name = payload.get("display_name") or "New Role"
|
||||||
perms = payload.get("permissions") or []
|
perms = payload.get("permissions") or []
|
||||||
org = await db.get_organization(str(org_uuid))
|
org = db.get_organization(str(org_uuid))
|
||||||
grantable = set(org.permissions or [])
|
grantable = set(org.permissions or [])
|
||||||
for pid in perms:
|
for pid in perms:
|
||||||
await db.get_permission(pid)
|
db.get_permission(pid)
|
||||||
if pid not in grantable:
|
if pid not in grantable:
|
||||||
raise ValueError(f"Permission not grantable by org: {pid}")
|
raise ValueError(f"Permission not grantable by org: {pid}")
|
||||||
role = RoleDC(
|
role = RoleDC(
|
||||||
@@ -252,7 +252,7 @@ async def admin_create_role(
|
|||||||
display_name=display_name,
|
display_name=display_name,
|
||||||
permissions=perms,
|
permissions=perms,
|
||||||
)
|
)
|
||||||
await db.create_role(role)
|
db.create_role(role)
|
||||||
return {"uuid": str(role_uuid)}
|
return {"uuid": str(role_uuid)}
|
||||||
|
|
||||||
|
|
||||||
@@ -271,7 +271,7 @@ async def admin_update_role(
|
|||||||
match=permutil.has_any,
|
match=permutil.has_any,
|
||||||
host=request.headers.get("host"),
|
host=request.headers.get("host"),
|
||||||
)
|
)
|
||||||
role = await db.get_role(role_uuid)
|
role = db.get_role(role_uuid)
|
||||||
if role.org_uuid != org_uuid:
|
if role.org_uuid != org_uuid:
|
||||||
raise HTTPException(status_code=404, detail="Role not found in organization")
|
raise HTTPException(status_code=404, detail="Role not found in organization")
|
||||||
from ..db import Role as RoleDC
|
from ..db import Role as RoleDC
|
||||||
@@ -280,11 +280,11 @@ async def admin_update_role(
|
|||||||
permissions = payload.get("permissions")
|
permissions = payload.get("permissions")
|
||||||
if permissions is None:
|
if permissions is None:
|
||||||
permissions = role.permissions
|
permissions = role.permissions
|
||||||
org = await db.get_organization(str(org_uuid))
|
org = db.get_organization(str(org_uuid))
|
||||||
grantable = set(org.permissions or [])
|
grantable = set(org.permissions or [])
|
||||||
existing_permissions = set(role.permissions)
|
existing_permissions = set(role.permissions)
|
||||||
for pid in permissions:
|
for pid in permissions:
|
||||||
await db.get_permission(pid)
|
db.get_permission(pid)
|
||||||
if pid not in existing_permissions and pid not in grantable:
|
if pid not in existing_permissions and pid not in grantable:
|
||||||
raise ValueError(f"Permission not grantable by org: {pid}")
|
raise ValueError(f"Permission not grantable by org: {pid}")
|
||||||
|
|
||||||
@@ -302,7 +302,7 @@ async def admin_update_role(
|
|||||||
display_name=display_name,
|
display_name=display_name,
|
||||||
permissions=permissions,
|
permissions=permissions,
|
||||||
)
|
)
|
||||||
await db.update_role(updated)
|
db.update_role(updated)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@@ -320,7 +320,7 @@ async def admin_delete_role(
|
|||||||
host=request.headers.get("host"),
|
host=request.headers.get("host"),
|
||||||
max_age="5m",
|
max_age="5m",
|
||||||
)
|
)
|
||||||
role = await db.get_role(role_uuid)
|
role = db.get_role(role_uuid)
|
||||||
if role.org_uuid != org_uuid:
|
if role.org_uuid != org_uuid:
|
||||||
raise HTTPException(status_code=404, detail="Role not found in organization")
|
raise HTTPException(status_code=404, detail="Role not found in organization")
|
||||||
|
|
||||||
@@ -328,7 +328,7 @@ async def admin_delete_role(
|
|||||||
if ctx.role.uuid == role_uuid:
|
if ctx.role.uuid == role_uuid:
|
||||||
raise ValueError("Cannot delete your own role")
|
raise ValueError("Cannot delete your own role")
|
||||||
|
|
||||||
await db.delete_role(role_uuid)
|
db.delete_role(role_uuid)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@@ -354,7 +354,7 @@ async def admin_create_user(
|
|||||||
raise ValueError("display_name and role are required")
|
raise ValueError("display_name and role are required")
|
||||||
from ..db import User as UserDC
|
from ..db import User as UserDC
|
||||||
|
|
||||||
roles = await db.get_roles_by_organization(str(org_uuid))
|
roles = db.get_roles_by_organization(str(org_uuid))
|
||||||
role_obj = next((r for r in roles if r.display_name == role_name), None)
|
role_obj = next((r for r in roles if r.display_name == role_name), None)
|
||||||
if not role_obj:
|
if not role_obj:
|
||||||
raise ValueError("Role not found in organization")
|
raise ValueError("Role not found in organization")
|
||||||
@@ -366,7 +366,7 @@ async def admin_create_user(
|
|||||||
visits=0,
|
visits=0,
|
||||||
created_at=None,
|
created_at=None,
|
||||||
)
|
)
|
||||||
await db.create_user(user)
|
db.create_user(user)
|
||||||
return {"uuid": str(user_uuid)}
|
return {"uuid": str(user_uuid)}
|
||||||
|
|
||||||
|
|
||||||
@@ -388,12 +388,12 @@ async def admin_update_user_role(
|
|||||||
if not new_role:
|
if not new_role:
|
||||||
raise ValueError("role is required")
|
raise ValueError("role is required")
|
||||||
try:
|
try:
|
||||||
user_org, _current_role = await db.get_user_organization(user_uuid)
|
user_org, _current_role = db.get_user_organization(user_uuid)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError("User not found")
|
raise ValueError("User not found")
|
||||||
if user_org.uuid != org_uuid:
|
if user_org.uuid != org_uuid:
|
||||||
raise ValueError("User does not belong to this organization")
|
raise ValueError("User does not belong to this organization")
|
||||||
roles = await db.get_roles_by_organization(str(org_uuid))
|
roles = db.get_roles_by_organization(str(org_uuid))
|
||||||
if not any(r.display_name == new_role for r in roles):
|
if not any(r.display_name == new_role for r in roles):
|
||||||
raise ValueError("Role not found in organization")
|
raise ValueError("Role not found in organization")
|
||||||
|
|
||||||
@@ -410,7 +410,7 @@ async def admin_update_user_role(
|
|||||||
"Cannot change your own role to one without admin permissions"
|
"Cannot change your own role to one without admin permissions"
|
||||||
)
|
)
|
||||||
|
|
||||||
await db.update_user_role_in_organization(user_uuid, new_role)
|
db.update_user_role_in_organization(user_uuid, new_role)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@@ -422,7 +422,7 @@ async def admin_create_user_registration_link(
|
|||||||
auth=AUTH_COOKIE,
|
auth=AUTH_COOKIE,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
user_org, _role_name = await db.get_user_organization(user_uuid)
|
user_org, _role_name = db.get_user_organization(user_uuid)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
if user_org.uuid != org_uuid:
|
if user_org.uuid != org_uuid:
|
||||||
@@ -443,12 +443,12 @@ async def admin_create_user_registration_link(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Check if user has existing credentials
|
# Check if user has existing credentials
|
||||||
credentials = await db.get_credentials_by_user_uuid(user_uuid)
|
credentials = db.get_credentials_by_user_uuid(user_uuid)
|
||||||
token_type = "user registration" if not credentials else "account recovery"
|
token_type = "user registration" if not credentials else "account recovery"
|
||||||
|
|
||||||
token = passphrase.generate()
|
token = passphrase.generate()
|
||||||
expiry = reset_expires()
|
expiry = reset_expires()
|
||||||
await db.create_reset_token(
|
db.create_reset_token(
|
||||||
user_uuid=user_uuid,
|
user_uuid=user_uuid,
|
||||||
key=tokens.reset_key(token),
|
key=tokens.reset_key(token),
|
||||||
expiry=expiry,
|
expiry=expiry,
|
||||||
@@ -473,7 +473,7 @@ async def admin_get_user_detail(
|
|||||||
auth=AUTH_COOKIE,
|
auth=AUTH_COOKIE,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
user_org, role_name = await db.get_user_organization(user_uuid)
|
user_org, role_name = db.get_user_organization(user_uuid)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
if user_org.uuid != org_uuid:
|
if user_org.uuid != org_uuid:
|
||||||
@@ -491,13 +491,13 @@ async def admin_get_user_detail(
|
|||||||
raise authz.AuthException(
|
raise authz.AuthException(
|
||||||
status_code=403, detail="Insufficient permissions", mode="forbidden"
|
status_code=403, detail="Insufficient permissions", mode="forbidden"
|
||||||
)
|
)
|
||||||
user = await db.get_user_by_uuid(user_uuid)
|
user = db.get_user_by_uuid(user_uuid)
|
||||||
cred_ids = await db.get_credentials_by_user_uuid(user_uuid)
|
cred_ids = db.get_credentials_by_user_uuid(user_uuid)
|
||||||
creds: list[dict] = []
|
creds: list[dict] = []
|
||||||
aaguids: set[str] = set()
|
aaguids: set[str] = set()
|
||||||
for cid in cred_ids:
|
for cid in cred_ids:
|
||||||
try:
|
try:
|
||||||
c = await db.get_credential_by_id(cid)
|
c = db.get_credential_by_id(cid)
|
||||||
except ValueError: # pragma: no cover - race condition handling
|
except ValueError: # pragma: no cover - race condition handling
|
||||||
continue
|
continue
|
||||||
aaguid_str = str(c.aaguid)
|
aaguid_str = str(c.aaguid)
|
||||||
@@ -552,7 +552,7 @@ async def admin_get_user_detail(
|
|||||||
|
|
||||||
# Get sessions for the user
|
# Get sessions for the user
|
||||||
normalized_request_host = hostutil.normalize_host(request.headers.get("host"))
|
normalized_request_host = hostutil.normalize_host(request.headers.get("host"))
|
||||||
session_records = await db.list_sessions_for_user(user_uuid)
|
session_records = db.list_sessions_for_user(user_uuid)
|
||||||
current_session_key = session_key(auth)
|
current_session_key = session_key(auth)
|
||||||
sessions_payload: list[dict] = []
|
sessions_payload: list[dict] = []
|
||||||
for entry in session_records:
|
for entry in session_records:
|
||||||
@@ -623,7 +623,7 @@ async def admin_update_user_display_name(
|
|||||||
auth=AUTH_COOKIE,
|
auth=AUTH_COOKIE,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
user_org, _role_name = await db.get_user_organization(user_uuid)
|
user_org, _role_name = db.get_user_organization(user_uuid)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
if user_org.uuid != org_uuid:
|
if user_org.uuid != org_uuid:
|
||||||
@@ -646,7 +646,7 @@ async def admin_update_user_display_name(
|
|||||||
raise HTTPException(status_code=400, detail="display_name required")
|
raise HTTPException(status_code=400, detail="display_name required")
|
||||||
if len(new_name) > 64:
|
if len(new_name) > 64:
|
||||||
raise HTTPException(status_code=400, detail="display_name too long")
|
raise HTTPException(status_code=400, detail="display_name too long")
|
||||||
await db.update_user_display_name(user_uuid, new_name)
|
db.update_user_display_name(user_uuid, new_name)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@@ -659,7 +659,7 @@ async def admin_delete_user_credential(
|
|||||||
auth=AUTH_COOKIE,
|
auth=AUTH_COOKIE,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
user_org, _role_name = await db.get_user_organization(user_uuid)
|
user_org, _role_name = db.get_user_organization(user_uuid)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
if user_org.uuid != org_uuid:
|
if user_org.uuid != org_uuid:
|
||||||
@@ -678,7 +678,7 @@ async def admin_delete_user_credential(
|
|||||||
raise authz.AuthException(
|
raise authz.AuthException(
|
||||||
status_code=403, detail="Insufficient permissions", mode="forbidden"
|
status_code=403, detail="Insufficient permissions", mode="forbidden"
|
||||||
)
|
)
|
||||||
await db.delete_credential(credential_uuid, user_uuid)
|
db.delete_credential(credential_uuid, user_uuid)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@@ -691,7 +691,7 @@ async def admin_delete_user_session(
|
|||||||
auth=AUTH_COOKIE,
|
auth=AUTH_COOKIE,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
user_org, _role_name = await db.get_user_organization(user_uuid)
|
user_org, _role_name = db.get_user_organization(user_uuid)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
if user_org.uuid != org_uuid:
|
if user_org.uuid != org_uuid:
|
||||||
@@ -717,11 +717,11 @@ async def admin_delete_user_session(
|
|||||||
status_code=400, detail="Invalid session identifier"
|
status_code=400, detail="Invalid session identifier"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
target_session = await db.get_session(target_key)
|
target_session = db.get_session(target_key)
|
||||||
if not target_session or target_session.user_uuid != user_uuid:
|
if not target_session or target_session.user_uuid != user_uuid:
|
||||||
raise HTTPException(status_code=404, detail="Session not found")
|
raise HTTPException(status_code=404, detail="Session not found")
|
||||||
|
|
||||||
await db.delete_session(target_key)
|
db.delete_session(target_key)
|
||||||
|
|
||||||
# Check if admin terminated their own session
|
# Check if admin terminated their own session
|
||||||
current_terminated = target_key == session_key(auth)
|
current_terminated = target_key == session_key(auth)
|
||||||
@@ -739,7 +739,7 @@ async def admin_list_permissions(request: Request, auth=AUTH_COOKIE):
|
|||||||
match=permutil.has_any,
|
match=permutil.has_any,
|
||||||
host=request.headers.get("host"),
|
host=request.headers.get("host"),
|
||||||
)
|
)
|
||||||
perms = await db.list_permissions()
|
perms = db.list_permissions()
|
||||||
|
|
||||||
# Global admins see all permissions
|
# Global admins see all permissions
|
||||||
if "auth:admin" in ctx.role.permissions:
|
if "auth:admin" in ctx.role.permissions:
|
||||||
@@ -771,7 +771,7 @@ async def admin_create_permission(
|
|||||||
if not perm_id or not display_name:
|
if not perm_id or not display_name:
|
||||||
raise ValueError("id and display_name are required")
|
raise ValueError("id and display_name are required")
|
||||||
querysafe.assert_safe(perm_id, field="id")
|
querysafe.assert_safe(perm_id, field="id")
|
||||||
await db.create_permission(PermDC(id=perm_id, display_name=display_name))
|
db.create_permission(PermDC(id=perm_id, display_name=display_name))
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@@ -790,7 +790,7 @@ async def admin_update_permission(
|
|||||||
if not display_name:
|
if not display_name:
|
||||||
raise ValueError("display_name is required")
|
raise ValueError("display_name is required")
|
||||||
querysafe.assert_safe(permission_id, field="permission_id")
|
querysafe.assert_safe(permission_id, field="permission_id")
|
||||||
await db.update_permission(
|
db.update_permission(
|
||||||
PermDC(id=permission_id, display_name=display_name)
|
PermDC(id=permission_id, display_name=display_name)
|
||||||
)
|
)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
@@ -818,10 +818,10 @@ async def admin_rename_permission(
|
|||||||
querysafe.assert_safe(old_id, field="old_id")
|
querysafe.assert_safe(old_id, field="old_id")
|
||||||
querysafe.assert_safe(new_id, field="new_id")
|
querysafe.assert_safe(new_id, field="new_id")
|
||||||
if display_name is None:
|
if display_name is None:
|
||||||
perm = await db.get_permission(old_id)
|
perm = db.get_permission(old_id)
|
||||||
display_name = perm.display_name
|
display_name = perm.display_name
|
||||||
# All current backends support rename_permission
|
# All current backends support rename_permission
|
||||||
await db.rename_permission(old_id, new_id, display_name)
|
db.rename_permission(old_id, new_id, display_name)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@@ -844,5 +844,5 @@ async def admin_delete_permission(
|
|||||||
if permission_id == "auth:admin":
|
if permission_id == "auth:admin":
|
||||||
raise ValueError("Cannot delete the master admin permission")
|
raise ValueError("Cannot delete the master admin permission")
|
||||||
|
|
||||||
await db.delete_permission(permission_id)
|
db.delete_permission(permission_id)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ async def api_token_info(token: str):
|
|||||||
# Check if this is a reset token
|
# Check if this is a reset token
|
||||||
try:
|
try:
|
||||||
reset_token = await get_reset(token)
|
reset_token = await get_reset(token)
|
||||||
user = await db.get_user_by_uuid(reset_token.user_uuid)
|
user = db.get_user_by_uuid(reset_token.user_uuid)
|
||||||
return {
|
return {
|
||||||
"type": "reset",
|
"type": "reset",
|
||||||
"user_name": user.display_name,
|
"user_name": user.display_name,
|
||||||
@@ -297,7 +297,7 @@ async def api_logout(request: Request, response: Response, auth=AUTH_COOKIE):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return {"message": "Already logged out"}
|
return {"message": "Already logged out"}
|
||||||
with suppress(Exception):
|
with suppress(Exception):
|
||||||
await db.delete_session(session_key(auth))
|
db.delete_session(session_key(auth))
|
||||||
session.clear_session_cookie(response)
|
session.clear_session_cookie(response)
|
||||||
return {"message": "Logged out successfully"}
|
return {"message": "Logged out successfully"}
|
||||||
|
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ async def websocket_remote_auth_permit(ws: WebSocket):
|
|||||||
|
|
||||||
# Fetch and verify credential
|
# Fetch and verify credential
|
||||||
try:
|
try:
|
||||||
stored_cred = await db.get_credential_by_id(
|
stored_cred = db.get_credential_by_id(
|
||||||
credential.raw_id
|
credential.raw_id
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -338,7 +338,7 @@ async def websocket_remote_auth_permit(ws: WebSocket):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Update credential last_used
|
# Update credential last_used
|
||||||
await db.login(stored_cred.user_uuid, stored_cred)
|
db.login(stored_cred.user_uuid, stored_cred)
|
||||||
|
|
||||||
# Create a session for the REQUESTING device
|
# Create a session for the REQUESTING device
|
||||||
assert stored_cred.uuid is not None
|
assert stored_cred.uuid is not None
|
||||||
@@ -353,7 +353,7 @@ async def websocket_remote_auth_permit(ws: WebSocket):
|
|||||||
|
|
||||||
token_str = passphrase.generate()
|
token_str = passphrase.generate()
|
||||||
expiry = expires()
|
expiry = expires()
|
||||||
await db.create_reset_token(
|
db.create_reset_token(
|
||||||
user_uuid=stored_cred.user_uuid,
|
user_uuid=stored_cred.user_uuid,
|
||||||
key=tokens.reset_key(token_str),
|
key=tokens.reset_key(token_str),
|
||||||
expiry=expiry,
|
expiry=expiry,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ async def user_update_display_name(
|
|||||||
raise HTTPException(status_code=400, detail="display_name required")
|
raise HTTPException(status_code=400, detail="display_name required")
|
||||||
if len(new_name) > 64:
|
if len(new_name) > 64:
|
||||||
raise HTTPException(status_code=400, detail="display_name too long")
|
raise HTTPException(status_code=400, detail="display_name too long")
|
||||||
await db.update_user_display_name(s.user_uuid, new_name)
|
db.update_user_display_name(s.user_uuid, new_name)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ async def api_logout_all(request: Request, response: Response, auth=AUTH_COOKIE)
|
|||||||
raise authz.AuthException(
|
raise authz.AuthException(
|
||||||
status_code=401, detail="Session expired", mode="login"
|
status_code=401, detail="Session expired", mode="login"
|
||||||
)
|
)
|
||||||
await db.delete_sessions_for_user(s.user_uuid)
|
db.delete_sessions_for_user(s.user_uuid)
|
||||||
session.clear_session_cookie(response)
|
session.clear_session_cookie(response)
|
||||||
return {"message": "Logged out from all hosts"}
|
return {"message": "Logged out from all hosts"}
|
||||||
|
|
||||||
@@ -99,11 +99,11 @@ async def api_delete_session(
|
|||||||
status_code=400, detail="Invalid session identifier"
|
status_code=400, detail="Invalid session identifier"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
target_session = await db.get_session(target_key)
|
target_session = db.get_session(target_key)
|
||||||
if not target_session or target_session.user_uuid != current_session.user_uuid:
|
if not target_session or target_session.user_uuid != current_session.user_uuid:
|
||||||
raise HTTPException(status_code=404, detail="Session not found")
|
raise HTTPException(status_code=404, detail="Session not found")
|
||||||
|
|
||||||
await db.delete_session(target_key)
|
db.delete_session(target_key)
|
||||||
current_terminated = target_key == session_key(auth)
|
current_terminated = target_key == session_key(auth)
|
||||||
if current_terminated:
|
if current_terminated:
|
||||||
session.clear_session_cookie(response) # explicit because 200
|
session.clear_session_cookie(response) # explicit because 200
|
||||||
@@ -144,7 +144,7 @@ async def api_create_link(
|
|||||||
) from e
|
) from e
|
||||||
token = passphrase.generate()
|
token = passphrase.generate()
|
||||||
expiry = expires()
|
expiry = expires()
|
||||||
await db.create_reset_token(
|
db.create_reset_token(
|
||||||
user_uuid=s.user_uuid,
|
user_uuid=s.user_uuid,
|
||||||
key=tokens.reset_key(token),
|
key=tokens.reset_key(token),
|
||||||
expiry=expiry,
|
expiry=expiry,
|
||||||
|
|||||||
@@ -66,13 +66,13 @@ async def websocket_register_add(
|
|||||||
s = ctx.session
|
s = ctx.session
|
||||||
|
|
||||||
# Get user information and determine effective user_name for this registration
|
# Get user information and determine effective user_name for this registration
|
||||||
user = await db.get_user_by_uuid(user_uuid)
|
user = db.get_user_by_uuid(user_uuid)
|
||||||
user_name = user.display_name
|
user_name = user.display_name
|
||||||
if name is not None:
|
if name is not None:
|
||||||
stripped = name.strip()
|
stripped = name.strip()
|
||||||
if stripped:
|
if stripped:
|
||||||
user_name = stripped
|
user_name = stripped
|
||||||
challenge_ids = await db.get_credentials_by_user_uuid(user_uuid)
|
challenge_ids = db.get_credentials_by_user_uuid(user_uuid)
|
||||||
|
|
||||||
# WebAuthn registration
|
# WebAuthn registration
|
||||||
credential = await register_chat(ws, user_uuid, user_name, origin, challenge_ids)
|
credential = await register_chat(ws, user_uuid, user_name, origin, challenge_ids)
|
||||||
@@ -80,7 +80,7 @@ async def websocket_register_add(
|
|||||||
# Create a new session and store everything in database
|
# Create a new session and store everything in database
|
||||||
token = create_token()
|
token = create_token()
|
||||||
metadata = infodict(ws, "authenticated")
|
metadata = infodict(ws, "authenticated")
|
||||||
await db.create_credential_session( # type: ignore[attr-defined]
|
db.create_credential_session( # type: ignore[attr-defined]
|
||||||
user_uuid=user_uuid,
|
user_uuid=user_uuid,
|
||||||
credential=credential,
|
credential=credential,
|
||||||
reset_key=(s.key if reset is not None else None),
|
reset_key=(s.key if reset is not None else None),
|
||||||
@@ -116,7 +116,7 @@ async def websocket_authenticate(ws: WebSocket, auth=AUTH_COOKIE):
|
|||||||
try:
|
try:
|
||||||
session = await get_session(auth, host=host)
|
session = await get_session(auth, host=host)
|
||||||
session_user_uuid = session.user_uuid
|
session_user_uuid = session.user_uuid
|
||||||
credential_ids = await db.get_credentials_by_user_uuid(
|
credential_ids = db.get_credentials_by_user_uuid(
|
||||||
session_user_uuid
|
session_user_uuid
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -130,7 +130,7 @@ async def websocket_authenticate(ws: WebSocket, auth=AUTH_COOKIE):
|
|||||||
credential = passkey.instance.auth_parse(await ws.receive_json())
|
credential = passkey.instance.auth_parse(await ws.receive_json())
|
||||||
# Fetch from the database by credential ID
|
# Fetch from the database by credential ID
|
||||||
try:
|
try:
|
||||||
stored_cred = await db.get_credential_by_id(credential.raw_id)
|
stored_cred = db.get_credential_by_id(credential.raw_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"This passkey is no longer registered with {passkey.instance.rp_name}"
|
f"This passkey is no longer registered with {passkey.instance.rp_name}"
|
||||||
@@ -143,7 +143,7 @@ async def websocket_authenticate(ws: WebSocket, auth=AUTH_COOKIE):
|
|||||||
# Verify the credential matches the stored data
|
# Verify the credential matches the stored data
|
||||||
passkey.instance.auth_verify(credential, challenge, stored_cred, origin)
|
passkey.instance.auth_verify(credential, challenge, stored_cred, origin)
|
||||||
# Update both credential and user's last_seen timestamp
|
# Update both credential and user's last_seen timestamp
|
||||||
await db.login(stored_cred.user_uuid, stored_cred)
|
db.login(stored_cred.user_uuid, stored_cred)
|
||||||
|
|
||||||
# Create a session token for the authenticated user
|
# Create a session token for the authenticated user
|
||||||
assert stored_cred.uuid is not None
|
assert stored_cred.uuid is not None
|
||||||
|
|||||||
@@ -29,4 +29,4 @@ async def session_context(auth: str | None, host: str | None = None):
|
|||||||
if not auth:
|
if not auth:
|
||||||
return None
|
return None
|
||||||
normalized_host = normalize_host(host) if host else None
|
normalized_host = normalize_host(host) if host else None
|
||||||
return await db.get_session_context(session_key(auth), normalized_host)
|
return db.get_session_context(session_key(auth), normalized_host)
|
||||||
|
|||||||
@@ -41,17 +41,17 @@ async def format_user_info(
|
|||||||
- Sessions list
|
- Sessions list
|
||||||
- Permissions
|
- Permissions
|
||||||
"""
|
"""
|
||||||
u = await db.get_user_by_uuid(user_uuid)
|
u = db.get_user_by_uuid(user_uuid)
|
||||||
ctx = await permutil.session_context(auth, request_host)
|
ctx = await permutil.session_context(auth, request_host)
|
||||||
|
|
||||||
# Fetch and format credentials
|
# Fetch and format credentials
|
||||||
credential_ids = await db.get_credentials_by_user_uuid(user_uuid)
|
credential_ids = db.get_credentials_by_user_uuid(user_uuid)
|
||||||
credentials: list[dict] = []
|
credentials: list[dict] = []
|
||||||
user_aaguids: set[str] = set()
|
user_aaguids: set[str] = set()
|
||||||
|
|
||||||
for cred_id in credential_ids:
|
for cred_id in credential_ids:
|
||||||
try:
|
try:
|
||||||
c = await db.get_credential_by_id(cred_id)
|
c = db.get_credential_by_id(cred_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ async def format_user_info(
|
|||||||
|
|
||||||
# Format sessions
|
# Format sessions
|
||||||
normalized_request_host = hostutil.normalize_host(request_host)
|
normalized_request_host = hostutil.normalize_host(request_host)
|
||||||
session_records = await db.list_sessions_for_user(user_uuid)
|
session_records = db.list_sessions_for_user(user_uuid)
|
||||||
current_session_key = session_key(auth)
|
current_session_key = session_key(auth)
|
||||||
sessions_payload: list[dict] = []
|
sessions_payload: list[dict] = []
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ async def format_reset_user_info(user_uuid, reset_token) -> dict:
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary with minimal user info for password reset flow
|
Dictionary with minimal user info for password reset flow
|
||||||
"""
|
"""
|
||||||
u = await db.get_user_by_uuid(user_uuid)
|
u = db.get_user_by_uuid(user_uuid)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"authenticated": False,
|
"authenticated": False,
|
||||||
|
|||||||
+15
-14
@@ -21,6 +21,7 @@ import pytest
|
|||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
import uuid7
|
import uuid7
|
||||||
|
|
||||||
|
from paskia import globals as paskia_globals
|
||||||
from paskia.db import Credential, Org, Permission, Role, User
|
from paskia.db import Credential, Org, Permission, Role, User
|
||||||
from paskia.db.json import DB
|
from paskia.db.json import DB
|
||||||
from paskia.fastapi.session import AUTH_COOKIE_NAME
|
from paskia.fastapi.session import AUTH_COOKIE_NAME
|
||||||
@@ -46,7 +47,7 @@ async def test_db() -> AsyncGenerator[DB, None]:
|
|||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=True) as f:
|
with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=True) as f:
|
||||||
db = DB(f.name)
|
db = DB(f.name)
|
||||||
await db.init_db()
|
db.load() # Synchronous now
|
||||||
json_db._db = db
|
json_db._db = db
|
||||||
yield db
|
yield db
|
||||||
# Clean up
|
# Clean up
|
||||||
@@ -61,9 +62,9 @@ async def passkey_instance() -> Passkey:
|
|||||||
rp_name="Test RP",
|
rp_name="Test RP",
|
||||||
origins=["http://localhost:4401"],
|
origins=["http://localhost:4401"],
|
||||||
)
|
)
|
||||||
globals.passkey._instance = pk
|
paskia_globals.passkey._instance = pk
|
||||||
yield pk
|
yield pk
|
||||||
globals.passkey._instance = None
|
paskia_globals.passkey._instance = None
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture(scope="function")
|
@pytest_asyncio.fixture(scope="function")
|
||||||
@@ -74,7 +75,7 @@ async def test_org(test_db: DB, admin_permission: Permission) -> Org:
|
|||||||
display_name="Test Organization",
|
display_name="Test Organization",
|
||||||
permissions=["auth:admin"], # Org can grant this permission
|
permissions=["auth:admin"], # Org can grant this permission
|
||||||
)
|
)
|
||||||
await test_db.create_organization(org)
|
test_db.create_organization(org)
|
||||||
return org
|
return org
|
||||||
|
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ async def test_org(test_db: DB, admin_permission: Permission) -> Org:
|
|||||||
async def admin_permission(test_db: DB) -> Permission:
|
async def admin_permission(test_db: DB) -> Permission:
|
||||||
"""Create the auth:admin permission."""
|
"""Create the auth:admin permission."""
|
||||||
perm = Permission(id="auth:admin", display_name="Master Admin")
|
perm = Permission(id="auth:admin", display_name="Master Admin")
|
||||||
await test_db.create_permission(perm)
|
test_db.create_permission(perm)
|
||||||
return perm
|
return perm
|
||||||
|
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ async def test_role(test_db: DB, test_org: Org, admin_permission: Permission) ->
|
|||||||
display_name="Test Admin Role",
|
display_name="Test Admin Role",
|
||||||
permissions=["auth:admin", f"auth:org:{test_org.uuid}"],
|
permissions=["auth:admin", f"auth:org:{test_org.uuid}"],
|
||||||
)
|
)
|
||||||
await test_db.create_role(role)
|
test_db.create_role(role)
|
||||||
return role
|
return role
|
||||||
|
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ async def user_role(test_db: DB, test_org: Org) -> Role:
|
|||||||
display_name="User Role",
|
display_name="User Role",
|
||||||
permissions=[],
|
permissions=[],
|
||||||
)
|
)
|
||||||
await test_db.create_role(role)
|
test_db.create_role(role)
|
||||||
return role
|
return role
|
||||||
|
|
||||||
|
|
||||||
@@ -122,7 +123,7 @@ async def test_user(test_db: DB, test_role: Role) -> User:
|
|||||||
created_at=datetime.now(timezone.utc),
|
created_at=datetime.now(timezone.utc),
|
||||||
visits=0,
|
visits=0,
|
||||||
)
|
)
|
||||||
await test_db.create_user(user)
|
test_db.create_user(user)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ async def regular_user(test_db: DB, user_role: Role) -> User:
|
|||||||
created_at=datetime.now(timezone.utc),
|
created_at=datetime.now(timezone.utc),
|
||||||
visits=0,
|
visits=0,
|
||||||
)
|
)
|
||||||
await test_db.create_user(user)
|
test_db.create_user(user)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -154,7 +155,7 @@ async def test_credential(test_db: DB, test_user: User) -> Credential:
|
|||||||
last_used=None,
|
last_used=None,
|
||||||
last_verified=None,
|
last_verified=None,
|
||||||
)
|
)
|
||||||
await test_db.create_credential(credential)
|
test_db.create_credential(credential)
|
||||||
return credential
|
return credential
|
||||||
|
|
||||||
|
|
||||||
@@ -172,7 +173,7 @@ async def regular_credential(test_db: DB, regular_user: User) -> Credential:
|
|||||||
last_used=None,
|
last_used=None,
|
||||||
last_verified=None,
|
last_verified=None,
|
||||||
)
|
)
|
||||||
await test_db.create_credential(credential)
|
test_db.create_credential(credential)
|
||||||
return credential
|
return credential
|
||||||
|
|
||||||
|
|
||||||
@@ -182,7 +183,7 @@ async def session_token(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""Create a session for the admin user and return the token."""
|
"""Create a session for the admin user and return the token."""
|
||||||
token = create_token()
|
token = create_token()
|
||||||
await test_db.create_session(
|
test_db.create_session(
|
||||||
user_uuid=test_user.uuid,
|
user_uuid=test_user.uuid,
|
||||||
credential_uuid=test_credential.uuid,
|
credential_uuid=test_credential.uuid,
|
||||||
key=session_key(token),
|
key=session_key(token),
|
||||||
@@ -200,7 +201,7 @@ async def regular_session_token(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""Create a session for a regular user and return the token."""
|
"""Create a session for a regular user and return the token."""
|
||||||
token = create_token()
|
token = create_token()
|
||||||
await test_db.create_session(
|
test_db.create_session(
|
||||||
user_uuid=regular_user.uuid,
|
user_uuid=regular_user.uuid,
|
||||||
credential_uuid=regular_credential.uuid,
|
credential_uuid=regular_credential.uuid,
|
||||||
key=session_key(token),
|
key=session_key(token),
|
||||||
@@ -220,7 +221,7 @@ async def reset_token(test_db: DB, test_user: User, test_credential: Credential)
|
|||||||
from paskia.util.tokens import reset_key
|
from paskia.util.tokens import reset_key
|
||||||
|
|
||||||
token = generate()
|
token = generate()
|
||||||
await test_db.create_reset_token(
|
test_db.create_reset_token(
|
||||||
user_uuid=test_user.uuid,
|
user_uuid=test_user.uuid,
|
||||||
key=reset_key(token),
|
key=reset_key(token),
|
||||||
expiry=reset_expires(),
|
expiry=reset_expires(),
|
||||||
|
|||||||
+28
-26
@@ -35,7 +35,7 @@ async def second_org(test_db: DB) -> Org:
|
|||||||
display_name="Second Organization",
|
display_name="Second Organization",
|
||||||
permissions=[],
|
permissions=[],
|
||||||
)
|
)
|
||||||
await test_db.create_organization(org)
|
test_db.create_organization(org)
|
||||||
return org
|
return org
|
||||||
|
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ async def second_org_role(
|
|||||||
display_name="Second Org Admin Role",
|
display_name="Second Org Admin Role",
|
||||||
permissions=["auth:admin"],
|
permissions=["auth:admin"],
|
||||||
)
|
)
|
||||||
await test_db.create_role(role)
|
test_db.create_role(role)
|
||||||
return role
|
return role
|
||||||
|
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ async def second_org_user(test_db: DB, second_org_role: Role) -> User:
|
|||||||
created_at=datetime.now(timezone.utc),
|
created_at=datetime.now(timezone.utc),
|
||||||
visits=0,
|
visits=0,
|
||||||
)
|
)
|
||||||
await test_db.create_user(user)
|
test_db.create_user(user)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ async def second_org_credential(test_db: DB, second_org_user: User) -> Credentia
|
|||||||
last_used=datetime.now(timezone.utc),
|
last_used=datetime.now(timezone.utc),
|
||||||
last_verified=datetime.now(timezone.utc),
|
last_verified=datetime.now(timezone.utc),
|
||||||
)
|
)
|
||||||
await test_db.create_credential(credential)
|
test_db.create_credential(credential)
|
||||||
return credential
|
return credential
|
||||||
|
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ async def second_org_session_token(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""Create a session for the second org admin user."""
|
"""Create a session for the second org admin user."""
|
||||||
token = create_token()
|
token = create_token()
|
||||||
await test_db.create_session(
|
test_db.create_session(
|
||||||
user_uuid=second_org_user.uuid,
|
user_uuid=second_org_user.uuid,
|
||||||
credential_uuid=second_org_credential.uuid,
|
credential_uuid=second_org_credential.uuid,
|
||||||
key=session_key(token),
|
key=session_key(token),
|
||||||
@@ -115,7 +115,7 @@ async def org_admin_role(test_db: DB, test_org: Org) -> Role:
|
|||||||
display_name="Org Admin Role",
|
display_name="Org Admin Role",
|
||||||
permissions=[f"auth:org:{test_org.uuid}"],
|
permissions=[f"auth:org:{test_org.uuid}"],
|
||||||
)
|
)
|
||||||
await test_db.create_role(role)
|
test_db.create_role(role)
|
||||||
return role
|
return role
|
||||||
|
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ async def org_admin_user(test_db: DB, org_admin_role: Role) -> User:
|
|||||||
visits=5,
|
visits=5,
|
||||||
last_seen=datetime.now(timezone.utc),
|
last_seen=datetime.now(timezone.utc),
|
||||||
)
|
)
|
||||||
await test_db.create_user(user)
|
test_db.create_user(user)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ async def org_admin_credential(test_db: DB, org_admin_user: User) -> Credential:
|
|||||||
last_used=datetime.now(timezone.utc),
|
last_used=datetime.now(timezone.utc),
|
||||||
last_verified=None,
|
last_verified=None,
|
||||||
)
|
)
|
||||||
await test_db.create_credential(credential)
|
test_db.create_credential(credential)
|
||||||
return credential
|
return credential
|
||||||
|
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ async def org_admin_session_token(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""Create a session for the org admin user."""
|
"""Create a session for the org admin user."""
|
||||||
token = create_token()
|
token = create_token()
|
||||||
await test_db.create_session(
|
test_db.create_session(
|
||||||
user_uuid=org_admin_user.uuid,
|
user_uuid=org_admin_user.uuid,
|
||||||
credential_uuid=org_admin_credential.uuid,
|
credential_uuid=org_admin_credential.uuid,
|
||||||
key=session_key(token),
|
key=session_key(token),
|
||||||
@@ -176,9 +176,9 @@ async def org_admin_session_token(
|
|||||||
async def grantable_permission(test_db: DB, test_org: Org) -> Permission:
|
async def grantable_permission(test_db: DB, test_org: Org) -> Permission:
|
||||||
"""Create a permission and add it to org's grantable permissions."""
|
"""Create a permission and add it to org's grantable permissions."""
|
||||||
perm = Permission(id="test:grantable:perm", display_name="Grantable Perm")
|
perm = Permission(id="test:grantable:perm", display_name="Grantable Perm")
|
||||||
await test_db.create_permission(perm)
|
test_db.create_permission(perm)
|
||||||
# Add to org's grantable permissions
|
# Add to org's grantable permissions
|
||||||
await test_db.add_permission_to_organization(str(test_org.uuid), perm.id)
|
test_db.add_permission_to_organization(str(test_org.uuid), perm.id)
|
||||||
return perm
|
return perm
|
||||||
|
|
||||||
|
|
||||||
@@ -375,12 +375,12 @@ class TestAdminOrganizations:
|
|||||||
org_admin_perm_id = f"auth:org:{test_org.uuid}"
|
org_admin_perm_id = f"auth:org:{test_org.uuid}"
|
||||||
perm = Permission(id=org_admin_perm_id, display_name="Org Admin")
|
perm = Permission(id=org_admin_perm_id, display_name="Org Admin")
|
||||||
try:
|
try:
|
||||||
await test_db.create_permission(perm)
|
test_db.create_permission(perm)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # Permission may already exist
|
pass # Permission may already exist
|
||||||
|
|
||||||
# Add it to the org's permissions
|
# Add it to the org's permissions
|
||||||
await test_db.add_permission_to_organization(
|
test_db.add_permission_to_organization(
|
||||||
str(test_org.uuid), org_admin_perm_id
|
str(test_org.uuid), org_admin_perm_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -424,13 +424,13 @@ class TestAdminOrganizations:
|
|||||||
display_name="Org To Delete",
|
display_name="Org To Delete",
|
||||||
permissions=[],
|
permissions=[],
|
||||||
)
|
)
|
||||||
await test_db.create_organization(org_to_delete)
|
test_db.create_organization(org_to_delete)
|
||||||
|
|
||||||
# Create some org-specific permissions to test cleanup
|
# Create some org-specific permissions to test cleanup
|
||||||
org_perm = Permission(
|
org_perm = Permission(
|
||||||
id=f"test:org:{org_to_delete.uuid}:feature", display_name="Org Feature"
|
id=f"test:org:{org_to_delete.uuid}:feature", display_name="Org Feature"
|
||||||
)
|
)
|
||||||
await test_db.create_permission(org_perm)
|
test_db.create_permission(org_perm)
|
||||||
|
|
||||||
response = await client.delete(
|
response = await client.delete(
|
||||||
f"/auth/api/admin/orgs/{org_to_delete.uuid}",
|
f"/auth/api/admin/orgs/{org_to_delete.uuid}",
|
||||||
@@ -603,7 +603,7 @@ class TestAdminRoles:
|
|||||||
"""Creating role with non-grantable permission should fail."""
|
"""Creating role with non-grantable permission should fail."""
|
||||||
# Create permission but don't add to org
|
# Create permission but don't add to org
|
||||||
perm = Permission(id="test:not:grantable", display_name="Not Grantable")
|
perm = Permission(id="test:not:grantable", display_name="Not Grantable")
|
||||||
await test_db.create_permission(perm)
|
test_db.create_permission(perm)
|
||||||
|
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"/auth/api/admin/orgs/{test_org.uuid}/roles",
|
f"/auth/api/admin/orgs/{test_org.uuid}/roles",
|
||||||
@@ -673,7 +673,7 @@ class TestAdminRoles:
|
|||||||
):
|
):
|
||||||
"""Adding non-grantable permission to role should fail."""
|
"""Adding non-grantable permission to role should fail."""
|
||||||
perm = Permission(id="test:not:grantable:update", display_name="Not Grantable")
|
perm = Permission(id="test:not:grantable:update", display_name="Not Grantable")
|
||||||
await test_db.create_permission(perm)
|
test_db.create_permission(perm)
|
||||||
|
|
||||||
response = await client.put(
|
response = await client.put(
|
||||||
f"/auth/api/admin/orgs/{test_org.uuid}/roles/{user_role.uuid}",
|
f"/auth/api/admin/orgs/{test_org.uuid}/roles/{user_role.uuid}",
|
||||||
@@ -1087,7 +1087,7 @@ class TestAdminUsersInOrg:
|
|||||||
created_at=datetime.now(timezone.utc),
|
created_at=datetime.now(timezone.utc),
|
||||||
visits=0,
|
visits=0,
|
||||||
)
|
)
|
||||||
await test_db.create_user(user_no_cred)
|
test_db.create_user(user_no_cred)
|
||||||
|
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"/auth/api/admin/orgs/{test_org.uuid}/users/{user_no_cred.uuid}/create-link",
|
f"/auth/api/admin/orgs/{test_org.uuid}/users/{user_no_cred.uuid}/create-link",
|
||||||
@@ -1174,7 +1174,7 @@ class TestAdminSessions:
|
|||||||
# Create an additional session to delete
|
# Create an additional session to delete
|
||||||
extra_token = create_token()
|
extra_token = create_token()
|
||||||
extra_key = session_key(extra_token)
|
extra_key = session_key(extra_token)
|
||||||
await test_db.create_session(
|
test_db.create_session(
|
||||||
user_uuid=test_user.uuid,
|
user_uuid=test_user.uuid,
|
||||||
credential_uuid=test_credential.uuid,
|
credential_uuid=test_credential.uuid,
|
||||||
key=extra_key,
|
key=extra_key,
|
||||||
@@ -1301,7 +1301,7 @@ class TestAdminPermissions:
|
|||||||
test_org,
|
test_org,
|
||||||
grantable_permission,
|
grantable_permission,
|
||||||
):
|
):
|
||||||
"""Org admin should only see grantable permissions."""
|
"""Org admin should only see permissions their org can grant."""
|
||||||
response = await client.get(
|
response = await client.get(
|
||||||
"/auth/api/admin/permissions",
|
"/auth/api/admin/permissions",
|
||||||
headers={**auth_headers(org_admin_session_token), "Host": "localhost:4401"},
|
headers={**auth_headers(org_admin_session_token), "Host": "localhost:4401"},
|
||||||
@@ -1311,8 +1311,10 @@ class TestAdminPermissions:
|
|||||||
# Should only see permissions the org can grant
|
# Should only see permissions the org can grant
|
||||||
perm_ids = [p["id"] for p in data]
|
perm_ids = [p["id"] for p in data]
|
||||||
assert grantable_permission.id in perm_ids
|
assert grantable_permission.id in perm_ids
|
||||||
# Should NOT see auth:admin (not grantable by org)
|
# test_org CAN grant auth:admin (it's in org.permissions), so org admin sees it
|
||||||
assert "auth:admin" not in perm_ids
|
assert "auth:admin" in perm_ids
|
||||||
|
# Should also see auto-created org admin permission
|
||||||
|
assert f"auth:org:{test_org.uuid}" in perm_ids
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_permission(
|
async def test_create_permission(
|
||||||
@@ -1364,7 +1366,7 @@ class TestAdminPermissions:
|
|||||||
"""Admin should be able to update a permission."""
|
"""Admin should be able to update a permission."""
|
||||||
# Create permission first
|
# Create permission first
|
||||||
perm = Permission(id="test:updateable", display_name="Updateable")
|
perm = Permission(id="test:updateable", display_name="Updateable")
|
||||||
await test_db.create_permission(perm)
|
test_db.create_permission(perm)
|
||||||
|
|
||||||
response = await client.put(
|
response = await client.put(
|
||||||
"/auth/api/admin/permission?permission_id=test:updateable&display_name=Updated%20Name",
|
"/auth/api/admin/permission?permission_id=test:updateable&display_name=Updated%20Name",
|
||||||
@@ -1394,7 +1396,7 @@ class TestAdminPermissions:
|
|||||||
"""Admin should be able to rename a permission."""
|
"""Admin should be able to rename a permission."""
|
||||||
# Create permission first
|
# Create permission first
|
||||||
perm = Permission(id="test:renameable2", display_name="Renameable")
|
perm = Permission(id="test:renameable2", display_name="Renameable")
|
||||||
await test_db.create_permission(perm)
|
test_db.create_permission(perm)
|
||||||
|
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/auth/api/admin/permission/rename",
|
"/auth/api/admin/permission/rename",
|
||||||
@@ -1437,7 +1439,7 @@ class TestAdminPermissions:
|
|||||||
):
|
):
|
||||||
"""Renaming permission can also update display name."""
|
"""Renaming permission can also update display name."""
|
||||||
perm = Permission(id="test:rename:withname", display_name="Old Name")
|
perm = Permission(id="test:rename:withname", display_name="Old Name")
|
||||||
await test_db.create_permission(perm)
|
test_db.create_permission(perm)
|
||||||
|
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/auth/api/admin/permission/rename",
|
"/auth/api/admin/permission/rename",
|
||||||
@@ -1457,7 +1459,7 @@ class TestAdminPermissions:
|
|||||||
"""Admin should be able to delete a permission."""
|
"""Admin should be able to delete a permission."""
|
||||||
# Create permission first
|
# Create permission first
|
||||||
perm = Permission(id="test:deleteable", display_name="Deleteable")
|
perm = Permission(id="test:deleteable", display_name="Deleteable")
|
||||||
await test_db.create_permission(perm)
|
test_db.create_permission(perm)
|
||||||
|
|
||||||
response = await client.delete(
|
response = await client.delete(
|
||||||
"/auth/api/admin/permission?permission_id=test:deleteable",
|
"/auth/api/admin/permission?permission_id=test:deleteable",
|
||||||
|
|||||||
+2
-2
@@ -525,7 +525,7 @@ class TestValidateSessionRefresh:
|
|||||||
# Create a session with an old renewed time to trigger refresh
|
# Create a session with an old renewed time to trigger refresh
|
||||||
token = create_token()
|
token = create_token()
|
||||||
old_time = datetime.now(timezone.utc) - timedelta(minutes=10)
|
old_time = datetime.now(timezone.utc) - timedelta(minutes=10)
|
||||||
await test_db.create_session(
|
test_db.create_session(
|
||||||
user_uuid=test_user.uuid,
|
user_uuid=test_user.uuid,
|
||||||
credential_uuid=test_credential.uuid,
|
credential_uuid=test_credential.uuid,
|
||||||
key=session_key(token),
|
key=session_key(token),
|
||||||
@@ -536,7 +536,7 @@ class TestValidateSessionRefresh:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Delete the session right before validate tries to refresh
|
# Delete the session right before validate tries to refresh
|
||||||
await test_db.delete_session(session_key(token))
|
test_db.delete_session(session_key(token))
|
||||||
|
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/auth/api/validate",
|
"/auth/api/validate",
|
||||||
|
|||||||
Reference in New Issue
Block a user