Remove remaining DB getter functions, inline at call site and add ResetToken.by_passphrase().

This commit is contained in:
2026-02-10 22:48:31 +00:00
parent bad709a3ab
commit d41cffc03e
8 changed files with 52 additions and 96 deletions
+2 -1
View File
@@ -14,6 +14,7 @@ from uuid import UUID
from paskia import db
from paskia.config import RESET_LIFETIME, SESSION_LIFETIME
from paskia.db.structs import ResetToken
from paskia.util import hostutil
if TYPE_CHECKING:
@@ -33,7 +34,7 @@ def reset_expires() -> datetime:
def get_reset(token: str) -> "ResetToken":
"""Validate a credential reset token."""
record = db.get_reset_token(token)
record = ResetToken.by_passphrase(token)
if record:
return record
raise ValueError("This authentication link is no longer valid.")
+1 -1
View File
@@ -81,7 +81,7 @@ async def check_admin_credentials() -> bool:
# Check first admin user for credentials
admin_user = admin_users[0]
if not db.get_user_credential_ids(admin_user.uuid):
if not admin_user.credential_ids:
# Admin exists but has no credentials, create reset link
logger.info("⚠️ Admin user has no credentials!")
-6
View File
@@ -48,9 +48,6 @@ from paskia.db.operations import (
delete_sessions_for_user,
delete_user,
get_config,
get_reset_token,
get_user_credential_ids,
get_user_organization,
login,
remove_permission_from_org,
remove_permission_from_role,
@@ -111,9 +108,6 @@ __all__ = [
"build_user",
# Read ops
"get_config",
"get_reset_token",
"get_user_credential_ids",
"get_user_organization",
# Write ops
"add_permission_to_org",
"add_permission_to_role",
-57
View File
@@ -6,7 +6,6 @@ Context lookup: _db.session_ctx() returns full SessionContext with effective per
Write operations: Functions that validate and commit, or raise ValueError.
"""
import hashlib
import logging
from datetime import UTC, datetime, timedelta
from uuid import UUID
@@ -29,7 +28,6 @@ from paskia.db.structs import (
SessionContext,
User,
)
from paskia.util.passphrase import is_well_formed as _is_passphrase
_logger = logging.getLogger(__name__)
@@ -40,61 +38,6 @@ _db._store = _store
_initialized = False
# -------------------------------------------------------------------------
# Read/lookup functions
# -------------------------------------------------------------------------
def get_user_organization(user_uuid: UUID) -> tuple[Org, str]:
"""Get the organization a user belongs to and their role name.
Raises ValueError if user not found.
Call sites:
- admin_create_user_registration_link: org only
- admin_get_user_detail: org and role
- admin_update_user_display_name: org only
- admin_delete_user_credential: org only
- admin_delete_user_session: org only
- admin_update_user_role: org only
"""
if user_uuid not in _db.users:
raise ValueError(f"User {user_uuid} not found")
user = _db.users[user_uuid]
role = user.role
return role.org, role.display_name
def get_user_credential_ids(user_uuid: UUID) -> list[bytes]:
"""Get credential IDs for a user (for WebAuthn exclude lists).
Returns empty list if user has no credentials.
"""
assert user_uuid
return [c.credential_id for c in _db.users[user_uuid].credentials]
def _reset_key(passphrase: str) -> bytes:
"""Hash a passphrase to bytes for reset token storage."""
if not _is_passphrase(passphrase):
raise ValueError(
"Trying to reset with a session token in place of a passphrase"
if len(passphrase) == 16
else "Invalid passphrase format"
)
return hashlib.sha512(passphrase.encode()).digest()[:9]
def get_reset_token(passphrase: str) -> ResetToken | None:
"""Get reset token by passphrase.
Call sites:
- Get reset token to validate it (authsession.py:34)
"""
key = _reset_key(passphrase)
return _db.reset_tokens.get(key)
# -------------------------------------------------------------------------
# Write operations (validate, modify, commit or raise ValueError)
# -------------------------------------------------------------------------
+22 -4
View File
@@ -11,7 +11,12 @@ from msgspec import field
from paskia import db
from paskia.util.hostutil import normalize_host
from paskia.util.passphrase import generate as generate_passphrase
from paskia.util.passphrase import (
generate as generate_passphrase,
)
from paskia.util.passphrase import (
is_well_formed as _is_passphrase,
)
# Sentinel for uuid fields before they are set by create() or DB post init
_UUID_UNSET = UUID(int=0)
@@ -229,6 +234,11 @@ class User(msgspec.Struct, dict=True, omit_defaults=True):
"""Get all credentials for this user."""
return [c for c in db.data().credentials.values() if c.user_uuid == self.uuid]
@property
def credential_ids(self) -> list[bytes]:
"""Get credential IDs for this user (for WebAuthn exclude lists)."""
return [c.credential_id for c in self.credentials]
@property
def sessions(self) -> list[Session]:
"""Get all sessions for this user."""
@@ -452,9 +462,17 @@ class ResetToken(msgspec.Struct, dict=True):
"""Store this reset token in the database. Must be called inside a transaction."""
db.data().reset_tokens[self.key] = self
def delete(self) -> None:
"""Delete this reset token from the database. Must be called inside a transaction."""
del db.data().reset_tokens[self.key]
@classmethod
def by_passphrase(cls, passphrase: str) -> ResetToken | None:
"""Get a reset token by passphrase."""
if not _is_passphrase(passphrase):
raise ValueError(
"Trying to reset with a session token in place of a passphrase"
if len(passphrase) == 16
else "Invalid passphrase format"
)
key = hashlib.sha512(passphrase.encode()).digest()[:9]
return db.data().reset_tokens.get(key)
@classmethod
def create(
+25 -25
View File
@@ -462,8 +462,8 @@ async def admin_update_user_role(
auth=AUTH_COOKIE,
):
try:
user_org, _current_role = db.get_user_organization(user_uuid)
except ValueError:
user = db.data().users[user_uuid]
except KeyError:
raise HTTPException(status_code=404, detail="User not found")
ctx = await authz.verify(
auth,
@@ -471,7 +471,7 @@ async def admin_update_user_role(
match=permutil.has_any,
host=request.headers.get("host"),
)
if not can_manage_org(ctx, user_org.uuid):
if not can_manage_org(ctx, user.org.uuid):
raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden"
)
@@ -483,7 +483,7 @@ async def admin_update_user_role(
except (ValueError, TypeError):
raise ValueError("Invalid role UUID")
new_role = db.data().roles.get(new_role_uuid)
if not new_role or new_role.org_uuid != user_org.uuid:
if not new_role or new_role.org_uuid != user.org.uuid:
raise ValueError("Role not found in organization")
# Sanity check: prevent admin from removing their own access
@@ -511,8 +511,8 @@ async def admin_create_user_registration_link(
auth=AUTH_COOKIE,
):
try:
user_org, _role_name = db.get_user_organization(user_uuid)
except ValueError:
user = db.data().users[user_uuid]
except KeyError:
raise HTTPException(status_code=404, detail="User not found")
ctx = await authz.verify(
auth,
@@ -521,13 +521,13 @@ async def admin_create_user_registration_link(
host=request.headers.get("host"),
max_age="5m",
)
if not can_manage_org(ctx, user_org.uuid):
if not can_manage_org(ctx, user.org.uuid):
raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden"
)
# Check if user has existing credentials
has_credentials = db.get_user_credential_ids(user_uuid)
has_credentials = db.data().users[user_uuid].credential_ids
token_type = "user registration" if not has_credentials else "account recovery"
expiry = reset_expires()
@@ -552,8 +552,9 @@ async def admin_get_user_detail(
auth=AUTH_COOKIE,
):
try:
user_org, role_name = db.get_user_organization(user_uuid)
except ValueError:
user = db.data().users[user_uuid]
role_name = user.role.display_name
except KeyError:
raise HTTPException(status_code=404, detail="User not found")
ctx = await authz.verify(
auth,
@@ -561,17 +562,16 @@ async def admin_get_user_detail(
match=permutil.has_any,
host=request.headers.get("host"),
)
if not can_manage_org(ctx, user_org.uuid):
if not can_manage_org(ctx, user.org.uuid):
raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden"
)
user = db.data().users.get(user_uuid)
normalized_host = hostutil.normalize_host(request.headers.get("host"))
return MsgspecResponse(
{
"display_name": user.display_name,
"org": {"display_name": user_org.display_name},
"org": {"display_name": user.org.display_name},
"role": role_name,
"visits": user.visits,
"created_at": user.created_at,
@@ -609,8 +609,8 @@ async def admin_update_user_display_name(
auth=AUTH_COOKIE,
):
try:
user_org, _role_name = db.get_user_organization(user_uuid)
except ValueError:
user = db.data().users[user_uuid]
except KeyError:
raise HTTPException(status_code=404, detail="User not found")
ctx = await authz.verify(
auth,
@@ -618,7 +618,7 @@ async def admin_update_user_display_name(
match=permutil.has_any,
host=request.headers.get("host"),
)
if not can_manage_org(ctx, user_org.uuid):
if not can_manage_org(ctx, user.org.uuid):
raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden"
)
@@ -639,8 +639,8 @@ async def admin_delete_user(
):
"""Delete a user and all their credentials/sessions."""
try:
user_org, _role_name = db.get_user_organization(user_uuid)
except ValueError:
user = db.data().users[user_uuid]
except KeyError:
raise HTTPException(status_code=404, detail="User not found")
ctx = await authz.verify(
auth,
@@ -649,7 +649,7 @@ async def admin_delete_user(
host=request.headers.get("host"),
max_age="5m",
)
if not can_manage_org(ctx, user_org.uuid):
if not can_manage_org(ctx, user.org.uuid):
raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden"
)
@@ -668,8 +668,8 @@ async def admin_delete_user_credential(
auth=AUTH_COOKIE,
):
try:
user_org, _role_name = db.get_user_organization(user_uuid)
except ValueError:
user = db.data().users[user_uuid]
except KeyError:
raise HTTPException(status_code=404, detail="User not found")
ctx = await authz.verify(
auth,
@@ -678,7 +678,7 @@ async def admin_delete_user_credential(
host=request.headers.get("host"),
max_age="5m",
)
if not can_manage_org(ctx, user_org.uuid):
if not can_manage_org(ctx, user.org.uuid):
raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden"
)
@@ -694,8 +694,8 @@ async def admin_delete_user_session(
auth=AUTH_COOKIE,
):
try:
user_org, _role_name = db.get_user_organization(user_uuid)
except ValueError:
user = db.data().users[user_uuid]
except KeyError:
raise HTTPException(status_code=404, detail="User not found")
ctx = await authz.verify(
auth,
@@ -703,7 +703,7 @@ async def admin_delete_user_session(
match=permutil.has_any,
host=request.headers.get("host"),
)
if not can_manage_org(ctx, user_org.uuid):
if not can_manage_org(ctx, user.org.uuid):
raise authz.AuthException(
status_code=403, detail="Insufficient permissions", mode="forbidden"
)
+1 -1
View File
@@ -52,7 +52,7 @@ async def websocket_register_add(
stripped = name.strip()
if stripped:
user_name = stripped
credential_ids = db.get_user_credential_ids(user_uuid) or None
credential_ids = user.credential_ids or None
# WebAuthn registration
credential = await register_chat(ws, user_uuid, user_name, origin, credential_ids)
+1 -1
View File
@@ -92,7 +92,7 @@ async def authenticate_and_login(
if auth:
existing_ctx = db.data().session_ctx(auth, host)
if existing_ctx:
credential_ids = db.get_user_credential_ids(existing_ctx.user.uuid) or None
credential_ids = existing_ctx.user.credential_ids or None
cred, new_sign_count = await authenticate_chat(ws, credential_ids)