Large refactoring for better JSONL context. Switched back the urlsafe for session tokens that need to be passed in URLs. Other minor fixes.
This commit is contained in:
+7
-5
@@ -20,12 +20,13 @@ _logger = logging.getLogger(__name__)
|
||||
DB_PATH_DEFAULT = "paskia.jsonl"
|
||||
|
||||
|
||||
class _ChangeRecord(msgspec.Struct):
|
||||
class _ChangeRecord(msgspec.Struct, omit_defaults=True):
|
||||
"""A single change record in the JSONL file."""
|
||||
|
||||
ts: datetime
|
||||
actor: str
|
||||
diff: dict
|
||||
a: str # action - describes the operation (e.g., "migrate", "login", "create_user")
|
||||
u: str | None = None # user UUID who performed the action (None for system)
|
||||
diff: dict = {}
|
||||
|
||||
|
||||
# msgspec encoder for change records
|
||||
@@ -83,11 +84,12 @@ def compute_diff(previous: dict, current: dict) -> dict | None:
|
||||
return diff if diff else None
|
||||
|
||||
|
||||
def create_change_record(actor: str, diff: dict) -> _ChangeRecord:
|
||||
def create_change_record(action: str, diff: dict, user: str | None = None) -> _ChangeRecord:
|
||||
"""Create a change record for persistence."""
|
||||
return _ChangeRecord(
|
||||
ts=datetime.now(timezone.utc),
|
||||
actor=actor,
|
||||
a=action,
|
||||
u=user,
|
||||
diff=diff,
|
||||
)
|
||||
|
||||
|
||||
+206
-98
@@ -73,7 +73,8 @@ class DB:
|
||||
)
|
||||
self._previous_builtins: dict[str, Any] = {}
|
||||
self._pending_changes: deque[_ChangeRecord] = deque()
|
||||
self._current_actor: str = "system"
|
||||
self._current_action: str = "system"
|
||||
self._current_user: str | None = None
|
||||
|
||||
async def load(self, db_path: str | None = None) -> None:
|
||||
"""Load data from JSONL change log.
|
||||
@@ -98,31 +99,58 @@ class DB:
|
||||
diff = compute_diff(self._previous_builtins, current)
|
||||
if diff:
|
||||
self._pending_changes.append(
|
||||
create_change_record(self._current_actor, diff)
|
||||
create_change_record(self._current_action, diff, self._current_user)
|
||||
)
|
||||
self._previous_builtins = current
|
||||
# Log the change with user display name if available
|
||||
actor_display = self._current_actor
|
||||
if self._current_actor not in ("system", "expiry", "migrate"):
|
||||
user_display = None
|
||||
if self._current_user:
|
||||
try:
|
||||
user_uuid = UUID(self._current_actor)
|
||||
user_uuid = UUID(self._current_user)
|
||||
if user_uuid in self._data.users:
|
||||
actor_display = self._data.users[user_uuid].display_name
|
||||
user_display = self._data.users[user_uuid].display_name
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
user_display = self._current_user
|
||||
import json
|
||||
_logger.info("DB change by %s: %s", actor_display, json.dumps(diff, default=str))
|
||||
|
||||
if user_display:
|
||||
_logger.info(
|
||||
"DB %s by %s: %s",
|
||||
self._current_action,
|
||||
user_display,
|
||||
json.dumps(diff, default=str),
|
||||
)
|
||||
else:
|
||||
_logger.info(
|
||||
"DB %s: %s", self._current_action, json.dumps(diff, default=str)
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
def transaction(self, actor: str = "system"):
|
||||
"""Wrap writes in transaction. Queues change on successful exit."""
|
||||
old_actor = self._current_actor
|
||||
self._current_actor = actor
|
||||
def transaction(
|
||||
self,
|
||||
action: str,
|
||||
ctx: SessionContext | None = None,
|
||||
*,
|
||||
user: str | None = None,
|
||||
):
|
||||
"""Wrap writes in transaction. Queues change on successful exit.
|
||||
|
||||
Args:
|
||||
action: Describes the operation (e.g., "Created user", "Login")
|
||||
ctx: Session context of user performing the action (None for system operations)
|
||||
user: User UUID string (alternative to ctx when full context unavailable)
|
||||
"""
|
||||
old_action = self._current_action
|
||||
old_user = self._current_user
|
||||
self._current_action = action
|
||||
# Prefer ctx.user.uuid if ctx provided, otherwise use user param
|
||||
self._current_user = str(ctx.user.uuid) if ctx else user
|
||||
try:
|
||||
yield
|
||||
self._queue_change()
|
||||
finally:
|
||||
self._current_actor = old_actor
|
||||
self._current_action = old_action
|
||||
self._current_user = old_user
|
||||
|
||||
|
||||
# Global instance, always available (empty until init() loads data)
|
||||
@@ -176,7 +204,9 @@ def build_role(uuid: UUID) -> Role:
|
||||
|
||||
def build_org(uuid: UUID, include_roles: bool = False) -> Org:
|
||||
o = _db._data.orgs[uuid]
|
||||
perm_uuids = [str(pid) for pid, p in _db._data.permissions.items() if uuid in p.orgs]
|
||||
perm_uuids = [
|
||||
str(pid) for pid, p in _db._data.permissions.items() if uuid in p.orgs
|
||||
]
|
||||
org = Org(uuid=uuid, display_name=o.display_name, permissions=perm_uuids)
|
||||
if include_roles:
|
||||
org.roles = [
|
||||
@@ -495,11 +525,13 @@ def get_session_context(
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
def create_permission(perm: Permission, actor: str = "system") -> None:
|
||||
def create_permission(
|
||||
perm: Permission, *, ctx: SessionContext | None = None
|
||||
) -> None:
|
||||
"""Create a new permission."""
|
||||
if perm.uuid in _db._data.permissions:
|
||||
raise ValueError(f"Permission {perm.uuid} already exists")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Created permission", ctx):
|
||||
_db._data.permissions[perm.uuid] = _PermissionData(
|
||||
scope=perm.scope,
|
||||
display_name=perm.display_name,
|
||||
@@ -508,11 +540,13 @@ def create_permission(perm: Permission, actor: str = "system") -> None:
|
||||
)
|
||||
|
||||
|
||||
def update_permission(perm: Permission, actor: str = "system") -> None:
|
||||
def update_permission(
|
||||
perm: Permission, *, ctx: SessionContext | None = None
|
||||
) -> None:
|
||||
"""Update a permission's scope, display_name, and domain."""
|
||||
if perm.uuid not in _db._data.permissions:
|
||||
raise ValueError(f"Permission {perm.uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Updated permission", ctx):
|
||||
_db._data.permissions[perm.uuid].scope = perm.scope
|
||||
_db._data.permissions[perm.uuid].display_name = perm.display_name
|
||||
_db._data.permissions[perm.uuid].domain = perm.domain
|
||||
@@ -523,7 +557,8 @@ def rename_permission(
|
||||
new_scope: str,
|
||||
display_name: str,
|
||||
domain: str | None = None,
|
||||
actor: str = "system",
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Rename a permission's scope. The UUID remains the same.
|
||||
|
||||
@@ -539,40 +574,46 @@ def rename_permission(
|
||||
if not key:
|
||||
raise ValueError(f"Permission with scope '{old_scope}' not found")
|
||||
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Renamed permission", ctx):
|
||||
# Update the permission
|
||||
_db._data.permissions[key].scope = new_scope
|
||||
_db._data.permissions[key].display_name = display_name
|
||||
_db._data.permissions[key].domain = domain
|
||||
|
||||
|
||||
def delete_permission(uuid: str | UUID, actor: str = "system") -> None:
|
||||
def delete_permission(
|
||||
uuid: str | UUID, *, ctx: SessionContext | None = None
|
||||
) -> None:
|
||||
"""Delete a permission and remove it from all roles."""
|
||||
if isinstance(uuid, str):
|
||||
uuid = UUID(uuid)
|
||||
if uuid not in _db._data.permissions:
|
||||
raise ValueError(f"Permission {uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Deleted permission", ctx):
|
||||
# Remove this permission from all roles
|
||||
for role in _db._data.roles.values():
|
||||
role.permissions.pop(uuid, None)
|
||||
del _db._data.permissions[uuid]
|
||||
|
||||
|
||||
def create_organization(org: Org, actor: str = "system") -> None:
|
||||
def create_organization(
|
||||
org: Org, *, ctx: SessionContext | None = None
|
||||
) -> None:
|
||||
"""Create a new organization with an Administration role.
|
||||
|
||||
Automatically creates an 'Administration' role with auth:org:admin permission.
|
||||
"""
|
||||
if org.uuid in _db._data.orgs:
|
||||
raise ValueError(f"Organization {org.uuid} already exists")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Created organization", ctx):
|
||||
_db._data.orgs[org.uuid] = _OrgData(
|
||||
display_name=org.display_name, created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
# Grant listed permissions to this org (org.permissions contains UUIDs now)
|
||||
for perm_uuid_str in org.permissions:
|
||||
perm_uuid = UUID(perm_uuid_str) if isinstance(perm_uuid_str, str) else perm_uuid_str
|
||||
perm_uuid = (
|
||||
UUID(perm_uuid_str) if isinstance(perm_uuid_str, str) else perm_uuid_str
|
||||
)
|
||||
if perm_uuid in _db._data.permissions:
|
||||
_db._data.permissions[perm_uuid].orgs[org.uuid] = True
|
||||
# Create Administration role with org admin permission
|
||||
@@ -594,24 +635,29 @@ def create_organization(org: Org, actor: str = "system") -> None:
|
||||
|
||||
|
||||
def update_organization_name(
|
||||
uuid: str | UUID, display_name: str, actor: str = "system"
|
||||
uuid: str | UUID,
|
||||
display_name: str,
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Update organization display name."""
|
||||
if isinstance(uuid, str):
|
||||
uuid = UUID(uuid)
|
||||
if uuid not in _db._data.orgs:
|
||||
raise ValueError(f"Organization {uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Renamed organization", ctx):
|
||||
_db._data.orgs[uuid].display_name = display_name
|
||||
|
||||
|
||||
def delete_organization(uuid: str | UUID, actor: str = "system") -> None:
|
||||
def delete_organization(
|
||||
uuid: str | UUID, *, ctx: SessionContext | None = None
|
||||
) -> None:
|
||||
"""Delete organization and all its roles/users."""
|
||||
if isinstance(uuid, str):
|
||||
uuid = UUID(uuid)
|
||||
if uuid not in _db._data.orgs:
|
||||
raise ValueError(f"Organization {uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Deleted organization", ctx):
|
||||
# Remove org from all permissions
|
||||
for p in _db._data.permissions.values():
|
||||
p.orgs.pop(uuid, None)
|
||||
@@ -627,7 +673,10 @@ def delete_organization(uuid: str | UUID, actor: str = "system") -> None:
|
||||
|
||||
|
||||
def add_permission_to_organization(
|
||||
org_uuid: str | UUID, permission_id: str | UUID, actor: str = "system"
|
||||
org_uuid: str | UUID,
|
||||
permission_id: str | UUID,
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Grant a permission to an organization by UUID."""
|
||||
if isinstance(org_uuid, str):
|
||||
@@ -653,12 +702,15 @@ def add_permission_to_organization(
|
||||
if permission_uuid not in _db._data.permissions:
|
||||
raise ValueError(f"Permission {permission_uuid} not found")
|
||||
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Granted org permission", ctx):
|
||||
_db._data.permissions[permission_uuid].orgs[org_uuid] = True
|
||||
|
||||
|
||||
def remove_permission_from_organization(
|
||||
org_uuid: str | UUID, permission_id: str | UUID, actor: str = "system"
|
||||
org_uuid: str | UUID,
|
||||
permission_id: str | UUID,
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Remove a permission from an organization by UUID."""
|
||||
if isinstance(org_uuid, str):
|
||||
@@ -684,17 +736,19 @@ def remove_permission_from_organization(
|
||||
if permission_uuid not in _db._data.permissions:
|
||||
return # Permission not found, silently return
|
||||
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Revoked org permission", ctx):
|
||||
_db._data.permissions[permission_uuid].orgs.pop(org_uuid, None)
|
||||
|
||||
|
||||
def create_role(role: Role, actor: str = "system") -> None:
|
||||
def create_role(
|
||||
role: Role, *, ctx: SessionContext | None = None
|
||||
) -> None:
|
||||
"""Create a new role."""
|
||||
if role.uuid in _db._data.roles:
|
||||
raise ValueError(f"Role {role.uuid} already exists")
|
||||
if role.org_uuid not in _db._data.orgs:
|
||||
raise ValueError(f"Organization {role.org_uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Created role", ctx):
|
||||
_db._data.roles[role.uuid] = _RoleData(
|
||||
org=role.org_uuid,
|
||||
display_name=role.display_name,
|
||||
@@ -703,19 +757,25 @@ def create_role(role: Role, actor: str = "system") -> None:
|
||||
|
||||
|
||||
def update_role_name(
|
||||
uuid: str | UUID, display_name: str, actor: str = "system"
|
||||
uuid: str | UUID,
|
||||
display_name: str,
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Update role display name."""
|
||||
if isinstance(uuid, str):
|
||||
uuid = UUID(uuid)
|
||||
if uuid not in _db._data.roles:
|
||||
raise ValueError(f"Role {uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Renamed role", ctx):
|
||||
_db._data.roles[uuid].display_name = display_name
|
||||
|
||||
|
||||
def add_permission_to_role(
|
||||
role_uuid: str | UUID, permission_uuid: str | UUID, actor: str = "system"
|
||||
role_uuid: str | UUID,
|
||||
permission_uuid: str | UUID,
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Add permission to role by UUID."""
|
||||
if isinstance(role_uuid, str):
|
||||
@@ -726,12 +786,15 @@ def add_permission_to_role(
|
||||
raise ValueError(f"Role {role_uuid} not found")
|
||||
if permission_uuid not in _db._data.permissions:
|
||||
raise ValueError(f"Permission {permission_uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Granted role permission", ctx):
|
||||
_db._data.roles[role_uuid].permissions[permission_uuid] = True
|
||||
|
||||
|
||||
def remove_permission_from_role(
|
||||
role_uuid: str | UUID, permission_uuid: str | UUID, actor: str = "system"
|
||||
role_uuid: str | UUID,
|
||||
permission_uuid: str | UUID,
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Remove permission from role by UUID."""
|
||||
if isinstance(role_uuid, str):
|
||||
@@ -740,11 +803,13 @@ def remove_permission_from_role(
|
||||
permission_uuid = UUID(permission_uuid)
|
||||
if role_uuid not in _db._data.roles:
|
||||
raise ValueError(f"Role {role_uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Revoked role permission", ctx):
|
||||
_db._data.roles[role_uuid].permissions.pop(permission_uuid, None)
|
||||
|
||||
|
||||
def delete_role(uuid: str | UUID, actor: str = "system") -> None:
|
||||
def delete_role(
|
||||
uuid: str | UUID, *, ctx: SessionContext | None = None
|
||||
) -> None:
|
||||
"""Delete a role."""
|
||||
if isinstance(uuid, str):
|
||||
uuid = UUID(uuid)
|
||||
@@ -753,40 +818,54 @@ def delete_role(uuid: str | UUID, actor: str = "system") -> None:
|
||||
# Check no users have this role
|
||||
if any(u.role == uuid for u in _db._data.users.values()):
|
||||
raise ValueError(f"Cannot delete role {uuid}: users still assigned")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Deleted role", ctx):
|
||||
del _db._data.roles[uuid]
|
||||
|
||||
|
||||
def create_user(user: User, actor: str = "system") -> None:
|
||||
def create_user(
|
||||
new_user: User, *, ctx: SessionContext | None = None
|
||||
) -> None:
|
||||
"""Create a new user."""
|
||||
if user.uuid in _db._data.users:
|
||||
raise ValueError(f"User {user.uuid} already exists")
|
||||
if user.role_uuid not in _db._data.roles:
|
||||
raise ValueError(f"Role {user.role_uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
_db._data.users[user.uuid] = _UserData(
|
||||
display_name=user.display_name,
|
||||
role=user.role_uuid,
|
||||
created_at=user.created_at or datetime.now(timezone.utc),
|
||||
last_seen=user.last_seen,
|
||||
visits=user.visits,
|
||||
if new_user.uuid in _db._data.users:
|
||||
raise ValueError(f"User {new_user.uuid} already exists")
|
||||
if new_user.role_uuid not in _db._data.roles:
|
||||
raise ValueError(f"Role {new_user.role_uuid} not found")
|
||||
with _db.transaction("Created user", ctx):
|
||||
_db._data.users[new_user.uuid] = _UserData(
|
||||
display_name=new_user.display_name,
|
||||
role=new_user.role_uuid,
|
||||
created_at=new_user.created_at or datetime.now(timezone.utc),
|
||||
last_seen=new_user.last_seen,
|
||||
visits=new_user.visits,
|
||||
)
|
||||
|
||||
|
||||
def update_user_display_name(
|
||||
uuid: str | UUID, display_name: str, actor: str = "system"
|
||||
uuid: str | UUID,
|
||||
display_name: str,
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Update user display name."""
|
||||
"""Update user display name.
|
||||
|
||||
For self-service (user updating own name), ctx can be None and user is derived from uuid.
|
||||
For admin operations, ctx should be provided.
|
||||
"""
|
||||
if isinstance(uuid, str):
|
||||
uuid = UUID(uuid)
|
||||
if uuid not in _db._data.users:
|
||||
raise ValueError(f"User {uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
# For self-service, derive user from the uuid being modified
|
||||
user_str = str(uuid) if not ctx else None
|
||||
with _db.transaction("Renamed user", ctx, user=user_str):
|
||||
_db._data.users[uuid].display_name = display_name
|
||||
|
||||
|
||||
def update_user_role(
|
||||
uuid: str | UUID, role_uuid: str | UUID, actor: str = "system"
|
||||
uuid: str | UUID,
|
||||
role_uuid: str | UUID,
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Update user's role."""
|
||||
if isinstance(uuid, str):
|
||||
@@ -797,12 +876,15 @@ def update_user_role(
|
||||
raise ValueError(f"User {uuid} not found")
|
||||
if role_uuid not in _db._data.roles:
|
||||
raise ValueError(f"Role {role_uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Changed user role", ctx):
|
||||
_db._data.users[uuid].role = role_uuid
|
||||
|
||||
|
||||
def update_user_role_in_organization(
|
||||
user_uuid: str | UUID, role_name: str, actor: str = "system"
|
||||
user_uuid: str | UUID,
|
||||
role_name: str,
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Update user's role by role name within their current organization."""
|
||||
if isinstance(user_uuid, str):
|
||||
@@ -821,17 +903,19 @@ def update_user_role_in_organization(
|
||||
break
|
||||
if new_role_uuid is None:
|
||||
raise ValueError(f"Role '{role_name}' not found in organization")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Changed user role", ctx):
|
||||
_db._data.users[user_uuid].role = new_role_uuid
|
||||
|
||||
|
||||
def delete_user(uuid: str | UUID, actor: str = "system") -> None:
|
||||
def delete_user(
|
||||
uuid: str | UUID, *, ctx: SessionContext | None = None
|
||||
) -> None:
|
||||
"""Delete user and their credentials/sessions."""
|
||||
if isinstance(uuid, str):
|
||||
uuid = UUID(uuid)
|
||||
if uuid not in _db._data.users:
|
||||
raise ValueError(f"User {uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Deleted user", ctx):
|
||||
# Delete credentials
|
||||
cred_uuids = [cid for cid, c in _db._data.credentials.items() if c.user == uuid]
|
||||
for cid in cred_uuids:
|
||||
@@ -847,13 +931,15 @@ def delete_user(uuid: str | UUID, actor: str = "system") -> None:
|
||||
del _db._data.users[uuid]
|
||||
|
||||
|
||||
def create_credential(cred: Credential, actor: str = "system") -> None:
|
||||
def create_credential(
|
||||
cred: Credential, *, ctx: SessionContext | None = None
|
||||
) -> None:
|
||||
"""Create a new credential."""
|
||||
if cred.uuid in _db._data.credentials:
|
||||
raise ValueError(f"Credential {cred.uuid} already exists")
|
||||
if cred.user_uuid not in _db._data.users:
|
||||
raise ValueError(f"User {cred.user_uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Added credential", ctx):
|
||||
_db._data.credentials[cred.uuid] = _CredentialData(
|
||||
credential_id=cred.credential_id,
|
||||
user=cred.user_uuid,
|
||||
@@ -870,21 +956,25 @@ def update_credential_sign_count(
|
||||
uuid: str | UUID,
|
||||
sign_count: int,
|
||||
last_used: datetime | None = None,
|
||||
actor: str = "system",
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Update credential sign count and last_used."""
|
||||
if isinstance(uuid, str):
|
||||
uuid = UUID(uuid)
|
||||
if uuid not in _db._data.credentials:
|
||||
raise ValueError(f"Credential {uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Updated credential", ctx):
|
||||
_db._data.credentials[uuid].sign_count = sign_count
|
||||
if last_used:
|
||||
_db._data.credentials[uuid].last_used = last_used
|
||||
|
||||
|
||||
def delete_credential(
|
||||
uuid: str | UUID, user_uuid: str | UUID | None = None, actor: str = "system"
|
||||
uuid: str | UUID,
|
||||
user_uuid: str | UUID | None = None,
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Delete a credential.
|
||||
|
||||
@@ -900,7 +990,7 @@ def delete_credential(
|
||||
cred_user = _db._data.credentials[uuid].user
|
||||
if cred_user != user_uuid:
|
||||
raise ValueError(f"Credential {uuid} does not belong to user {user_uuid}")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Deleted credential", ctx):
|
||||
del _db._data.credentials[uuid]
|
||||
|
||||
|
||||
@@ -912,7 +1002,8 @@ def create_session(
|
||||
ip: str | None,
|
||||
user_agent: str | None,
|
||||
expiry: datetime,
|
||||
actor: str = "system",
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Create a new session."""
|
||||
if key in _db._data.sessions:
|
||||
@@ -921,7 +1012,7 @@ def create_session(
|
||||
raise ValueError(f"User {user_uuid} not found")
|
||||
if credential_uuid not in _db._data.credentials:
|
||||
raise ValueError(f"Credential {credential_uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Created session", ctx):
|
||||
_db._data.sessions[key] = _SessionData(
|
||||
user=user_uuid,
|
||||
credential=credential_uuid,
|
||||
@@ -938,12 +1029,13 @@ def update_session(
|
||||
ip: str | None = None,
|
||||
user_agent: str | None = None,
|
||||
expiry: datetime | None = None,
|
||||
actor: str = "system",
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Update session metadata."""
|
||||
if key not in _db._data.sessions:
|
||||
raise ValueError("Session not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Updated session", ctx):
|
||||
s = _db._data.sessions[key]
|
||||
if host is not None:
|
||||
s.host = host
|
||||
@@ -955,24 +1047,36 @@ def update_session(
|
||||
s.expiry = expiry
|
||||
|
||||
|
||||
def set_session_host(key: str, host: str, actor: str = "system") -> None:
|
||||
def set_session_host(key: str, host: str, *, ctx: SessionContext | None = None) -> None:
|
||||
"""Set the host for a session (first-time binding)."""
|
||||
update_session(key, host=host, actor=actor)
|
||||
update_session(key, host=host, ctx=ctx)
|
||||
|
||||
|
||||
def delete_session(key: str, actor: str = "system") -> None:
|
||||
"""Delete a session."""
|
||||
def delete_session(key: str, *, ctx: SessionContext | None = None) -> None:
|
||||
"""Delete a session.
|
||||
|
||||
For logout (user deleting own session), ctx can be None and user is derived from session.
|
||||
For admin operations, ctx should be provided.
|
||||
"""
|
||||
if key not in _db._data.sessions:
|
||||
raise ValueError("Session not found")
|
||||
with _db.transaction(actor):
|
||||
# For self-service logout, derive user from the session being deleted
|
||||
user_str = str(_db._data.sessions[key].user) if not ctx else None
|
||||
with _db.transaction("Deleted session", ctx, user=user_str):
|
||||
del _db._data.sessions[key]
|
||||
|
||||
|
||||
def delete_sessions_for_user(user_uuid: str | UUID, actor: str = "system") -> None:
|
||||
"""Delete all sessions for a user."""
|
||||
def delete_sessions_for_user(user_uuid: str | UUID, *, ctx: SessionContext | None = None) -> None:
|
||||
"""Delete all sessions for a user.
|
||||
|
||||
For logout-all (user deleting own sessions), ctx can be None and user is derived from user_uuid.
|
||||
For admin operations, ctx should be provided.
|
||||
"""
|
||||
if isinstance(user_uuid, str):
|
||||
user_uuid = UUID(user_uuid)
|
||||
with _db.transaction(actor):
|
||||
# For self-service, derive user from the user_uuid param
|
||||
user_str = str(user_uuid) if not ctx else None
|
||||
with _db.transaction("Deleted user sessions", ctx, user=user_str):
|
||||
keys = [k for k, s in _db._data.sessions.items() if s.user == user_uuid]
|
||||
for k in keys:
|
||||
del _db._data.sessions[k]
|
||||
@@ -983,25 +1087,32 @@ def create_reset_token(
|
||||
user_uuid: UUID,
|
||||
expiry: datetime,
|
||||
token_type: str,
|
||||
actor: str = "system",
|
||||
*,
|
||||
ctx: SessionContext | None = None,
|
||||
) -> None:
|
||||
"""Create a reset token from a passphrase."""
|
||||
"""Create a reset token from a passphrase.
|
||||
|
||||
For self-service (user creating own recovery link), ctx can be None and user is derived from user_uuid.
|
||||
For admin operations, ctx should be provided.
|
||||
"""
|
||||
key = _reset_key(passphrase)
|
||||
if key in _db._data.reset_tokens:
|
||||
raise ValueError("Reset token already exists")
|
||||
if user_uuid not in _db._data.users:
|
||||
raise ValueError(f"User {user_uuid} not found")
|
||||
with _db.transaction(actor):
|
||||
# For self-service, derive user from the user_uuid param
|
||||
user_str = str(user_uuid) if not ctx else None
|
||||
with _db.transaction("Created reset token", ctx, user=user_str):
|
||||
_db._data.reset_tokens[key] = _ResetTokenData(
|
||||
user=user_uuid, expiry=expiry, token_type=token_type
|
||||
)
|
||||
|
||||
|
||||
def delete_reset_token(key: bytes, actor: str = "system") -> None:
|
||||
def delete_reset_token(key: bytes, *, ctx: SessionContext | None = None) -> None:
|
||||
"""Delete a reset token."""
|
||||
if key not in _db._data.reset_tokens:
|
||||
raise ValueError("Reset token not found")
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Deleted reset token", ctx):
|
||||
del _db._data.reset_tokens[key]
|
||||
|
||||
|
||||
@@ -1010,11 +1121,11 @@ def delete_reset_token(key: bytes, actor: str = "system") -> None:
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
def cleanup_expired(actor: str = "system") -> int:
|
||||
def cleanup_expired() -> int:
|
||||
"""Remove expired sessions and reset tokens. Returns count removed."""
|
||||
now = datetime.now(timezone.utc)
|
||||
count = 0
|
||||
with _db.transaction(actor):
|
||||
with _db.transaction("Cleaned up expired"):
|
||||
expired_sessions = [k for k, s in _db._data.sessions.items() if s.expiry < now]
|
||||
for k in expired_sessions:
|
||||
del _db._data.sessions[k]
|
||||
@@ -1034,10 +1145,8 @@ def cleanup_expired(actor: str = "system") -> int:
|
||||
|
||||
|
||||
def _create_token() -> str:
|
||||
"""Generate a 16-character session token using standard base64."""
|
||||
import base64
|
||||
|
||||
return base64.b64encode(secrets.token_bytes(12)).decode()
|
||||
"""Generate a 16-character URL-safe session token."""
|
||||
return secrets.token_urlsafe(12)
|
||||
|
||||
|
||||
def login(
|
||||
@@ -1056,7 +1165,6 @@ def login(
|
||||
Creates:
|
||||
- new session
|
||||
|
||||
Actor is set to the user UUID being logged in.
|
||||
Returns the generated session token.
|
||||
"""
|
||||
if isinstance(user_uuid, str):
|
||||
@@ -1068,8 +1176,8 @@ def login(
|
||||
raise ValueError(f"Credential {credential.uuid} not found")
|
||||
|
||||
session_key = _create_token()
|
||||
actor = str(user_uuid)
|
||||
with _db.transaction(actor):
|
||||
user_str = str(user_uuid)
|
||||
with _db.transaction("User logged in", user=user_str):
|
||||
# Update user
|
||||
_db._data.users[user_uuid].last_seen = now
|
||||
_db._data.users[user_uuid].visits += 1
|
||||
@@ -1096,7 +1204,6 @@ def create_credential_session(
|
||||
user_agent: str | None,
|
||||
display_name: str | None = None,
|
||||
reset_key: bytes | None = None,
|
||||
actor: str = "system",
|
||||
) -> str:
|
||||
"""Create a credential and session together, optionally consuming a reset token.
|
||||
|
||||
@@ -1117,7 +1224,8 @@ def create_credential_session(
|
||||
if user_uuid not in _db._data.users:
|
||||
raise ValueError(f"User {user_uuid} not found")
|
||||
|
||||
with _db.transaction(actor):
|
||||
user_str = str(user_uuid)
|
||||
with _db.transaction("Registered credential", user=user_str):
|
||||
# Update display name if provided
|
||||
if display_name:
|
||||
_db._data.users[user_uuid].display_name = display_name
|
||||
|
||||
+21
-22
@@ -135,12 +135,11 @@ async def admin_create_org(
|
||||
)
|
||||
from ..db import Org as OrgDC # local import to avoid cycles
|
||||
|
||||
actor = str(ctx.user.uuid)
|
||||
org_uuid = uuid4()
|
||||
display_name = payload.get("display_name") or "New Organization"
|
||||
permissions = payload.get("permissions") or []
|
||||
org = OrgDC(uuid=org_uuid, display_name=display_name, permissions=permissions)
|
||||
db.create_organization(org, actor=actor)
|
||||
db.create_organization(org, ctx=ctx)
|
||||
|
||||
return {"uuid": str(org_uuid)}
|
||||
|
||||
@@ -167,7 +166,7 @@ async def admin_update_org_name(
|
||||
if not display_name:
|
||||
raise ValueError("display_name is required")
|
||||
|
||||
db.update_organization_name(org_uuid, display_name, actor=str(ctx.user.uuid))
|
||||
db.update_organization_name(org_uuid, display_name, ctx=ctx)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -199,9 +198,9 @@ async def admin_delete_org(org_uuid: UUID, request: Request, auth=AUTH_COOKIE):
|
||||
or perm_scope_lower.endswith(f":{org_perm_pattern}")
|
||||
or perm_scope_lower == org_perm_pattern
|
||||
):
|
||||
db.delete_permission(str(perm.uuid), actor=str(ctx.user.uuid))
|
||||
db.delete_permission(str(perm.uuid), ctx=ctx)
|
||||
|
||||
db.delete_organization(org_uuid, actor=str(ctx.user.uuid))
|
||||
db.delete_organization(org_uuid, ctx=ctx)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -216,7 +215,7 @@ async def admin_add_org_permission(
|
||||
auth, ["auth:admin"], host=request.headers.get("host"), match=permutil.has_all
|
||||
)
|
||||
db.add_permission_to_organization(
|
||||
str(org_uuid), permission_id, actor=str(ctx.user.uuid)
|
||||
str(org_uuid), permission_id, ctx=ctx
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
@@ -242,7 +241,7 @@ async def admin_remove_org_permission(
|
||||
)
|
||||
|
||||
db.remove_permission_from_organization(
|
||||
str(org_uuid), permission_id, actor=str(ctx.user.uuid)
|
||||
str(org_uuid), permission_id, ctx=ctx
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
@@ -284,7 +283,7 @@ async def admin_create_role(
|
||||
display_name=display_name,
|
||||
permissions=perms,
|
||||
)
|
||||
db.create_role(role, actor=str(ctx.user.uuid))
|
||||
db.create_role(role, ctx=ctx)
|
||||
return {"uuid": str(role_uuid)}
|
||||
|
||||
|
||||
@@ -315,7 +314,7 @@ async def admin_update_role_name(
|
||||
if not display_name:
|
||||
raise ValueError("display_name is required")
|
||||
|
||||
db.update_role_name(role_uuid, display_name, actor=str(ctx.user.uuid))
|
||||
db.update_role_name(role_uuid, display_name, ctx=ctx)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -351,7 +350,7 @@ async def admin_add_role_permission(
|
||||
if str(permission_uuid) not in org.permissions:
|
||||
raise ValueError("Permission not grantable by organization")
|
||||
|
||||
db.add_permission_to_role(role_uuid, permission_uuid, actor=str(ctx.user.uuid))
|
||||
db.add_permission_to_role(role_uuid, permission_uuid, ctx=ctx)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -396,7 +395,7 @@ async def admin_remove_role_permission(
|
||||
if not has_admin:
|
||||
raise ValueError("Cannot remove your own admin permissions")
|
||||
|
||||
db.remove_permission_from_role(role_uuid, permission_uuid, actor=str(ctx.user.uuid))
|
||||
db.remove_permission_from_role(role_uuid, permission_uuid, ctx=ctx)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -426,7 +425,7 @@ async def admin_delete_role(
|
||||
if ctx.role.uuid == role_uuid:
|
||||
raise ValueError("Cannot delete your own role")
|
||||
|
||||
db.delete_role(role_uuid, actor=str(ctx.user.uuid))
|
||||
db.delete_role(role_uuid, ctx=ctx)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -468,7 +467,7 @@ async def admin_create_user(
|
||||
visits=0,
|
||||
created_at=None,
|
||||
)
|
||||
db.create_user(user, actor=str(ctx.user.uuid))
|
||||
db.create_user(user, ctx=ctx)
|
||||
return {"uuid": str(user_uuid)}
|
||||
|
||||
|
||||
@@ -516,7 +515,7 @@ async def admin_update_user_role(
|
||||
"Cannot change your own role to one without admin permissions"
|
||||
)
|
||||
|
||||
db.update_user_role_in_organization(user_uuid, new_role, actor=str(ctx.user.uuid))
|
||||
db.update_user_role_in_organization(user_uuid, new_role, ctx=ctx)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -556,7 +555,7 @@ async def admin_create_user_registration_link(
|
||||
passphrase=token,
|
||||
expiry=expiry,
|
||||
token_type=token_type,
|
||||
actor=str(ctx.user.uuid),
|
||||
ctx=ctx,
|
||||
)
|
||||
url = hostutil.reset_link_url(token)
|
||||
return {
|
||||
@@ -739,7 +738,7 @@ async def admin_update_user_display_name(
|
||||
raise HTTPException(status_code=400, detail="display_name required")
|
||||
if len(new_name) > 64:
|
||||
raise HTTPException(status_code=400, detail="display_name too long")
|
||||
db.update_user_display_name(user_uuid, new_name, actor=str(ctx.user.uuid))
|
||||
db.update_user_display_name(user_uuid, new_name, ctx=ctx)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -768,7 +767,7 @@ async def admin_delete_user_credential(
|
||||
raise authz.AuthException(
|
||||
status_code=403, detail="Insufficient permissions", mode="forbidden"
|
||||
)
|
||||
db.delete_credential(credential_uuid, user_uuid, actor=str(ctx.user.uuid))
|
||||
db.delete_credential(credential_uuid, user_uuid, ctx=ctx)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -801,7 +800,7 @@ async def admin_delete_user_session(
|
||||
if not target_session or target_session.user_uuid != user_uuid:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
|
||||
db.delete_session(session_id, actor=str(ctx.user.uuid))
|
||||
db.delete_session(session_id, ctx=ctx)
|
||||
|
||||
# Check if admin terminated their own session
|
||||
current_terminated = session_id == auth
|
||||
@@ -954,7 +953,7 @@ async def admin_create_permission(
|
||||
PermDC(
|
||||
uuid=uuid7.create(), scope=scope, display_name=display_name, domain=domain
|
||||
),
|
||||
actor=str(ctx.user.uuid),
|
||||
ctx=ctx,
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
@@ -1004,7 +1003,7 @@ async def admin_update_permission(
|
||||
display_name=new_display_name,
|
||||
domain=domain_value,
|
||||
),
|
||||
actor=str(ctx.user.uuid),
|
||||
ctx=ctx,
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
@@ -1051,7 +1050,7 @@ async def admin_rename_permission(
|
||||
|
||||
# All current backends support rename_permission
|
||||
db.rename_permission(
|
||||
old_scope, new_scope, display_name, domain_value, actor=str(ctx.user.uuid)
|
||||
old_scope, new_scope, display_name, domain_value, ctx=ctx
|
||||
)
|
||||
return {"status": "ok"}
|
||||
|
||||
@@ -1083,5 +1082,5 @@ async def admin_delete_permission(
|
||||
if perm.scope == "auth:admin":
|
||||
_check_admin_lockout_on_delete(str(perm.uuid), request.headers.get("host"))
|
||||
|
||||
db.delete_permission(str(perm.uuid), actor=str(ctx.user.uuid))
|
||||
db.delete_permission(str(perm.uuid), ctx=ctx)
|
||||
return {"status": "ok"}
|
||||
|
||||
@@ -288,11 +288,11 @@ async def api_logout(request: Request, response: Response, auth=AUTH_COOKIE):
|
||||
if not auth:
|
||||
return {"message": "Already logged out"}
|
||||
try:
|
||||
s = await get_session(auth, host=request.headers.get("host"))
|
||||
_s = await get_session(auth, host=request.headers.get("host"))
|
||||
except ValueError:
|
||||
return {"message": "Already logged out"}
|
||||
with suppress(Exception):
|
||||
db.delete_session(auth, actor=str(s.user_uuid))
|
||||
db.delete_session(auth)
|
||||
session.clear_session_cookie(response)
|
||||
return {"message": "Logged out successfully"}
|
||||
|
||||
|
||||
@@ -351,7 +351,6 @@ async def websocket_remote_auth_permit(ws: WebSocket):
|
||||
passphrase=token_str,
|
||||
expiry=expiry,
|
||||
token_type="device addition",
|
||||
actor=str(stored_cred.user_uuid),
|
||||
)
|
||||
reset_token = token_str
|
||||
# Also create a session so the device is logged in
|
||||
|
||||
@@ -54,7 +54,7 @@ async def user_update_display_name(
|
||||
raise HTTPException(status_code=400, detail="display_name required")
|
||||
if len(new_name) > 64:
|
||||
raise HTTPException(status_code=400, detail="display_name too long")
|
||||
db.update_user_display_name(s.user_uuid, new_name, actor=str(s.user_uuid))
|
||||
db.update_user_display_name(s.user_uuid, new_name)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ async def api_logout_all(request: Request, response: Response, auth=AUTH_COOKIE)
|
||||
raise authz.AuthException(
|
||||
status_code=401, detail="Session expired", mode="login"
|
||||
)
|
||||
db.delete_sessions_for_user(s.user_uuid, actor=str(s.user_uuid))
|
||||
db.delete_sessions_for_user(s.user_uuid)
|
||||
session.clear_session_cookie(response)
|
||||
return {"message": "Logged out from all hosts"}
|
||||
|
||||
@@ -95,7 +95,7 @@ async def api_delete_session(
|
||||
if not target_session or target_session.user_uuid != current_session.user_uuid:
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
|
||||
db.delete_session(session_id, actor=str(current_session.user_uuid))
|
||||
db.delete_session(session_id)
|
||||
current_terminated = session_id == auth
|
||||
if current_terminated:
|
||||
session.clear_session_cookie(response) # explicit because 200
|
||||
@@ -141,7 +141,6 @@ async def api_create_link(
|
||||
passphrase=token,
|
||||
expiry=expiry,
|
||||
token_type="device addition",
|
||||
actor=str(s.user_uuid),
|
||||
)
|
||||
url = hostutil.reset_link_url(token)
|
||||
return {
|
||||
|
||||
@@ -86,7 +86,6 @@ async def websocket_register_add(
|
||||
host=host,
|
||||
ip=metadata.get("ip"),
|
||||
user_agent=metadata.get("user_agent"),
|
||||
actor=str(user_uuid),
|
||||
)
|
||||
auth = token
|
||||
|
||||
|
||||
+6
-8
@@ -38,10 +38,9 @@ from paskia.db import (
|
||||
create_session,
|
||||
create_user,
|
||||
)
|
||||
from paskia.db.operations import DB
|
||||
from paskia.db.operations import DB, _create_token
|
||||
from paskia.fastapi.session import AUTH_COOKIE_NAME
|
||||
from paskia.sansio import Passkey
|
||||
from paskia.util.tokens import create_token, session_key
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -220,11 +219,11 @@ async def session_token(
|
||||
test_db: DB, test_user: User, test_credential: Credential
|
||||
) -> str:
|
||||
"""Create a session for the admin user and return the token."""
|
||||
token = create_token()
|
||||
token = _create_token()
|
||||
create_session(
|
||||
user_uuid=test_user.uuid,
|
||||
credential_uuid=test_credential.uuid,
|
||||
key=session_key(token),
|
||||
key=token,
|
||||
host="localhost:4401",
|
||||
ip="127.0.0.1",
|
||||
user_agent="pytest",
|
||||
@@ -238,11 +237,11 @@ async def regular_session_token(
|
||||
test_db: DB, regular_user: User, regular_credential: Credential
|
||||
) -> str:
|
||||
"""Create a session for a regular user and return the token."""
|
||||
token = create_token()
|
||||
token = _create_token()
|
||||
create_session(
|
||||
user_uuid=regular_user.uuid,
|
||||
credential_uuid=regular_credential.uuid,
|
||||
key=session_key(token),
|
||||
key=token,
|
||||
host="localhost:4401",
|
||||
ip="127.0.0.1",
|
||||
user_agent="pytest",
|
||||
@@ -256,12 +255,11 @@ async def reset_token(test_db: DB, test_user: User, test_credential: Credential)
|
||||
"""Create a reset token for the test user."""
|
||||
from paskia.authsession import reset_expires
|
||||
from paskia.util.passphrase import generate
|
||||
from paskia.util.tokens import reset_key
|
||||
|
||||
token = generate()
|
||||
create_reset_token(
|
||||
user_uuid=test_user.uuid,
|
||||
key=reset_key(token),
|
||||
passphrase=token,
|
||||
expiry=reset_expires(),
|
||||
token_type="reset",
|
||||
)
|
||||
|
||||
+11
-16
@@ -34,8 +34,7 @@ from paskia.db import (
|
||||
create_session,
|
||||
create_user,
|
||||
)
|
||||
from paskia.db.operations import DB
|
||||
from paskia.util.tokens import create_token, encode_session_key, session_key
|
||||
from paskia.db.operations import DB, _create_token
|
||||
from tests.conftest import auth_headers
|
||||
|
||||
# -------------------- Additional Fixtures --------------------
|
||||
@@ -107,11 +106,11 @@ async def second_org_session_token(
|
||||
test_db: DB, second_org_user: User, second_org_credential: Credential
|
||||
) -> str:
|
||||
"""Create a session for the second org admin user."""
|
||||
token = create_token()
|
||||
token = _create_token()
|
||||
create_session(
|
||||
user_uuid=second_org_user.uuid,
|
||||
credential_uuid=second_org_credential.uuid,
|
||||
key=session_key(token),
|
||||
key=token,
|
||||
host="localhost:4401",
|
||||
ip="127.0.0.1",
|
||||
user_agent="pytest",
|
||||
@@ -173,11 +172,11 @@ async def org_admin_session_token(
|
||||
test_db: DB, org_admin_user: User, org_admin_credential: Credential
|
||||
) -> str:
|
||||
"""Create a session for the org admin user."""
|
||||
token = create_token()
|
||||
token = _create_token()
|
||||
create_session(
|
||||
user_uuid=org_admin_user.uuid,
|
||||
credential_uuid=org_admin_credential.uuid,
|
||||
key=session_key(token),
|
||||
key=token,
|
||||
host="localhost:4401",
|
||||
ip="127.0.0.1",
|
||||
user_agent="pytest",
|
||||
@@ -1195,21 +1194,19 @@ class TestAdminSessions:
|
||||
):
|
||||
"""Admin should be able to delete a user's session."""
|
||||
# Create an additional session to delete
|
||||
extra_token = create_token()
|
||||
extra_key = session_key(extra_token)
|
||||
extra_token = _create_token()
|
||||
create_session(
|
||||
user_uuid=test_user.uuid,
|
||||
credential_uuid=test_credential.uuid,
|
||||
key=extra_key,
|
||||
key=extra_token,
|
||||
host="other.host:4401",
|
||||
ip="192.168.1.1",
|
||||
user_agent="other-agent",
|
||||
expiry=expires(),
|
||||
)
|
||||
|
||||
encoded_key = encode_session_key(extra_key)
|
||||
response = await client.delete(
|
||||
f"/auth/api/admin/orgs/{test_org.uuid}/users/{test_user.uuid}/sessions/{encoded_key}",
|
||||
f"/auth/api/admin/orgs/{test_org.uuid}/users/{test_user.uuid}/sessions/{extra_token}",
|
||||
headers={**auth_headers(session_token), "Host": "localhost:4401"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -1226,9 +1223,8 @@ class TestAdminSessions:
|
||||
test_user,
|
||||
):
|
||||
"""Admin can delete their own current session."""
|
||||
encoded_key = encode_session_key(session_key(session_token))
|
||||
response = await client.delete(
|
||||
f"/auth/api/admin/orgs/{test_org.uuid}/users/{test_user.uuid}/sessions/{encoded_key}",
|
||||
f"/auth/api/admin/orgs/{test_org.uuid}/users/{test_user.uuid}/sessions/{session_token}",
|
||||
headers={**auth_headers(session_token), "Host": "localhost:4401"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -1283,10 +1279,9 @@ class TestAdminSessions:
|
||||
):
|
||||
"""Deleting non-existent session should fail."""
|
||||
# Use a valid format but non-existent key
|
||||
fake_key = session_key(create_token())
|
||||
encoded_key = encode_session_key(fake_key)
|
||||
fake_token = _create_token()
|
||||
response = await client.delete(
|
||||
f"/auth/api/admin/orgs/{test_org.uuid}/users/{test_user.uuid}/sessions/{encoded_key}",
|
||||
f"/auth/api/admin/orgs/{test_org.uuid}/users/{test_user.uuid}/sessions/{fake_token}",
|
||||
headers={**auth_headers(session_token), "Host": "localhost:4401"},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
+6
-6
@@ -499,10 +499,10 @@ class TestValidateSessionRefresh:
|
||||
self, client: httpx.AsyncClient, test_db
|
||||
):
|
||||
"""Validate should handle session expiry during refresh attempt."""
|
||||
from paskia.util.tokens import create_token
|
||||
from paskia.db.operations import _create_token
|
||||
|
||||
# Create a token but don't create a session for it
|
||||
token = create_token()
|
||||
token = _create_token()
|
||||
response = await client.post(
|
||||
"/auth/api/validate",
|
||||
headers={**auth_headers(token), "Host": "localhost:4401"},
|
||||
@@ -522,15 +522,15 @@ class TestValidateSessionRefresh:
|
||||
from datetime import timedelta
|
||||
|
||||
from paskia.authsession import EXPIRES
|
||||
from paskia.util.tokens import create_token, session_key
|
||||
from paskia.db.operations import _create_token
|
||||
|
||||
# Create a session with an old expiry time to trigger refresh
|
||||
token = create_token()
|
||||
token = _create_token()
|
||||
old_expiry = datetime.now(timezone.utc) + EXPIRES - timedelta(minutes=10)
|
||||
create_session(
|
||||
user_uuid=test_user.uuid,
|
||||
credential_uuid=test_credential.uuid,
|
||||
key=session_key(token),
|
||||
key=token,
|
||||
host="localhost:4401",
|
||||
ip="127.0.0.1",
|
||||
user_agent="pytest",
|
||||
@@ -538,7 +538,7 @@ class TestValidateSessionRefresh:
|
||||
)
|
||||
|
||||
# Delete the session right before validate tries to refresh
|
||||
delete_session(session_key(token))
|
||||
delete_session(token)
|
||||
|
||||
response = await client.post(
|
||||
"/auth/api/validate",
|
||||
|
||||
Reference in New Issue
Block a user