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?"

BegriffBeispiel
SubjectUser Alice (id=42), Rolle „Editor", Tenant „acme"
ObjectDokument id=99, Tenant „acme"
Aktionread, 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

ModellWer entscheidet?Typische App
DAC (Discretionary AC)Object-OwnerFilesystem, Google Docs Sharing
MAC (Mandatory AC)Zentrale Policy, vom OS/System erzwungenMilitär (Bell-LaPadula), SELinux
RBAC (Role-Based AC)Rollen-ZuweisungKlassische Enterprise-App, Admin-Panels
ABAC (Attribute-Based AC)Policy-Engine mit AttributenCloud-IAM (AWS, GCP), feinkörnige Apps
ReBAC (Relationship-Based AC)Beziehungs-GraphGoogle 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:

Plain rbac-structure.txt
User
  ↓ hat
Role(s)
  ↓ hat
Permission(s)
  ↓ erlaubt
Action auf Resource-Typ

Beispiel-Schema:

SQL rbac-schema.sql
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 alles

Check-Code:

JavaScript rbac-check.js
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):

JavaScript permissions-scattered-antipattern.js
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):

JavaScript policy-centralized.js
// 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.
JavaScript default-deny-pattern.js
// 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() haben

Policy-Engines

Bei wachsender Komplexität lohnen sich dedizierte Policy-Engines:

EngineSpracheUse Case
OPA (Open Policy Agent)RegoCloud-Native, K8s, Microservices, ABAC
CasbinCSV/Conf-DSLApp-eingebettet, RBAC/ABAC
OsoPolarCode-nahe, RBAC/ReBAC
OpenFGADSL (Zanzibar-Style)ReBAC, große Graphen
CerbosYAMLSelf-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:

Plain zanzibar-tuples.txt
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:

JavaScript policy-unit-tests.js
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

/ Weiter

Zurück zu Autorisierung & Access Control

Zur Übersicht