RBAC ist das Standard-Modell für Permissions in den meisten Apps: User bekommt eine Rolle, Rolle hat Permissions, fertig. Bis die Anforderungen komplex werden — „Editor darf, aber nur in eigenem Tenant, nur tagsüber, nur bei Documents mit Status draft". Dann braucht es ABAC (Attribute-Based) oder einen Hybrid-Ansatz. Dieser Artikel zeigt die Modelle in der Praxis, konkrete Engines und die Entscheidungs-Faktoren.

RBAC — die pragmatische Standard-Wahl

In RBAC ist die Permission-Entscheidung an Rollen geknüpft. Ein User hat eine oder mehrere Rollen, Rolle bündelt Permissions.

Typische Rollen-Hierarchie in einer App:

Plain typical-roles.txt
Owner       — Tenant-Admin, alle Permissions in eigenem Tenant

Admin       — App-weit Admin (nur für Operator-Team)

Editor      — kann Content erstellen/bearbeiten

Viewer      — Read-only

Guest       — eingeschränkter Read-Zugriff

Permissions als Wertebereich: typisch <resource>:<action>-Strings (post:read, post:write, user:manage).

Implementierung (vereinfacht):

JavaScript rbac-simple-implementation.js
const ROLE_PERMISSIONS = {
  viewer:  ['post:read', 'comment:read'],
  editor:  ['post:read', 'post:write', 'comment:read', 'comment:write'],
  admin:   ['post:read', 'post:write', 'post:delete', 'user:manage'],
  owner:   ['*'],  // alle Permissions
};

function can(user, action) {
  const perms = ROLE_PERMISSIONS[user.role] || [];
  return perms.includes('*') || perms.includes(action);
}

app.get('/posts/:id', requirePermission('post:read'), handler);

Vorteile von RBAC:

  • Einfach zu verstehen — User können in UI selbst ihre Rolle sehen.
  • Audit-freundlich — „Welche Permissions hat User X?" ist eine simple JOIN.
  • Skaliert bis ~50 Rollen — danach wird die Rollen-Matrix unübersichtlich.

Nachteile:

  • Rollen-Explosion bei feinkörnigen Anforderungen — schnell 100+ Rollen.
  • Kein Resource-Owner-Concept — RBAC sagt „Editor darf Post bearbeiten" — nicht „nur eigene Posts". Owner-Check muss separat.
  • Statisch — Bedingungen wie „nur tagsüber" passen nicht direkt.

ABAC — Attribut-basiert

Bei ABAC entscheidet eine Policy auf Basis von Attributen über die Permission. Attribute können sein:

  • Subject-Attribute — User-Rolle, Department, Klassifikations-Level, Geo-Location.
  • Resource-Attribute — Owner, Tenant, Status, Sensitivität, Erstellungs-Datum.
  • Action-Attribute — read, write, delete.
  • Environment-Attribute — Tageszeit, IP-Range, Risiko-Score.

Beispiel-Policy in natürlicher Sprache:

„Ein User darf ein Document löschen, wenn er Editor in dessen Tenant ist UND das Document den Status draft hat UND der User selbst der Author ist ODER der User die Admin-Rolle hat."

Als Pseudo-Policy:

Plain abac-policy-example.txt
allow {
  input.action == "delete"
  input.resource.type == "document"
  input.user.tenant == input.resource.tenant
  (
    input.user.role == "admin"
    OR (input.user.role == "editor"
        AND input.resource.status == "draft"
        AND input.user.id == input.resource.authorId)
  )
}

Vorteile:

  • Sehr flexibel — beliebige Bedingungen.
  • Skaliert ohne Rollen-Explosion — gleiche Policy für unterschiedliche Kontexte.
  • Externe Policy-Files — Updates ohne App-Deploy möglich.

Nachteile:

  • Komplexer — Debuggen einer Permission-Entscheidung schwerer.
  • Policy-Sprache lernen — Rego (OPA), Polar (Oso), Casbin-Conf.
  • Performance — bei komplexen Policies und großen Datasets Bottleneck möglich.

OPA / Rego als ABAC-Standard

Open Policy Agent ist die CNCF-Standard-Engine für Cloud-Native-Policy-Decisions. Sprache: Rego (deklarativ, Datalog-inspiriert).

Rego document-delete-policy.rego
package authz

default allow := false

# Admin darf alles im eigenen Tenant
allow if {
    input.action == "delete"
    input.user.role == "admin"
    input.user.tenant == input.resource.tenant
}

# Author darf eigene Drafts löschen
allow if {
    input.action == "delete"
    input.user.role == "editor"
    input.resource.status == "draft"
    input.user.id == input.resource.authorId
    input.user.tenant == input.resource.tenant
}

App-seitiger Call:

JavaScript opa-eval-from-app.js
// OPA läuft als Sidecar oder zentraler Service
async function checkPermission(user, action, resource) {
  const response = await fetch('http://opa:8181/v1/data/authz/allow', {
    method: 'POST',
    body: JSON.stringify({
      input: { user, action, resource },
    }),
  });
  const { result } = await response.json();
  return result === true;
}

app.delete('/documents/:id', async (req, res) => {
  const doc = await db.documents.findOne({ id: req.params.id });
  if (!(await checkPermission(req.user, 'delete', doc))) {
    return res.status(403).end();
  }
  // ... löschen ...
});

Wann OPA:

  • Microservices mit zentraler Policy.
  • K8s-Setups (OPA Gatekeeper).
  • Compliance-Audits mit Policy-as-Code.

Casbin — App-eingebettete RBAC/ABAC

Casbin ist eine Library (kein Daemon), in vielen Sprachen verfügbar. Einfacher Setup, RBAC und ABAC in einer DSL.

Model-Datei:

Plain casbin-model.conf
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

Policy-CSV:

Plain casbin-policy.csv
p, admin, posts, read
p, admin, posts, write
p, editor, posts, write
p, viewer, posts, read

g, alice, admin
g, bob, editor

Node.js:

JavaScript casbin-node.js
import { newEnforcer } from 'casbin';

const enforcer = await newEnforcer('model.conf', 'policy.csv');
const allowed = await enforcer.enforce('alice', 'posts', 'write');  // true

Wann Casbin:

  • Single-App-Setup mit komplexer Permission-Logik.
  • Policy soll editierbar sein (CSV oder DB-Storage).
  • Kein zusätzlicher Daemon gewünscht.

Cerbos — Self-Hosted ABAC mit YAML

Cerbos läuft als Sidecar/Service. Policy in YAML, kein DSL-Lernen.

YAML cerbos-policy.yaml
apiVersion: api.cerbos.dev/v1
resourcePolicy:
  version: default
  resource: document
  rules:
    - actions: [view]
      effect: EFFECT_ALLOW
      roles: [viewer, editor, admin]

    - actions: [edit, delete]
      effect: EFFECT_ALLOW
      roles: [editor, admin]
      condition:
        match:
          expr: request.principal.attr.tenant == request.resource.attr.tenant

    - actions: [delete]
      effect: EFFECT_ALLOW
      roles: [editor]
      condition:
        match:
          expr: |
            request.resource.attr.status == "draft" &&
            request.resource.attr.authorId == request.principal.id

Wann Cerbos:

  • YAML-affines Team.
  • Self-Hosted mit klaren API-Schnittstellen.
  • Policy-Tests als YAML im Repo.

Hybrid: RBAC mit ABAC-Conditions

In der Praxis ist die häufigste Lösung: RBAC als Grundgerüst, ABAC für die Ausnahmen.

JavaScript hybrid-rbac-abac.js
// RBAC-Basis: Rollen mit Standard-Permissions
const ROLE_PERMISSIONS = {
  viewer:  ['post:read'],
  editor:  ['post:read', 'post:write'],
  admin:   ['post:read', 'post:write', 'post:delete'],
};

// Plus ABAC-Conditions per Resource
const RESOURCE_CONDITIONS = {
  'post:delete': (user, post) => {
    // Admin darf immer, Editor nur eigene drafts
    if (user.role === 'admin') return true;
    if (user.role === 'editor') {
      return post.status === 'draft' && post.authorId === user.id;
    }
    return false;
  },
  'post:write': (user, post) => {
    return post.tenant === user.tenant;
  },
};

function can(user, action, resource = null) {
  const perms = ROLE_PERMISSIONS[user.role] || [];
  if (!perms.includes(action)) return false;
  const condition = RESOURCE_CONDITIONS[action];
  if (condition && resource) return condition(user, resource);
  return true;
}

Vorteile:

  • Standard-Pfad einfach — meiste Endpoints brauchen nur RBAC.
  • Spezial-Fälle isoliert — Resource-Owner-Checks als zusätzliche Schicht.
  • Übersichtlich — Code, kein DSL-Setup.

Stand 2026 ist das der pragmatische Mittelweg für mittelgroße Apps.

Permissions im JWT vs. Server-seitige Auflösung

Wenn Permissions Teil des JWT-Payloads sind, kann der Validator sie direkt nutzen — kein DB-Lookup. Performance-Vorteil, aber Revocation wird schwerer (siehe jwt-stateless-tokens).

JSON jwt-with-permissions.json
{
  "sub": "user-42",
  "role": "editor",
  "tenant": "acme",
  "permissions": ["post:read", "post:write"],
  "exp": 1700000000
}

Trade-offs:

  • Permission-Wechsel wirkt erst nach Token-Ablauf — bei 15-min-Access-Tokens akzeptabel.
  • Token-Size wächst — bei vielen Permissions wird der JWT groß. Bei mehr als ~50 Permissions: nur Rolle/Group im Token, Permissions server-seitig aus Rolle ableiten.
  • Stateless — JWT-Validator braucht keinen Permission-Store.

Konsens: Bei kleinen Permission-Sets im JWT halten; bei großen nur Rolle/Group im Token, Permission-Resolution server-seitig.

Entscheidungs-Faustregeln

SituationEmpfehlung
Kleine App, ~5 Rollen, simple PermissionsRBAC im Code, keine Engine
Mittlere App, ~10–30 Rollen, einzelne ConditionsRBAC + ABAC-Conditions, Casbin oder Eigenbau
Komplexe Conditions, ~5+ Resource-TypenCerbos oder OPA
Microservices mit zentraler PolicyOPA als Sidecar
Sharing-Modell (Drive-/Notion-artig)ReBAC (OpenFGA, SpiceDB)
K8s-Workload-PolicyOPA Gatekeeper
Compliance (Auditable Policy-Files)OPA mit Rego-Tests, oder Cerbos mit YAML-Tests

Migrations-Pfad: mit RBAC starten. Wenn Bedingungen wachsen, RBAC-Code um Conditions erweitern (Hybrid). Wenn Conditions > ~30: Engine evaluieren. Selten muss man von Anfang an mit OPA starten.

Besonderheiten

Rollen-Hierarchie als Inheritance

Klassisches Pattern: Admin erbt von Editor erbt von Viewer. Spart Duplikate. In Casbin direkt unterstützt (g2-Definition für Role-Hierarchy); in eigenem Code per Rekursion bei Permission-Resolution. Vorsicht vor Zykeln in der Hierarchie — wenn A erbt von B und B erbt von A — Endlos-Loop.

Permissions als Bitmask für extreme Performance

Bei sehr großen User-/Permission-Mengen: Permission-Set als Bitmask im User-Record. permissions BIGINT mit jedem Bit für eine Permission. Check: (user.permissions & PERM_BIT) != 0. Sehr schnell, aber unflexibel (max 64 Permissions in BIGINT, mehr braucht BYTEA). Eher Spezialfall in Performance-kritischen Systemen.

Owner-only ist eine eigene Permission-Klasse

„Nur der Owner einer Resource darf X" — taucht in jeder zweiten App auf. Sauberes Pattern: Resource-Owner als spezielle (synthetische) Rolle pro Resource-Instance. Permission-Check fragt: „Hat User in dieser Resource die Owner-Rolle ODER eine globale Permission, die Owner-Permission impliziert?". Vermeidet ständiges Code-Pattern „if author == user OR admin".

Policy-as-Code-Tests in CI

Wenn Policies in OPA/Rego oder Cerbos/YAML liegen, lassen sie sich als Code testen — Test-Inputs (User, Resource, Action) plus erwartete Decisions. CI-Integration: bei jeder Policy-Änderung Tests laufen lassen. Verhindert „kleine Policy-Änderung bricht 5 andere Endpoints".

"Allow with Justification" für Audit-Trail

Bei sehr sensitiven Aktionen: nicht nur „allow / deny", sondern „allow if user gives reason". Reason wird im Audit-Log gespeichert. Patient-Datenzugriff (Healthcare), Datenbank-Admin-Eingriffe — alles Use-Cases. Casbin/OPA können Justifications als Input akzeptieren und an Audit-Log weiterreichen.

Negative Permissions (Deny) als Anti-Pattern

Pattern „Role X hat alle Permissions außer Y" mit expliziten Deny-Einträgen ist verlockend, aber schwer zu reasonen. Konflikte (Allow von Rolle A, Deny von Rolle B) brauchen Konflikt-Auflösungs-Regeln. Sauberer: Allow-only-Modell. Wer Y nicht darf, kriegt Y nicht in der Permission-Liste.

Permission-Caching mit Invalidierungs-Plan

Permissions pro Request aus DB zu laden ist DB-Last. Cache (Redis, Memory) hilft — verschiebt aber Revocation-Probleme. Pattern: User-Permission-Hash als Cache-Key, TTL kurz (60s), bei Permission-Wechsel: Cache-Eintrag invalidieren UND Counter im User-Record erhöhen → JWT mit altem Counter wird abgewiesen.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Autorisierung & Access Control

Zur Übersicht