feat: rework auth model with UserToken and Daedalus/Pallas integration
Some checks failed
CVE Scan & Docker Build / build-and-push (push) Has been cancelled
CVE Scan & Docker Build / security-scan (push) Has been cancelled
Build & Deploy Docs / build-and-deploy (push) Successful in 1m10s

- Rename MCPToken to UserToken across models, views, and tests
- Update URL names from mcp-token-* to token-*
- Add Daedalus/Pallas integration design doc (v2)
- Switch docker-compose to build local mnemosyne:local image via shared
  build config instead of pulling from git.helu.ca
This commit is contained in:
2026-05-23 19:50:29 -04:00
parent 735eb9de1a
commit 93639188d3
44 changed files with 1305 additions and 865 deletions

View File

@@ -1,8 +1,8 @@
"""Self-service dashboard for MCP bearer tokens.
"""Self-service dashboard for per-user API tokens.
Mirrors the Themis API-keys flow visually but stores hashed tokens. Plaintext
is shown to the user exactly once (on the create-success page) and never
persisted.
Mirrors the Themis API-keys flow visually but stores hashed tokens.
Plaintext is shown to the user exactly once (on the create-success
page) and never persisted.
"""
from __future__ import annotations
@@ -13,24 +13,24 @@ from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_GET, require_http_methods, require_POST
from .forms import MCPTokenCreateForm, MCPTokenEditForm
from .models import MCPToken
from .forms import UserTokenCreateForm, UserTokenEditForm
from .models import UserToken
@login_required
@require_GET
def mcp_token_list(request: HttpRequest) -> HttpResponse:
tokens = MCPToken.objects.filter(user=request.user).order_by("-created_at")
def token_list(request: HttpRequest) -> HttpResponse:
tokens = UserToken.objects.filter(user=request.user).order_by("-created_at")
return render(request, "mcp_server/tokens/list.html", {"tokens": tokens})
@login_required
@require_http_methods(["GET", "POST"])
def mcp_token_create(request: HttpRequest) -> HttpResponse:
def token_create(request: HttpRequest) -> HttpResponse:
if request.method == "POST":
form = MCPTokenCreateForm(request.POST, user=request.user)
form = UserTokenCreateForm(request.POST, user=request.user)
if form.is_valid():
token, plaintext = MCPToken.objects.create_token(
token, plaintext = UserToken.objects.create_token(
user=request.user,
name=form.cleaned_data["name"],
allowed_tools=form.cleaned_data.get("allowed_tools") or [],
@@ -43,25 +43,25 @@ def mcp_token_create(request: HttpRequest) -> HttpResponse:
{"token": token, "plaintext": plaintext},
)
else:
form = MCPTokenCreateForm(user=request.user)
form = UserTokenCreateForm(user=request.user)
return render(request, "mcp_server/tokens/create.html", {"form": form})
@login_required
@require_GET
def mcp_token_detail(request: HttpRequest, pk: int) -> HttpResponse:
token = get_object_or_404(MCPToken, pk=pk, user=request.user)
def token_detail(request: HttpRequest, pk: int) -> HttpResponse:
token = get_object_or_404(UserToken, pk=pk, user=request.user)
return render(request, "mcp_server/tokens/detail.html", {"token": token})
@login_required
@require_http_methods(["GET", "POST"])
def mcp_token_edit(request: HttpRequest, pk: int) -> HttpResponse:
token = get_object_or_404(MCPToken, pk=pk, user=request.user)
def token_edit(request: HttpRequest, pk: int) -> HttpResponse:
token = get_object_or_404(UserToken, pk=pk, user=request.user)
if request.method == "POST":
form = MCPTokenEditForm(request.POST, instance=token, user=request.user)
form = UserTokenEditForm(request.POST, instance=token, user=request.user)
if form.is_valid():
instance = form.save(commit=False)
instance.allowed_tools = form.cleaned_data.get("allowed_tools") or []
@@ -69,10 +69,10 @@ def mcp_token_edit(request: HttpRequest, pk: int) -> HttpResponse:
form.cleaned_data.get("allowed_libraries") or []
)
instance.save()
messages.success(request, "MCP token updated.")
return redirect("mcp_server:mcp-token-detail", pk=token.pk)
messages.success(request, "Token updated.")
return redirect("mcp_server:token-detail", pk=token.pk)
else:
form = MCPTokenEditForm(instance=token, user=request.user)
form = UserTokenEditForm(instance=token, user=request.user)
return render(
request, "mcp_server/tokens/edit.html", {"form": form, "token": token}
@@ -81,19 +81,19 @@ def mcp_token_edit(request: HttpRequest, pk: int) -> HttpResponse:
@login_required
@require_POST
def mcp_token_revoke(request: HttpRequest, pk: int) -> HttpResponse:
token = get_object_or_404(MCPToken, pk=pk, user=request.user)
def token_revoke(request: HttpRequest, pk: int) -> HttpResponse:
token = get_object_or_404(UserToken, pk=pk, user=request.user)
token.is_active = False
token.save(update_fields=["is_active", "updated_at"])
messages.success(request, f"Revoked “{token.name}”. The token can no longer be used.")
return redirect("mcp_server:mcp-token-detail", pk=token.pk)
return redirect("mcp_server:token-detail", pk=token.pk)
@login_required
@require_POST
def mcp_token_delete(request: HttpRequest, pk: int) -> HttpResponse:
token = get_object_or_404(MCPToken, pk=pk, user=request.user)
def token_delete(request: HttpRequest, pk: int) -> HttpResponse:
token = get_object_or_404(UserToken, pk=pk, user=request.user)
name = token.name
token.delete()
messages.success(request, f"Deleted “{name}”.")
return redirect("mcp_server:mcp-token-list")
return redirect("mcp_server:token-list")