Auth og roles
6. Auth og roles
Auth-laget er en kombination af Azure Container Apps' Easy Auth (identity) + vores egen TenantRoles-tabel (authorization).
Easy Auth-flow
- Bruger besøger en chrome-side (typisk
/admin-noget) - Middleware ser ingen valid session-cookie → redirect til Microsoft-login
- Microsoft authenticerer (med MFA hvis konfigureret) og returnerer en JWT-token
- Easy Auth modtager tokenen, sætter en HTTP-only cookie, og lader requesten gå videre
- Hver subsequent request: Easy Auth injicerer headere som
X-MS-CLIENT-PRINCIPALmed base64-encoded user-info - Vores kode læser headeren via
src/lib/auth-server.ts:getCurrentUser()for at vide hvem requestor er
getCurrentUser()
src/lib/auth-server.ts:
export async function getCurrentUser(): Promise<User | null> {
// Læs X-MS-CLIENT-PRINCIPAL fra request headers
// Returnerer { email, displayName, oid, ... } eller null hvis ikke logget ind
}
Server-components + route-handlers kalder denne for at få brugeren. Aldrig direkte parse JWT eller cookie — brug helperen.
Lokalt dev: når NEXT_PUBLIC_BYPASS_AUTH=1, mocker getCurrentUser() en hardcoded admin-bruger. Du skal ikke logge ind. Brugen brugen mock'er du i getCurrentUser selv ved at sætte de rigtige env-vars.
TenantRoles-tabel
For at vide hvilken rolle bruger X har på tenant Y:
- PartitionKey:
<tenant-slug> - RowKey:
<user-email>(lowercase) - Data: role ("Owner" | "Editor"), assignedBy, assignedAt
Helpers i src/lib/storage/tenant-roles.ts:
import { getTenantRole, setTenantRole, listTenantRoles, deleteTenantRole } from "@/lib/storage/tenant-roles";
const role = await getTenantRole("palle_jacobsen", "nis@example.com");
// → "Owner" | "Editor" | null
await setTenantRole("palle_jacobsen", "nye-editor@example.com", "Editor", actorEmail);
// Skriver TenantRoles-row + logger til RoleAuditLog
setTenantRole enforcer last-Owner-invariant: kan ikke fjerne eller demote den sidste Owner.
Permission-helpers
auth-server.ts har convenience-helpers:
import {
isPlatformAdmin,
isTenantOwner,
isEditor,
canEditContent,
canManageUsers,
getUserTenantRole,
} from "@/lib/auth-server";
if (await canManageUsers(currentUser, tenantSlug)) {
// user can invite/remove/change roles for this tenant
}
if (await canEditContent(currentUser, tenantSlug)) {
// user can edit pages, upload media, etc.
}
canManageUsers = Platform Admin OR Tenant Owner.
canEditContent = Platform Admin OR Tenant Owner OR Editor.
Brug DISSE i route-handlers og server-actions — IKKE rå rolle-check. De gør koden self-documenting og let at refactor hvis rolle-modellen ændrer sig.
requireXxx() helpers
src/lib/route-helpers.ts har throw-if-not-allowed-helpers:
import { requireTenantOwner, requireTenantEdit } from "@/lib/route-helpers";
export async function POST(req: Request, { params }: { params: { artist: string } }) {
const user = await getCurrentUser();
await requireTenantEdit(user, params.artist); // throws Response(403) hvis ikke allowed
// ... do the thing
}
Det er det helt foretrukne pattern — single line at toppen af handleren beskytter routen.
Audit-log
Hver gang setTenantRole/deleteTenantRole kaldes, skriver vi en row til RoleAuditLog med:
PartitionKey: <tenant-slug>
RowKey: <reverse-timestamp>_<uuid>
Data: actor, target, action, beforeRole, afterRole, timestamp, reason
Reverse-timestamp så nyeste først ved ascending query. Append-only — ingen update- eller delete-paths fra UI.
For at se historik: /admin/tenants/<slug> → Audit log-fanen, eller direct query via Storage Explorer.
Mocking auth i tests
Vi har endnu ikke et test-framework opsat. Når vi får det, planen er:
- Mock
getCurrentUser()med jest/vitest's module-mocking - Mock
getTenantRole()til at returnere ønsket rolle - Test route-handler logik isolert
For nu: smoke-test manuelt med forskellige browsers / incognito for at simulere forskellige brugere.