diff --git a/frontend/src/admin/AdminUserDetail.vue b/frontend/src/admin/AdminUserDetail.vue index c71a0b0..608afaf 100644 --- a/frontend/src/admin/AdminUserDetail.vue +++ b/frontend/src/admin/AdminUserDetail.vue @@ -24,6 +24,11 @@ const terminatingSessions = ref({}) const hoveredCredentialUuid = ref(null) const hoveredSession = ref(null) +// Convert credentials dict to array with uuid attached as 'credential' +const credentials = computed(() => + Object.entries(props.userDetail?.credentials || {}).map(([uuid, c]) => ({ ...c, credential: uuid })) +) + // Template refs for navigation const userInfoRef = ref(null) const regActionsRef = ref(null) @@ -218,7 +223,7 @@ defineExpose({ focusFirstElement })
{ return perms?.includes('auth:admin') || perms?.includes('auth:org:admin') }) const hasMultipleSessions = computed(() => Object.keys(sessions.value).length > 1) -const credentials = computed(() => authStore.userInfo?.credentials ? Object.values(authStore.userInfo.credentials) : []) +const credentials = computed(() => + Object.entries(authStore.userInfo?.credentials || {}).map(([uuid, c]) => ({ ...c, credential: uuid })) +) const useWideLayout = computed(() => { // Check if any single site has more than 8 sessions const groups = {} diff --git a/paskia/db/operations.py b/paskia/db/operations.py index d20eb30..b273009 100644 --- a/paskia/db/operations.py +++ b/paskia/db/operations.py @@ -11,8 +11,8 @@ import secrets from datetime import UTC, datetime, timedelta from uuid import UUID -import uuid7 import base64url +import uuid7 from paskia.config import SESSION_LIFETIME from paskia.db.jsonl import ( @@ -474,7 +474,7 @@ def set_session_host( def delete_session( - key: bytes, *, ctx: SessionContext | None = None, action: str = "delete_session" + key: str, *, ctx: SessionContext | None = None, action: str = "delete_session" ) -> None: """Delete a session. diff --git a/paskia/db/structs.py b/paskia/db/structs.py index d02ff10..abdd9ec 100644 --- a/paskia/db/structs.py +++ b/paskia/db/structs.py @@ -5,9 +5,9 @@ import secrets from datetime import UTC, datetime from uuid import UUID +import base64url import msgspec import uuid7 -import base64url from paskia import db from paskia.util import hostutil diff --git a/paskia/fastapi/admin.py b/paskia/fastapi/admin.py index 6dd90f0..5eafa59 100644 --- a/paskia/fastapi/admin.py +++ b/paskia/fastapi/admin.py @@ -1,7 +1,6 @@ import logging from uuid import UUID -import base64url from fastapi import Body, FastAPI, HTTPException, Query, Request, Response from fastapi.responses import JSONResponse diff --git a/paskia/fastapi/user.py b/paskia/fastapi/user.py index 4730766..932cf88 100644 --- a/paskia/fastapi/user.py +++ b/paskia/fastapi/user.py @@ -1,6 +1,5 @@ from uuid import UUID -import base64url from fastapi import ( Body, FastAPI, diff --git a/paskia/oidc_notify.py b/paskia/oidc_notify.py index ca8eb78..6eeba4a 100644 --- a/paskia/oidc_notify.py +++ b/paskia/oidc_notify.py @@ -9,12 +9,10 @@ import asyncio import logging from uuid import UUID -import base64url import httpx from paskia import db from paskia.util import oidjwt -from paskia.util.crypto import hash_secret from paskia.util.hostutil import _load_config _logger = logging.getLogger(__name__) @@ -30,7 +28,7 @@ def _issuer() -> str: def _collect_oidc_sessions( - session_keys: list[bytes], + session_keys: list[str], ) -> list[tuple[str, str, UUID, UUID | None]]: """Collect (backchannel_logout_uri, sid, client_uuid, user_uuid) for OIDC sessions. @@ -46,7 +44,7 @@ def _collect_oidc_sessions( client = data.oidc.clients.get(session.client_uuid) if not client or not client.backchannel_logout_uri: continue - sid = base64url.enc(hash_secret("oidc", session.key)) + sid = session.key notifications.append( (client.backchannel_logout_uri, sid, session.client_uuid, session.user_uuid) ) @@ -104,7 +102,7 @@ async def notify( await asyncio.gather(*tasks, return_exceptions=True) -def schedule_notifications(session_keys: list[bytes]) -> None: +def schedule_notifications(session_keys: list[str]) -> None: """Collect OIDC info from sessions (before deletion) and schedule async notifications. Must be called BEFORE the sessions are deleted. The actual HTTP requests diff --git a/tests/conftest.py b/tests/conftest.py index 9cbdd73..78a450c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,7 @@ from collections.abc import AsyncGenerator from datetime import UTC, datetime, timedelta from uuid import UUID +import base64url import httpx import pytest import pytest_asyncio @@ -251,10 +252,10 @@ def create_test_session( ip: str = "127.0.0.1", user_agent: str = "pytest", duration: timedelta | None = None, -) -> tuple[bytes, str]: +) -> tuple[str, str]: """Create a test session. Returns (key, token) tuple. - - key: bytes used for session lookup (base64url encode for URLs) + - key: str used for session lookup (base64url encoded) - token: stored in cookie/sent to client """ if duration is None: @@ -267,7 +268,7 @@ def create_test_session( # Generate token and derive key token = secrets.token_urlsafe(12) - key = hash_secret("cookie", token) + key = base64url.enc(hash_secret("cookie", token)) session = Session.create( user=user_uuid, diff --git a/tests/test_admin.py b/tests/test_admin.py index 7ff8fc0..9cc8b41 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1300,7 +1300,7 @@ class TestAdminSessions: ) response = await client.delete( - f"/auth/api/admin/users/{test_user.uuid}/sessions/{base64url.enc(extra_db_key)}", + f"/auth/api/admin/users/{test_user.uuid}/sessions/{extra_db_key}", headers={**auth_headers(session_token), "Host": "localhost:4401"}, ) assert response.status_code == 200 @@ -1364,9 +1364,9 @@ class TestAdminSessions: f"/auth/api/admin/users/{test_user.uuid}/sessions/invalid!!id", headers={**auth_headers(session_token), "Host": "localhost:4401"}, ) - assert response.status_code == 400 + assert response.status_code == 404 data = response.json() - assert "Invalid session ID format" in data["detail"] + assert "Session not found" in data["detail"] @pytest.mark.asyncio async def test_delete_session_not_found(