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:
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-ZugriffPermissions als Wertebereich: typisch <resource>:<action>-Strings (post:read, post:write, user:manage).
Implementierung (vereinfacht):
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
drafthat UND der User selbst der Author ist ODER der User die Admin-Rolle hat."
Als Pseudo-Policy:
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).
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:
// 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:
[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.actPolicy-CSV:
p, admin, posts, read
p, admin, posts, write
p, editor, posts, write
p, viewer, posts, read
g, alice, admin
g, bob, editorNode.js:
import { newEnforcer } from 'casbin';
const enforcer = await newEnforcer('model.conf', 'policy.csv');
const allowed = await enforcer.enforce('alice', 'posts', 'write'); // trueWann 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.
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.idWann 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.
// 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).
{
"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
| Situation | Empfehlung |
|---|---|
| Kleine App, ~5 Rollen, simple Permissions | RBAC im Code, keine Engine |
| Mittlere App, ~10–30 Rollen, einzelne Conditions | RBAC + ABAC-Conditions, Casbin oder Eigenbau |
| Komplexe Conditions, ~5+ Resource-Typen | Cerbos oder OPA |
| Microservices mit zentraler Policy | OPA als Sidecar |
| Sharing-Modell (Drive-/Notion-artig) | ReBAC (OpenFGA, SpiceDB) |
| K8s-Workload-Policy | OPA 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
- NIST — Policy Machine (ABAC-Reference)
- OPA Documentation
- Rego Language
- Casbin Documentation
- Cerbos Documentation
- Oso (Polar Language)
- OpenFGA Documentation