Freitag, 16:47. Dein größter Enterprise-Kunde ruft an. Er sieht Rechnungen die nicht ihm gehören.
Du schaust in den Code. Der Endpoint für den Report-Export wurde letzte Woche deployt. Schnell gebaut, Deadline war eng. Der Query: SELECT * FROM invoices WHERE created_at > ?. Kein AND tenant_id = ?. Drei Tage in Produktion. 847 Datensätze exponiert.
Das passiert nicht weil dein Team schlecht ist. Das passiert weil dein Autorisierungsmodell auf einer Konvention basiert: “jeder Endpoint filtert nach tenant_id.” Konventionen brechen.
Klick auf Deploy und schau was passiert wenn ein einziger Endpoint die Konvention nicht einhält:
Ein Endpoint. Kein Tenant-Check. Und plötzlich leaken Cross-Tenant-Requests. In einer echten Anwendung mit 40-50 Endpoints passiert das nicht mit einem, sondern mit fünf oder sechs. Und du weißt nicht welche.
Das WHERE-Clause-Problem
Die meisten Multi-Tenant-Systeme autorisieren auf drei Arten:
Row-Level Security in der Datenbank. PostgreSQL RLS oder WHERE tenant_id = ? in jedem Query. Funktioniert, bis jemand eine Raw Query schreibt, eine View vergisst, oder einen Batch-Job baut der quer über Tenants läuft.
Middleware im API Layer. Ein Express-Middleware oder Spring-Filter der den Tenant aus dem JWT extrahiert und in den Request-Context packt. Besser, aber jeder Controller muss den Context auch nutzen. Vergisst einer ihn, bringt die Middleware nichts.
Manuell pro Endpoint. if (user.tenantId !== resource.tenantId) throw 403. Das skaliert bis es nicht mehr skaliert. Und “skaliert nicht” bedeutet hier: ein vergessenes if.
Row-Level Security in PostgreSQL ist mächtig: CREATE POLICY tenant_isolation ON orders USING (tenant_id = current_setting('app.tenant_id')). Aber es
schützt nur die Datenbank. Dein API Gateway, dein Cache, dein Event-Bus, dein
S3 Bucket alles andere ist nicht abgedeckt.
Alle drei Ansätze haben dasselbe Problem: die Autorisierungslogik ist verteilt. In jedem Query, jeder Middleware, jedem Controller. Und verteilte Logik ist schwer zu auditen. Wie willst du sicher sein, dass alle 47 Endpoints den Tenant prüfen? Code Review? Grep? Hoffen?
Cedar Autorisierung als Sprache
AWS Verified Permissions nutzt Cedar, eine Policy-Sprache die Amazon intern entwickelt hat. Cedar löst das Verteilungsproblem: statt Autorisierung in den Code zu streuen, schreibst du sie als deklarative Policies die zentral evaluiert werden.
Eine Cedar Policy:
permit(principal, action, resource)
when { principal.tenant == resource.tenant };
Eine Zeile. Kein if. Kein WHERE. Ein Policy-Satz der für jeden Request evaluiert wird. Principal hat denselben Tenant wie die Resource? Permit. Sonst? Default deny.
Klick dich durch die Szenarien und schau wie Cedar evaluiert:
Schau dir den letzten Fall an “Gesperrt.” Bob gehört zu Tenant Acme. Die Resource gehört zu Tenant Acme. Die Tenant-Isolation-Policy matcht: permit. Aber die Suspend-Policy matcht auch: forbid. Und in Cedar schlägt forbid immer permit. Egal wie viele Permit-Policies matchen: ein einziges Forbid reicht.
Das ist keine Eigenheit von Cedar. XACML, OPA/Rego und Google Zanzibar folgen demselben Prinzip: explicit deny overrides allow. Der Unterschied: Cedar hat formal verifizierte Evaluation: Amazon hat mathematisch bewiesen, dass die Evaluation deterministisch und terminierend ist.
Das ist der fundamentale Unterschied zu verstreuten if-Statements: du definierst einmal was erlaubt ist, und die Policy Engine sorgt dafür, dass es überall gilt.
Verified Permissions in der Praxis
AWS Verified Permissions ist der Managed Service für Cedar. Du erstellst einen Policy Store, definierst ein Schema, und rufst IsAuthorized auf:
import { VerifiedPermissions } from "@aws-sdk/client-verifiedpermissions";
const avp = new VerifiedPermissions({ region: "eu-central-1" });
async function authorize(req: Request): Promise<boolean> {
const { decision } = await avp.isAuthorized({
policyStoreId: "ps-abc123",
principal: {
entityType: "App::User",
entityId: req.userId,
},
action: {
actionType: "App::Action",
actionId: `${req.method}::${req.path}`,
},
resource: {
entityType: "App::Resource",
entityId: req.resourceId,
},
entities: {
entityList: [
{
identifier: {
entityType: "App::User",
entityId: req.userId,
},
attributes: {
tenant: { string: req.tenantId },
role: { string: req.userRole },
suspended: { boolean: req.userSuspended },
},
},
{
identifier: {
entityType: "App::Resource",
entityId: req.resourceId,
},
attributes: {
tenant: { string: req.resourceTenantId },
},
},
],
},
});
return decision === "ALLOW";
}
Jeder Request geht durch dieselbe Funktion. Keine vergessenen Checks, keine Konvention die jemand brechen kann. Die Policies leben im Policy Store: änderbar ohne Deployment, versioniert, auditierbar.
Das Schema
Das Cedar Schema definiert deine Entitäten und welche Actions auf welchen Ressourcen erlaubt sind:
entity User {
tenant: String,
role: String,
suspended: Bool,
};
entity Resource {
tenant: String,
};
action read, write, delete appliesTo {
principal: User,
resource: Resource,
};
Verified Permissions validiert deine Policies gegen das Schema. Wenn du eine Policy schreibst die auf ein Attribut zugreift das nicht existiert, kriegst du einen Fehler zur Deploy-Zeit, nicht erst zur Runtime.
Das Schema ist optional, aber du solltest es nutzen. Ohne Schema kann Cedar jede Policy speichern, auch solche mit Tippfehlern in Attributnamen. Mit Schema fängt der Service das ab. Wie TypeScript für deine Autorisierung.
Latenz
Die Frage die jeder stellt: “Wie schnell ist der API Call?” Verified Permissions antwortet in 5-15ms. Für die meisten APIs akzeptabel. Für High-Frequency-Pfade gibt es zwei Optionen:
Batch Authorization. IsAuthorizedWithToken evaluiert mehrere Entscheidungen in einem Call. Reduziert Roundtrips.
Local Evaluation. Du cachest die Policies lokal und evaluierst mit der Cedar SDK (@cedar-policy/cedar-wasm). Sub-Millisekunde, kein Netzwerk. Dafür musst du den Cache invalidieren wenn Policies sich ändern.
Für 95% der SaaS-Anwendungen reichen die 10ms. Optimier erst wenn du ein Problem hast.
Wann es sich lohnt
Ja bei Multi-Tenant SaaS mit Enterprise-Kunden: Tenant-Isolation ist nicht verhandelbar, deine Kunden werden nach SOC 2 fragen, und zentrale Policies die du auditen kannst sind kein Nice-to-have.
Ja bei komplexen Berechtigungsmodellen: wenn du Role-Based und Attribute-Based Access Control brauchst (Admin in Tenant A darf X, aber Manager in Tenant B darf nur Y), wird verstreute Logik unhaltbar.
Nein bei Single-Tenant Apps. Wenn es keinen zweiten Tenant gibt, reicht Middleware.
Nein bei Simple RBAC. Wenn du drei Rollen hast und das reicht, bau es nicht komplizierter als nötig.
Nein bei Prototypen. Erst das Produkt, dann die Autorisierung. Cedar ist für Systeme die in Produktion sind und bleiben.
Die Frage ist nicht “brauche ich AWS Verified Permissions?” Die Frage ist: kannst du in unter 5 Minuten belegen, dass jeder Endpoint in deiner Anwendung Tenant-isoliert ist?
Wenn die Antwort nein ist: dein Enterprise-Kunde wartet schon.