Authentifizierung sagt dir, wer jemand ist. Autorisierung sagt dir, was diese Person darf. Und hier wird es kompliziert: Ein User kann Admin sein, aber nur für bestimmte Ressourcen. Ein API-Key kann Lesezugriff haben, aber nur auf einen Tenant. Ein Service kann alles – außer Finanzdaten.
Dieser Artikel hilft dir, ein Autorisierungsmodell zu wählen, das flexibel genug für deine Anforderungen ist, aber einfach genug, um es zu verstehen und zu testen.
Zielbild
Nach diesem Artikel kannst du:
- Zwischen RBAC, ABAC und Hybrid begründet wählen
- Scopes und Permissions sinnvoll strukturieren
- BOLA (Broken Object Level Authorization) verhindern
- Tenant-Isolation in Multi-Tenant-Systemen umsetzen
- Least Privilege als Default etablieren
- Admin-Zugriffe und Impersonation sicher gestalten
Kernfragen
Bevor du weiterliest, versuche diese Fragen für dein Projekt zu beantworten:
- RBAC oder ABAC? Reichen Rollen, oder brauchst du Attribute?
- Wie granular sind Permissions? Pro Ressourcentyp? Pro Aktion?
- Wie prüft ihr Resource-Level-Zugriff? Gehört Order X zu User Y?
- Wie isoliert ihr Tenants? Datenbank? Row-Level? API-Layer?
- Wer darf alles? Gibt es Super-Admins? Wie kontrolliert ihr die?
Autorisierungsmodelle
Drei Hauptansätze haben sich etabliert, mit unterschiedlicher Komplexität und Flexibilität.
RBAC (Role-Based Access Control)
Benutzer haben Rollen, Rollen haben Permissions.
Beispiel:
{
"user_id": "user_alice",
"roles": [
"editor"
],
"permissions": [
"orders:read",
"orders:write",
"products:read"
]
}
Vorteile:
- Einfach zu verstehen und zu implementieren
- Gut für Audit (Wer hat welche Rolle?)
- Standardisiert in vielen Frameworks
Nachteile:
- Role Explosion bei komplexen Anforderungen
- Keine dynamischen Regeln (nur eigene Orders)
- Schwer, Ausnahmen zu modellieren
Wann verwenden:
- Klare, stabile Rollenstruktur
- Wenige Rollen (< 20)
- Keine ressourcenspezifischen Regeln
ABAC (Attribute-Based Access Control)
Entscheidungen basieren auf Attributen von User, Ressource und Kontext.
Policy: "User kann Order lesen, wenn Order.tenant_id == User.tenant_id"
Request:
User: { tenant_id: "tenant_a", role: "viewer" }
Resource: { type: "order", tenant_id: "tenant_a" }
Action: "read"
Decision: ALLOW
Vorteile:
- Sehr flexibel
- Dynamische Regeln möglich
- Keine Role Explosion
Nachteile:
- Komplexer zu implementieren und zu testen
- Schwerer zu auditieren
- Performance bei vielen Attributen
Wann verwenden:
- Komplexe, dynamische Anforderungen
- Multi-Tenant mit feingranularen Regeln
- Wenn RBAC zu viele Rollen erzeugt
Hybrid (RBAC + ABAC)
Kombiniert Rollen für grobe Einteilung mit Attributen für feine Kontrolle.
Vorteile:
- Beste aus beiden Welten
- Rollen für Übersicht, Attribute für Details
- Pragmatisch für die meisten Systeme
Empfehlung: Hybrid als Default. Rollen für API-Scopes, Attribute für Resource-Level-Checks.
Entscheidungsmatrix
| Kriterium | RBAC | ABAC | Hybrid |
|---|---|---|---|
| Komplexität | Niedrig | Hoch | Mittel |
| Flexibilität | Niedrig | Hoch | Hoch |
| Auditierbarkeit | Hoch | Mittel | Mittel |
| Multi-Tenant | Schwer | Einfach | Einfach |
| Performance | Hoch | Mittel | Mittel |
Scopes und Permissions
Scopes definieren, was ein Token darf. Permissions sind die feingranularen Rechte dahinter.
Scope-Namenskonvention
resource:action
Beispiele:
| Scope | Bedeutung |
|---|---|
orders:read |
Orders lesen |
orders:write |
Orders erstellen/ändern |
orders:delete |
Orders löschen |
users:read |
User-Profile lesen |
users:admin |
User verwalten |
* |
Alles (vermeiden!) |
Scope-Hierarchie
Manchmal macht eine Hierarchie Sinn:
Empfehlung: Explizite Scopes ohne implizite Hierarchie. Einfacher zu verstehen und zu testen.
Scope vs. Permission
| Konzept | Wo definiert | Beispiel |
|---|---|---|
| Scope | Im Token (OAuth2) | orders:read |
| Permission | Im System (RBAC) | can_read_orders |
| Policy | Im Code (ABAC) | order.tenant_id == user.tenant_id |
Flow:
Resource-Level Authorization
Der häufigste Fehler: API prüft nur Darf User Orders lesen?, nicht Darf User diese spezifische Order lesen?.
BOLA (Broken Object Level Authorization)
OWASP API Security #1: Zugriff auf fremde Ressourcen durch ID-Manipulation.
GET /v1/orders/order_123
Authorization: Bearer token_of_user_a
Wenn order_123 zu User B gehört, muss die API das ablehnen.
Resource-Level-Check implementieren
# FALSCH: Nur Scope prüfen
def get_order(order_id, user):
if not user.has_scope("orders:read"):
raise ForbiddenError()
return Order.find(order_id) # Jede Order!
# RICHTIG: Scope + Ownership prüfen
def get_order(order_id, user):
if not user.has_scope("orders:read"):
raise ForbiddenError()
order = Order.find(order_id)
if order.tenant_id != user.tenant_id:
raise ForbiddenError()
return order
Wichtig: Bei fehlender Berechtigung 403 Forbidden zurückgeben.
404 Not Found nur, wenn die Ressource nicht existiert. Information Hiding
ersetzt keine Autorisierung.
Ownership-Patterns
| Pattern | Prüfung | Use Case |
|---|---|---|
| Direct Ownership | resource.user_id == user.id |
Persönliche Daten |
| Tenant Ownership | resource.tenant_id == user.tenant_id |
Multi-Tenant |
| Team Membership | user.id IN resource.team.members |
Kollaboration |
| Hierarchical | resource.org_id IN user.accessible_orgs |
Org-Hierarchie |
Query-Level Filtering
Für List-Endpoints: Filter direkt in der Query, nicht nachträglich.
-- FALSCH: Alle laden, dann filtern (N+1, Memory)
SELECT *
FROM orders;
-- Dann in Code: orders.filter(o => o.tenant_id == user.tenant_id)
-- RICHTIG: Direkt filtern
SELECT *
FROM orders
WHERE tenant_id = $1;
Tenant-Isolation
In Multi-Tenant-Systemen ist Isolation kritisch. Ein Tenant darf nie Daten eines anderen sehen.
Isolation-Strategien
| Strategie | Isolation | Komplexität | Use Case |
|---|---|---|---|
| Separate Databases | Höchste | Hoch | Compliance, Enterprise |
| Separate Schemas | Hoch | Mittel | Große Tenants |
| Row-Level (tenant_id) | Mittel | Niedrig | SaaS Standard |
| API-Layer (zusätzlich) | Niedrig | Niedrig | Low-Risk, interne Apps |
Row-Level Security
PostgreSQL-Beispiel:
-- Policy erstellen
CREATE
POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id'));
-- Vor jeder Query
SET
app.tenant_id = 'tenant_abc';
-- Query ist automatisch gefiltert
SELECT *
FROM orders; -- Nur Orders von tenant_abc
Tenant-Context setzen
# Middleware: Tenant aus Token extrahieren
def tenant_middleware(request):
token = decode_jwt(request.headers["Authorization"])
request.tenant_id = token["tenant_id"]
# Für DB-Queries
db.execute("SELECT set_config('app.tenant_id', $1, true)", [request.tenant_id])
# In Handlern: tenant_id ist immer gesetzt
def list_orders(request):
# Query ist automatisch auf Tenant gefiltert
return Order.all()
Cross-Tenant-Zugriff
Manchmal nötig (z.B. Support, Admin). Dann:
- Explizite Flag:
X-Tenant-Override: tenant_xyz - Eigener Scope:
admin:cross-tenant - Audit-Log: Jeder Cross-Tenant-Zugriff wird geloggt
- Zeitlich begrenzt: Override gilt nur für diesen Request
Least Privilege
Default sollte kein Zugriff sein, nicht voller Zugriff.
Prinzipien
- Deny by Default: Ohne explizite Erlaubnis → Deny
- Minimale Scopes: Nur was nötig ist, nicht zur Sicherheit mehr
- Keine God-Tokens: Kein
*oderadmin:all - Scope-Reduktion: User kann Token mit weniger Scopes anfordern
Scope-Reduktion
Client fordert nur benötigte Scopes an:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&scope=orders:read products:read
Auch wenn Client mehr Scopes haben könnte, bekommt er nur die angefragten.
Default-Rollen
| Rolle | Scopes | Use Case |
|---|---|---|
| viewer | orders:read, products:read, users:read |
Nur lesen |
| editor | viewer + orders:write, products:write |
Lesen + Schreiben |
| admin | editor + orders:delete, users:write, users:admin |
Alles außer System |
| owner | admin + tenant:manage |
Tenant-Owner |
Regel: Neue User bekommen viewer, nicht admin.
Admin-Zugriffe und Break-Glass
Admins brauchen manchmal erweiterten Zugriff. Das muss kontrolliert sein.
Admin-Levels
| Level | Zugriff | Kontrolle |
|---|---|---|
| Tenant-Admin | Alles im eigenen Tenant | Normal |
| Support | Lesen in allen Tenants | Audit-Log |
| Super-Admin | Alles überall | Audit + Approval |
| Break-Glass | Notfall-Zugriff | Alarm + Post-Mortem |
Break-Glass-Prozess
Für Notfälle, wenn normale Wege nicht funktionieren:
- Anfrage: Admin dokumentiert Grund
- Approval: Zweite Person genehmigt (oder Auto-Approve mit Alert)
- Zeitfenster: Zugriff gilt nur X Minuten
- Audit: Alle Aktionen werden geloggt
- Review: Post-Mortem nach Nutzung
{
"type": "break_glass",
"admin_id": "admin_alice",
"reason": "Customer data recovery - Ticket #12345",
"approved_by": "admin_bob",
"valid_until": "2026-01-11T15:30:00Z",
"actions_logged": true
}
Impersonation
Admin agiert als anderer User. Nützlich für Support, aber riskant.
Regeln:
- Eigener Scope:
admin:impersonate - Audit-Trail: Wer hat wen wann impersoniert
- Sichtbar im Token:
impersonated_by: admin_alice - Eingeschränkt: Keine sensitiven Aktionen (Passwort ändern)
- Zeitlich begrenzt: Max. 1 Stunde
{
"sub": "user_bob",
"impersonated_by": "admin_alice",
"impersonation_reason": "Support ticket #12345",
"exp": 1706522400
}
Audit-Log:
{
"timestamp": "2026-01-11T14:30:00Z",
"action": "impersonation_start",
"admin_id": "admin_alice",
"target_user_id": "user_bob",
"reason": "Support ticket #12345",
"ip": "192.168.1.100"
}
Policy Testing
Autorisierungsregeln müssen getestet werden wie Code.
Test-Matrix
| User | Ressource | Aktion | Erwartung |
|---|---|---|---|
| user_a (tenant_a) | order_1 (tenant_a) | read | ALLOW |
| user_a (tenant_a) | order_2 (tenant_b) | read | DENY |
| admin (tenant_a) | order_1 (tenant_a) | delete | ALLOW |
| viewer (tenant_a) | order_1 (tenant_a) | delete | DENY |
Unit Tests
def test_user_can_read_own_tenant_order():
user = User(tenant_id="tenant_a", roles=["viewer"])
order = Order(tenant_id="tenant_a")
assert can_access(user, order, "read") == True
def test_user_cannot_read_other_tenant_order():
user = User(tenant_id="tenant_a", roles=["viewer"])
order = Order(tenant_id="tenant_b")
assert can_access(user, order, "read") == False
def test_viewer_cannot_delete():
user = User(tenant_id="tenant_a", roles=["viewer"])
order = Order(tenant_id="tenant_a")
assert can_access(user, order, "delete") == False
Integration Tests
def test_api_returns_403_for_other_tenant_order():
# User A's token
token = get_token(user="user_a", tenant="tenant_a")
# Order belongs to tenant_b
order_id = create_order(tenant="tenant_b")
response = client.get(
f"/v1/orders/{order_id}",
headers={"Authorization": f"Bearer {token}"}
)
# 403 bei fehlender Berechtigung
assert response.status_code == 403
Regeln & Anti-Patterns
Do
- Hybrid-Modell: RBAC für Scopes, ABAC für Resource-Level
- Resource-Level-Checks bei jedem Zugriff
- 403 bei fehlender Berechtigung, 404 nur bei nicht existenter Ressource
- Tenant-ID in jeder Query (Row-Level Security)
- Least Privilege als Default
- Admin-Aktionen auditieren
- Impersonation zeitlich begrenzen und loggen
- Policy-Tests für kritische Regeln
Don't
- Nur Scope prüfen, nicht Ownership
- God-Scopes (
*,admin:all) - Tenant-Filtering nur im Anwendungscode bei hohen Isolation-Anforderungen
- Neue User mit Admin-Rechten
- Impersonation ohne Audit-Trail
- Break-Glass ohne Approval-Prozess
- Policies ohne Tests
Artefakt: Policy-Matrix
# Autorisierungs-Policy
## Rollen
| Rolle | Beschreibung | Default-Scopes |
|--------|-----------------------|--------------------------------------------------------|
| viewer | Nur Lesen | `orders:read`, `products:read`, `users:read` |
| editor | Lesen + Schreiben | viewer + `orders:write`, `products:write` |
| admin | Tenant-Administration | editor + `orders:delete`, `users:write`, `users:admin` |
| owner | Tenant-Owner | admin + `tenant:manage` |
## Scopes
| Scope | Beschreibung |
|-----------------|-------------------------|
| `orders:read` | Orders lesen |
| `orders:write` | Orders erstellen/ändern |
| `orders:delete` | Orders löschen |
| `users:read` | User-Profile lesen |
| `users:write` | User-Profile ändern |
| `users:admin` | User erstellen/löschen |
| `tenant:manage` | Tenant-Einstellungen |
## Resource-Level-Policies
| Ressource | Policy |
|-----------|----------------------------------------------------------------------------|
| Order | `order.tenant_id == user.tenant_id` |
| User | `user.tenant_id == current_user.tenant_id` OR `user.id == current_user.id` |
| Product | `product.tenant_id == user.tenant_id` OR `product.is_public` |
| Invoice | `invoice.tenant_id == user.tenant_id` AND `user.has_scope('billing:read')` |
## Zugriffsmatrix
| Aktion | viewer | editor | admin | owner |
|-----------------|--------|--------|-------|-------|
| Order lesen | ✓ | ✓ | ✓ | ✓ |
| Order erstellen | ✗ | ✓ | ✓ | ✓ |
| Order löschen | ✗ | ✗ | ✓ | ✓ |
| User einladen | ✗ | ✗ | ✓ | ✓ |
| User löschen | ✗ | ✗ | ✗ | ✓ |
| Tenant-Settings | ✗ | ✗ | ✗ | ✓ |
## Admin-Zugriffe
### Support-Zugriff
- Scope: `support:read`
- Erlaubt: Lesen in allen Tenants
- Audit: Jeder Zugriff geloggt
- Einschränkung: Keine Schreibaktionen
### Impersonation
- Scope: `admin:impersonate`
- Zeitlimit: 1 Stunde
- Audit: Start, Ende, alle Aktionen
- Verboten: Passwort ändern, Rollen ändern, Billing
### Break-Glass
- Trigger: Security-Incident, kritischer Bug
- Approval: Zweite Admin-Person
- Zeitlimit: 30 Minuten
- Alarm: Sofort an Security-Team
- Review: Pflicht-Post-Mortem
## Test-Szenarien
| # | User | Ressource | Aktion | Ergebnis |
|---|-----------------|----------------|--------|---------------|
| 1 | viewer@tenant_a | order@tenant_a | read | ALLOW |
| 2 | viewer@tenant_a | order@tenant_b | read | DENY (403) |
| 3 | editor@tenant_a | order@tenant_a | write | ALLOW |
| 4 | viewer@tenant_a | order@tenant_a | delete | DENY (403) |
| 5 | admin@tenant_a | user@tenant_a | delete | ALLOW |
| 6 | admin@tenant_a | user@tenant_b | delete | DENY (403) |
| 7 | support | order@any | read | ALLOW + Audit |
| 8 | support | order@any | write | DENY (403) |
Checkliste
Bevor du zum nächsten Artikel gehst, prüfe:
- [ ] Autorisierungsmodell ist gewählt (RBAC, ABAC, Hybrid)
- [ ] Scopes sind definiert und dokumentiert
- [ ] Resource-Level-Checks sind implementiert
- [ ] BOLA ist verhindert (Ownership-Prüfung)
- [ ] Tenant-Isolation ist implementiert
- [ ] Least Privilege ist Default
- [ ] Admin-Zugriffe sind kontrolliert und auditiert
- [ ] Impersonation ist zeitlich begrenzt und geloggt
- [ ] Break-Glass-Prozess existiert
- [ ] Policy-Tests existieren für kritische Regeln
- [ ] Policy-Matrix ist dokumentiert
Wie es weitergeht
Im nächsten Teil geht es um Transport und Kryptografie: TLS-Konfiguration, Zertifikatsmanagement und wann mTLS für interne Services sinnvoll ist.
Alle Teile der Serie: Serie: API-Design