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

  1. Bruger besøger en chrome-side (typisk /admin-noget)
  2. Middleware ser ingen valid session-cookie → redirect til Microsoft-login
  3. Microsoft authenticerer (med MFA hvis konfigureret) og returnerer en JWT-token
  4. Easy Auth modtager tokenen, sætter en HTTP-only cookie, og lader requesten gå videre
  5. Hver subsequent request: Easy Auth injicerer headere som X-MS-CLIENT-PRINCIPAL med base64-encoded user-info
  6. 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.