docs: replace daedalus-service basic auth with per-user DRF tokens
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 56s
CVE Scan & Docker Build / build-and-push (push) Successful in 3m30s

This commit is contained in:
2026-05-22 22:59:59 -04:00
parent 7296b8c42f
commit 409da7d109
17 changed files with 364 additions and 163 deletions

View File

@@ -5,9 +5,12 @@ A "workspace" in Mnemosyne is a Library scoped to a Daedalus workspace UUID.
It uses the same Library node as a global library; the difference is that
`workspace_id` is set, and search must filter on it.
These endpoints are called by the Daedalus backend (HTTP Basic auth as
the `daedalus-service` user). Daedalus owns the workspace_id; Mnemosyne
just persists what Daedalus tells it.
These endpoints are called by the Daedalus backend authenticated as the
Mnemosyne user the workspace belongs to (per-user DRF token). The
workspace's owning user is recorded on the Library node as
``owner_username``; every read and mutation is scoped to that user.
Non-owners receive 404 so a workspace's existence isn't disclosed
across users.
"""
import logging
@@ -72,6 +75,17 @@ def workspace_create(request):
existing = None
if existing is not None:
if existing.owner_username and existing.owner_username != request.user.username:
# Same workspace_id under a different owner. Don't leak the
# collision shape; surface a generic conflict.
logger.warning(
"workspace_create owner_conflict workspace_id=%s caller=%s",
data["workspace_id"], request.user.username,
)
return Response(
{"detail": "Workspace id is already in use."},
status=status.HTTP_409_CONFLICT,
)
if existing.library_type != data["library_type"]:
return Response(
{
@@ -98,6 +112,7 @@ def workspace_create(request):
library_type=data["library_type"],
description=data.get("description", ""),
workspace_id=data["workspace_id"],
owner_username=request.user.username,
chunking_config=defaults["chunking_config"],
embedding_instruction=defaults["embedding_instruction"],
reranker_instruction=defaults["reranker_instruction"],
@@ -127,21 +142,26 @@ def workspace_detail_or_delete(request, workspace_id):
"""
from library.models import Library
try:
lib = Library.nodes.get(workspace_id=workspace_id)
except Library.DoesNotExist:
lib = None
# Cross-user reads/writes look like "not found" — don't disclose
# existence across users.
if lib is not None and lib.owner_username != request.user.username:
lib = None
if request.method == "GET":
try:
lib = Library.nodes.get(workspace_id=workspace_id)
except Library.DoesNotExist:
if lib is None:
return Response(
{"detail": "Workspace not found."},
status=status.HTTP_404_NOT_FOUND,
)
return Response(WorkspaceStatusSerializer(_serialize_workspace(lib)).data)
# DELETE — idempotent: a missing workspace returns 204.
try:
lib = Library.nodes.get(workspace_id=workspace_id)
except Library.DoesNotExist:
# DELETE — idempotent: a missing (or unowned) workspace returns 204.
if lib is None:
return Response(status=status.HTTP_204_NO_CONTENT)
library_uid = lib.uid