Autorisierung ist die zweite Frage nach Authentifizierung: Wer darf was? Während Auth (Kap 14) klärt, wer der User ist, klärt AuthZ, welche Aktionen dieser User auf welchen Ressourcen ausführen darf. Die Wahl des Access-Control-Modells (RBAC, ABAC, ReBAC) und die saubere Trennung von Policy und Mechanism sind die wichtigsten Architektur-Entscheidungen — Fehler hier führen zu IDOR/BOLA, Privilege-Escalation und Cross-Tenant-Leaks.
Autorisierung in einem Satz
Autorisierung beantwortet die Frage: „Darf Subject auf Object die Aktion ausführen — unter welchen Bedingungen?"
| Begriff | Beispiel |
|---|---|
| Subject | User Alice (id=42), Rolle „Editor", Tenant „acme" |
| Object | Dokument id=99, Tenant „acme" |
| Aktion | read, write, delete, share |
| Bedingungen | „Owner darf immer", „Editor nur in eigenem Tenant", „Außerhalb der Arbeitszeit nur read" |
Eine Autorisierungs-Entscheidung kombiniert all diese Inputs zu einem Allow / Deny.
Trennung von AuthN: Auth-Middleware setzt req.user. Permission-Check ist ein separater Schritt pro Endpoint oder pro Ressource. Wer das vermischt, baut sich IDOR-Lücken.
Die klassischen Modelle
| Modell | Wer entscheidet? | Typische App |
|---|---|---|
| DAC (Discretionary AC) | Object-Owner | Filesystem, Google Docs Sharing |
| MAC (Mandatory AC) | Zentrale Policy, vom OS/System erzwungen | Militär (Bell-LaPadula), SELinux |
| RBAC (Role-Based AC) | Rollen-Zuweisung | Klassische Enterprise-App, Admin-Panels |
| ABAC (Attribute-Based AC) | Policy-Engine mit Attributen | Cloud-IAM (AWS, GCP), feinkörnige Apps |
| ReBAC (Relationship-Based AC) | Beziehungs-Graph | Google Drive, Facebook, Zanzibar |
DAC — Standard in Filesystem-Welt. User, dem das Objekt gehört, entscheidet, wer noch Zugriff bekommt. „Alice teilt Datei mit Bob."
MAC — Sicherheits-Klassifikation (Confidential, Secret, Top Secret) wird vom System erzwungen. Klassisches Militär-Modell. In Web-Apps selten.
RBAC — User → Rolle → Permissions. Klassisch. Skaliert gut bis ~hundert Rollen; darüber wird's unübersichtlich.
ABAC — Policies entscheiden auf Basis von Attributen (User-Attribute, Resource-Attribute, Umgebungs-Attribute). Beispiel: „User mit department=hr darf Records mit category=hr lesen, wenn time zwischen 8 und 18 Uhr ist". Flexibler als RBAC, aber komplexer.
ReBAC — von Google Zanzibar populär gemacht. Permissions ergeben sich aus Beziehungen im Graphen. „Bob ist Editor in Folder X. Datei Y ist in Folder X. → Bob darf Y editieren." Skaliert auf Trillionen Edges.
RBAC in der Praxis
RBAC ist das pragmatische Default-Modell für die meisten Apps. Klare Struktur:
User
↓ hat
Role(s)
↓ hat
Permission(s)
↓ erlaubt
Action auf Resource-TypBeispiel-Schema:
CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT);
CREATE TABLE roles (id SERIAL PRIMARY KEY, name TEXT);
CREATE TABLE permissions (id SERIAL PRIMARY KEY, name TEXT);
CREATE TABLE user_roles (user_id INT, role_id INT);
CREATE TABLE role_permissions (role_id INT, permission_id INT);
-- Beispiel-Permissions
INSERT INTO permissions (name) VALUES
('post:read'), ('post:write'), ('post:delete'),
('user:manage'), ('billing:view');
-- Rolle "Editor" hat post:read, post:write
-- Rolle "Admin" hat allesCheck-Code:
async function hasPermission(userId, permissionName) {
const result = await db.query(`
SELECT 1 FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE ur.user_id = $1 AND p.name = $2
LIMIT 1
`, [userId, permissionName]);
return result.rowCount > 0;
}
// Middleware
function requirePermission(permission) {
return async (req, res, next) => {
if (!(await hasPermission(req.user.id, permission))) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
app.delete('/posts/:id', requirePermission('post:delete'), handler);Was RBAC nicht löst:
- Pro-Resource-Permission — „User darf Post X bearbeiten, aber nicht Post Y" — braucht zusätzlich Resource-Owner-Check (idor-und-bola).
- Komplexe Bedingungen — „nur tagsüber", „nur aus Firmennetz" — braucht ABAC oder Policy-Engine.
- Hierarchische Strukturen — „Manager hat alle Permissions seiner Reports" — braucht ReBAC oder erweitertes RBAC.
Policy vs Mechanism
Eine zentrale Architektur-Prinzip: Wer entscheidet (Policy) und wer prüft (Mechanism) sollten getrennt sein.
Anti-Pattern (Permissions verstreut im Code):
app.get('/admin/users', (req, res) => {
if (req.user.role === 'admin') return getUsers(res);
return res.status(403).end();
});
app.post('/posts', (req, res) => {
if (req.user.role === 'editor' || req.user.role === 'admin') return createPost(req, res);
return res.status(403).end();
});
app.delete('/posts/:id', async (req, res) => {
const post = await db.posts.findOne({ id: req.params.id });
if (req.user.role !== 'admin' && post.authorId !== req.user.id) {
return res.status(403).end();
}
return deletePost(req, res);
});Jede Endpoint hat eigene Permission-Logik. Bei Policy-Änderung (z. B. neue Rolle „Reviewer") muss man alle Endpoints durchgehen. Konsistenz-Risiko hoch.
Sauberes Pattern (Policy zentral, Mechanism überall gleich):
// policy.js — zentral
const policies = {
'users:read': user => user.role === 'admin',
'posts:create': user => ['editor', 'admin'].includes(user.role),
'posts:delete': (user, post) =>
user.role === 'admin' || post.authorId === user.id,
};
function can(user, action, resource = null) {
const policy = policies[action];
if (!policy) return false;
return policy(user, resource);
}
// Mechanism — überall gleich
function authorize(action, getResource = null) {
return async (req, res, next) => {
const resource = getResource ? await getResource(req) : null;
if (!can(req.user, action, resource)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
// Endpoints — deklarativ
app.get('/admin/users', authorize('users:read'), handler);
app.post('/posts', authorize('posts:create'), handler);
app.delete('/posts/:id',
authorize('posts:delete', req => db.posts.findOne({ id: req.params.id })),
handler
);Wenn Policies sich ändern → eine Datei. Endpoints bleiben deklarativ.
Default-Deny als Architektur-Grundsatz
Eine Permission-Architektur sollte Default-Deny sein: wenn keine explizite Erlaubnis vorliegt, ist die Aktion verboten.
Default-Allow ist Anti-Pattern: „Endpoint hat keine Auth-Middleware, also läuft er einfach durch" — klassische Quelle für Lücken bei neuen Endpoints, die jemand vergisst zu schützen.
Default-Deny in der Praxis:
- Auth-Middleware global — jeder Endpoint braucht Auth, außer wenn explizit als öffentlich markiert.
- Permission-Check zwingend — Helper-Funktion bei jedem sensitiven Endpoint pflichtgemäß.
- Code-Review als Backstop — Pull-Request-Checkliste hat „Permission-Check vorhanden?" als Pflicht-Item.
// Globale Middleware — Auth Pflicht
app.use((req, res, next) => {
if (PUBLIC_ENDPOINTS.includes(req.path)) return next();
if (!req.user) return res.status(401).end();
next();
});
// Pro Endpoint: Permission-Check
app.delete('/api/...', authorize('action:resource'), handler);
// Linter-Regel oder Custom-Test: alle non-public Endpoints müssen authorize() habenPolicy-Engines
Bei wachsender Komplexität lohnen sich dedizierte Policy-Engines:
| Engine | Sprache | Use Case |
|---|---|---|
| OPA (Open Policy Agent) | Rego | Cloud-Native, K8s, Microservices, ABAC |
| Casbin | CSV/Conf-DSL | App-eingebettet, RBAC/ABAC |
| Oso | Polar | Code-nahe, RBAC/ReBAC |
| OpenFGA | DSL (Zanzibar-Style) | ReBAC, große Graphen |
| Cerbos | YAML | Self-Hosted, ABAC mit Conditions |
Wann sich eine Engine lohnt:
- Mehr als ~20 Rollen oder komplexe Bedingungen — Code wird unwartbar.
- Policy-Änderungen sollen ohne Deploy möglich sein — Engine erlaubt Hot-Reload.
- Compliance-Anforderungen mit auditierbaren Policy-Files.
- Multi-Service-Setup — gleiche Policy für API-Gateway, Backend-Services, Datenbank-Layer.
Wann nicht:
- Kleine App mit wenigen Rollen — eigener Code reicht.
- Single-Service-Setup ohne Policy-Updates zur Laufzeit.
Faustregel: Mit eigenem Policy-Code starten. Wenn die Permission-Datei größer als ~500 LOC wird oder Policy-Updates per CI/CD nervig sind — Migration auf Engine ist angemessen.
ReBAC und Google Zanzibar
Zanzibar ist Googles internes Permission-System für Drive, Calendar, Photos, YouTube. 2019 als Paper veröffentlicht — sehr einflussreich.
Konzept: Permissions als Beziehungs-Tupel in einem Graphen:
doc:123#owner@user:alice (Alice ist Owner von Doc 123)
doc:123#viewer@user:bob (Bob ist Viewer von Doc 123)
doc:123#viewer@folder:hr#viewer (Wer hr-folder viewen darf, darf auch doc:123 viewen)
folder:hr#viewer@user:charlie (Charlie darf hr-folder viewen)Permission-Check: „Darf Charlie doc:123 viewen?" — Engine traversiert den Graph: Charlie ist Viewer von folder:hr → folder:hr ist Viewer von doc:123 → ja.
Open-Source-Implementierungen:
- OpenFGA — CNCF, Zanzibar-kompatibel.
- SpiceDB (Authzed) — kommerziell + Open-Source-Core.
- Warrant — als SaaS und Open-Source.
Wann ReBAC sinnvoll:
- Hierarchische Permissions (Folder/Subfolder/File).
- User können Resources teilen (Drive-/Notion-artig).
- Permissions ändern sich häufig (nicht fix wie Admin/User).
Wann nicht:
- Einfache App ohne Sharing-Modell — RBAC reicht.
- Wenig Performance-Skalierung nötig — eigene SQL-Queries einfacher.
Permissions-Test-Strategien
Unit-Tests für Policies:
describe('posts:delete policy', () => {
test('admin can delete any post', () => {
const admin = { id: 1, role: 'admin' };
const post = { authorId: 2 };
expect(can(admin, 'posts:delete', post)).toBe(true);
});
test('author can delete own post', () => {
const author = { id: 1, role: 'user' };
const post = { authorId: 1 };
expect(can(author, 'posts:delete', post)).toBe(true);
});
test('non-author cannot delete others post', () => {
const user = { id: 1, role: 'user' };
const post = { authorId: 2 };
expect(can(user, 'posts:delete', post)).toBe(false);
});
});Integration-Tests für Endpoints:
Cross-User-Tests sind die wichtigsten — pro Endpoint einen Test, der mit User A versucht, auf Resource von User B zuzugreifen. Erwarteter Response: 403.
Automatisierung:
- Permission-Matrix-Tests — generierter Test-Cube über alle (User-Rolle × Endpoint × Resource-Owner)-Kombinationen.
- OPA-Tests mit Rego-Test-Framework (für OPA-Setups).
- Cerbos-Tests mit YAML-Test-Definitionen.
Interessantes
Permissions sind Versionierung-bedürftig
Wenn sich Permission-Definitionen ändern, alte aktive Sessions können noch alte Permissions cachen. Pattern: permission_version beim User, bei Login in Session schreiben. Bei Inkrement → Session-Invalidierung-Pflicht. Verhindert „eingeloggter User behält veraltete Permissions" nach Re-Org.
Anti-Pattern: Permission-Check im Frontend (allein)
Frontend kann UI-Elemente verstecken („Delete-Button nur für Admin sichtbar"). Das ist UX, kein Sicherheits-Schutz. Backend muss jeden Permission-Check selbst durchführen — Frontend ist user-kontrolliert. Vertieft in feature-flags-und-soft-launch.
Permission vs. Visibility
Zwei separate Konzepte: kann der User die Resource sehen (List-Endpoint) und darf der User die Resource modifizieren (Action-Endpoint). Beide brauchen Checks, beide haben unterschiedliche Logik. Oft wird List-Permission vergessen — IDOR-Klassiker.
Group-Memberships als Permission-Vermittler
Statt direkter User-Permission-Verknüpfung: User → Gruppe → Permissions. Wenn neue Mitarbeiter:in startet, Group-Membership einrichten — Permissions sind automatisch da. Skaliert deutlich besser als User-individuelle Permissions. AzureAD/Okta/Keycloak nutzen das Pattern.
Audit-Log für Permission-Änderungen
Wer hat wem welche Rolle wann zugewiesen? Audit-Log mit User-ID, Target-User-ID, Rolle, Zeitstempel, Begründung. Bei Compliance-Audit oder Vorfalls-Forensik essenziell. Permission-Eskalation ist ein klassischer Insider-Angriffs-Vektor.
Lazy vs Eager Permission-Loading
Bei jedem Request alle User-Permissions laden (Lazy/per-Request) ist DB-Last. Caching im Session-Token oder Memory beschleunigt — verschiebt aber Revocation-Probleme (Permission-Wechsel wirkt erst nach Cache-Expiry). Hybrid: Permissions im Token einbetten, Token kurz (5–15 min), Refresh holt frische Permissions.
Permission-Inflation als organisatorischer Drift
Über Zeit sammeln sich Permissions an: User wechselt Rolle, behält alte zusätzlich. Nach 5 Jahren hat „lifelong employee" mehr Permissions als nötig. Regelmäßige Access-Reviews (quartärlich, jährlich) als Hygiene-Maßnahme. Compliance-Pflicht in einigen Branchen.
Weiterführende Ressourcen
Externe Quellen
- OWASP Authorization Cheat Sheet
- Google Zanzibar Paper
- Open Policy Agent (OPA)
- OpenFGA
- Cerbos
- Casbin
- NIST RBAC Definition