TS
Thomas Schmitz

Freiberuflicher IT-Berater, Softwareentwickler & DevOps. Praxisnahe Artikel zu API-Design, Architektur und Cloud-Workflows.

Serie: API-Design · Teil 10 von 22

API-Design Teil 10: Autorisierung (AuthZ)

RBAC, ABAC oder Hybrid? Wie du Berechtigungen modellierst, BOLA verhinderst und Least Privilege umsetzt.

Praxisnah Checkliste

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:

  1. RBAC oder ABAC? Reichen Rollen, oder brauchst du Attribute?
  2. Wie granular sind Permissions? Pro Ressourcentyp? Pro Aktion?
  3. Wie prüft ihr Resource-Level-Zugriff? Gehört Order X zu User Y?
  4. Wie isoliert ihr Tenants? Datenbank? Row-Level? API-Layer?
  5. 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.

User \alice\

Role \editor\

Permissions

orders:read

orders:write

products:read

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.

RBAC: Hat User die Rolle editor?

ABAC: Gehört die Order zum Tenant des Users?

Decision: ALLOW

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:

impliziert?

orders:*

orders:read

orders:write

orders:delete

Entscheidung dokumentieren

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:

Token hat Scope orders:read

User hat Permission can_read_orders (via Role)

Policy: Order gehört zum Tenant des Users

ALLOW

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:

  1. Explizite Flag: X-Tenant-Override: tenant_xyz
  2. Eigener Scope: admin:cross-tenant
  3. Audit-Log: Jeder Cross-Tenant-Zugriff wird geloggt
  4. Zeitlich begrenzt: Override gilt nur für diesen Request

Least Privilege

Default sollte kein Zugriff sein, nicht voller Zugriff.

Prinzipien

  1. Deny by Default: Ohne explizite Erlaubnis → Deny
  2. Minimale Scopes: Nur was nötig ist, nicht zur Sicherheit mehr
  3. Keine God-Tokens: Kein * oder admin:all
  4. 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:

  1. Anfrage: Admin dokumentiert Grund
  2. Approval: Zweite Person genehmigt (oder Auto-Approve mit Alert)
  3. Zeitfenster: Zugriff gilt nur X Minuten
  4. Audit: Alle Aktionen werden geloggt
  5. 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:

  1. Eigener Scope: admin:impersonate
  2. Audit-Trail: Wer hat wen wann impersoniert
  3. Sichtbar im Token: impersonated_by: admin_alice
  4. Eingeschränkt: Keine sensitiven Aktionen (Passwort ändern)
  5. 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

Mehr Beiträge aus dem Blog.