fix(t-paliad-146): gate Paliadin to owner email in code, drop PALIADIN_ENABLED

m's call (2026-05-07 21:52): "remove the export variable, that is bad
form. It should be connected only to my account."

The PALIADIN_ENABLED env var was a deploy-time toggle: easy to
mis-flip, splits prod/dev behaviour, and reads as "could be turned on
for anyone." Replaced with a per-request gate in code:

  services.PaliadinOwnerEmail = "matthias.siebels@hoganlovells.com"

handlers/paliadin.go now gates every entry point through
requirePaliadinOwner, which looks up paliad.users.email by the caller's
UUID and returns 404 (not 403 — pretend the route doesn't exist) for
anyone else.

Routes register unconditionally; the gate is in the code, not the
deploy. main.go wires PaliadinService whenever DATABASE_URL is set and
logs the owner identity at boot. CLAUDE.md drops the PALIADIN_ENABLED
row and gains an explanatory note about the in-code gate.

Sidebar entries (Paliadin under Übersicht; Paliadin Monitor under
Admin) now render with display:none, revealed by sidebar.ts after
/api/me confirms the caller's email matches PALIADIN_OWNER_EMAIL —
same fail-closed pattern the Admin group already uses.

Side-effect for ops: paliad.de production now serves the routes too,
but only to m, and only successfully if the host has tmux + claude
in PATH (which Dokploy doesn't). m hitting /paliadin from prod gets a
"tmux unavailable" — clear failure mode, not a security concern.

One new test (TestPaliadinOwnerEmail_IsLowercaseStable) keeps the
constant aligned with migration 023's seed so a future rename of m's
account doesn't silently strand the gate. All existing tests pass.
This commit is contained in:
m
2026-05-07 21:57:20 +02:00
parent 7b66c4d035
commit 8d714dd95e
8 changed files with 157 additions and 86 deletions

View File

@@ -116,7 +116,14 @@ export function Sidebar({ currentPath, authenticated = true }: SidebarProps): st
navItem("/dashboard", ICON_GAUGE, "nav.dashboard", "Dashboard", currentPath) +
navItem("/agenda", ICON_AGENDA, "nav.agenda", "Agenda", currentPath) +
navItem("/inbox", ICON_BELL, "nav.inbox", "Inbox", currentPath, "sidebar-inbox-badge") +
navItem("/paliadin", ICON_SPARKLE, "nav.paliadin", "Paliadin", currentPath) +
// Paliadin entry \u2014 owner-only, hidden by default. sidebar.ts
// reveals it after /api/me confirms the caller is the
// Paliadin owner (t-paliad-146 PoC scope). Same fail-closed
// pattern as the admin group below.
`<a href="/paliadin" class="sidebar-item sidebar-paliadin${currentPath === "/paliadin" ? " active" : ""}" id="sidebar-paliadin-link" style="display:none">` +
`<span class="sidebar-icon">${ICON_SPARKLE}</span>` +
`<span class="sidebar-label" data-i18n="nav.paliadin">Paliadin</span>` +
`</a>` +
navItem("/team", ICON_USERS, "nav.team", "Team", currentPath),
)}
@@ -172,7 +179,13 @@ export function Sidebar({ currentPath, authenticated = true }: SidebarProps): st
{navItem("/admin/partner-units", ICON_BUILDING, "nav.admin.partner_units", "Partner Units", currentPath)}
{navItem("/admin/event-types", ICON_TABLE, "nav.admin.event_types", "Event-Typen", currentPath)}
{navItem("/admin/audit-log", ICON_AUDIT_LOG, "nav.admin.audit", "Audit-Log", currentPath)}
{navItem("/admin/paliadin", ICON_SPARKLE, "nav.admin.paliadin", "Paliadin Monitor", currentPath)}
{/* Paliadin Monitor — owner-only sub-entry; revealed by sidebar.ts together with the /paliadin link. */}
<a href="/admin/paliadin" id="sidebar-admin-paliadin-link"
className={`sidebar-item${currentPath === "/admin/paliadin" ? " active" : ""}`}
style="display:none">
<span className="sidebar-icon" dangerouslySetInnerHTML={{ __html: ICON_SPARKLE }} />
<span className="sidebar-label" data-i18n="nav.admin.paliadin">Paliadin Monitor</span>
</a>
</div>
</nav>