Erweiterte rollenbasierte Zugriffskontrolle in PostgreSQL: Row-Level Security und Richtlinienmanagement

Nutzen Sie PostgreSQL-Zeilen-Sicherheit mit RBAC-Richtlinien für Mandantenisolierung, Eigentümerprüfungen und sichereren Anwendungszugriff.

Erweiterte PostgreSQL-Rollenbasierte Zugriffskontrolle: Zeilen-Sicherheit und Richtlinienverwaltung

PostgreSQL-Zeilen-Sicherheit (RLS) ermöglicht Ihrer Datenbank, Zeilen basierend auf der aktuellen Rolle und dem Sitzungskontext zu filtern. Wenn Ihre Anwendung mehrere Mandanten bedient oder Admin-, Manager- und normale Benutzerzugriffe mischt, kann RLS verhindern, dass ein vergessenes WHERE tenant_id = ... zu einem Datenleck wird.

RLS ersetzt keine Anwendungsautorisierung. Es bietet ein Sicherheitsnetz auf Datenbankebene für die sensibelste Grenze: welche Zeilen eine Verbindung lesen oder schreiben kann.

Warum Zeilen-Sicherheit?

Probleme des traditionellen Ansatzes

Anwendungsschicht-Filterung:

# Jede Abfrage muss sich merken zu filtern
query = "SELECT * FROM documents WHERE tenant_id = %s"
ergebnisse = db.execute(query, [aktueller_benutzer_mandanten_id])

Probleme:

  • Leicht vergessene Filter (Sicherheitslücken)
  • Code-Duplizierung in der gesamten Anwendung
  • Kein Schutz für direkten Datenbankzugriff
  • Komplexe Prüfanforderungen

Vorteile der Zeilen-Sicherheit

  • Automatische Durchsetzung: Richtlinien werden transparent angewendet
  • Zentralisierte Regeln: Sicherheitslogik an einem Ort
  • Mandantenisolierung: Perfekt für SaaS-Anwendungen
  • Prüfkonformität: Integrierte Sicherheitsgarantien
  • Leistung: Effiziente Abfrageplanung

Grundlegende RLS-Einrichtung

Schritt 1: RLS auf Tabelle aktivieren

CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200),
    content TEXT,
    owner_id INTEGER NOT NULL,
    department VARCHAR(50),
    is_public BOOLEAN DEFAULT false,
    created_at TIMESTAMP DEFAULT NOW()
);

-- Zeilen-Sicherheit aktivieren
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

Schritt 2: Rollen erstellen

-- Verschiedene Benutzerrollen erstellen
CREATE ROLE regular_user;
CREATE ROLE department_manager;
CREATE ROLE admin;

-- Tabellenzugriff gewähren
GRANT SELECT, INSERT, UPDATE, DELETE ON documents TO regular_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON documents TO department_manager;
GRANT ALL ON documents TO admin;

Schritt 3: Richtlinien erstellen

-- Richtlinie: Benutzer können ihre eigenen Dokumente sehen
CREATE POLICY user_own_documents ON documents
    FOR SELECT
    TO regular_user
    USING (owner_id = current_setting('app.user_id')::INTEGER);

-- Richtlinie: Benutzer können Dokumente einfügen, die ihnen gehören
CREATE POLICY user_insert_own ON documents
    FOR INSERT
    TO regular_user
    WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);

-- Richtlinie: Benutzer können ihre eigenen Dokumente aktualisieren
CREATE POLICY user_update_own ON documents
    FOR UPDATE
    TO regular_user
    USING (owner_id = current_setting('app.user_id')::INTEGER)
    WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);

Schritt 4: Sitzungsvariablen setzen

Im Anwendungscode:

import psycopg2

# Als Rolle verbinden
conn = psycopg2.connect(
    host="localhost",
    database="mydb",
    user="regular_user",
    password="password"
)

# Benutzerkontext setzen
cursor = conn.cursor()
cursor.execute("SELECT set_config('app.user_id', %s, false)", [str(aktueller_benutzer_id)])

# Abfragen werden jetzt automatisch gefiltert
cursor.execute("SELECT * FROM documents")
# Gibt nur Dokumente zurück, bei denen owner_id = aktueller_benutzer_id

Multi-Mandanten-SaaS-Anwendung

Vollständige Multi-Mandanten-Einrichtung

-- Mandantentabelle erstellen
CREATE TABLE tenants (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

-- Benutzertabelle erstellen
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    tenant_id INTEGER REFERENCES tenants(id),
    role VARCHAR(20) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

-- Anwendungsdatentabelle erstellen
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL REFERENCES tenants(id),
    customer_name VARCHAR(100),
    amount DECIMAL(10,2),
    status VARCHAR(20),
    created_by INTEGER REFERENCES users(id),
    created_at TIMESTAMP DEFAULT NOW()
);

-- RLS aktivieren
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Richtlinie: Benutzer sehen nur Bestellungen ihres Mandanten
CREATE POLICY tenant_isolation ON orders
    FOR ALL
    TO public
    USING (tenant_id = current_setting('app.tenant_id')::INTEGER)
    WITH CHECK (tenant_id = current_setting('app.tenant_id')::INTEGER);

Anwendungsintegration

Django-Beispiel:

from django.db import connection

class TenantMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.user.is_authenticated:
            with connection.cursor() as cursor:
                cursor.execute(
                    "SELECT set_config('app.tenant_id', %s, false)",
                    [str(request.user.tenant_id)]
                )
        
        response = self.get_response(request)
        return response

Node.js-Beispiel:

const { Pool } = require('pg');
const pool = new Pool();

async function setTenantContext(tenantId) {
    const client = await pool.connect();
    try {
        await client.query('SELECT set_config($1, $2, false)', ['app.tenant_id', String(tenantId)]);
        return client;
    } catch (err) {
        client.release();
        throw err;
    }
}

// Verwendung
app.use(async (req, res, next) => {
    if (req.user) {
        req.dbClient = await setTenantContext(req.user.tenantId);
    }
    next();
});

Erweiterte Richtlinienmuster

1. Hierarchische Zugriffskontrolle

-- Benutzer können Dokumente ihrer Abteilung und öffentliche sehen
CREATE POLICY department_access ON documents
    FOR SELECT
    TO regular_user
    USING (
        department = current_setting('app.user_department', true)
        OR is_public = true
        OR owner_id = current_setting('app.user_id')::INTEGER
    );

2. Zeitbasierter Zugriff

CREATE TABLE subscriptions (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL,
    content TEXT,
    valid_from TIMESTAMP NOT NULL,
    valid_until TIMESTAMP NOT NULL
);

ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY;

-- Richtlinie: Benutzer können nur aktive Abonnements sehen
CREATE POLICY active_subscriptions ON subscriptions
    FOR SELECT
    TO public
    USING (
        user_id = current_setting('app.user_id')::INTEGER
        AND NOW() BETWEEN valid_from AND valid_until
    );

3. Rollenbasierter Zugriff mit komplexer Logik

CREATE TABLE projects (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    owner_id INTEGER,
    team_id INTEGER,
    visibility VARCHAR(20) -- 'private', 'team', 'public'
);

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- Komplexe Sichtbarkeitsrichtlinie
CREATE POLICY project_visibility ON projects
    FOR SELECT
    TO public
    USING (
        CASE current_setting('app.user_role', true)
            WHEN 'admin' THEN true
            WHEN 'manager' THEN (
                team_id = current_setting('app.user_team_id')::INTEGER
                OR visibility = 'public'
            )
            ELSE (
                owner_id = current_setting('app.user_id')::INTEGER
                OR visibility = 'public'
            )
        END
    );

4. Gemeinsame Zugriffslisten

CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200),
    owner_id INTEGER NOT NULL
);

CREATE TABLE document_shares (
    document_id INTEGER REFERENCES documents(id),
    shared_with_user_id INTEGER,
    permission VARCHAR(20), -- 'read', 'write'
    PRIMARY KEY (document_id, shared_with_user_id)
);

ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

-- Richtlinie: Benutzer können eigene oder geteilte Dokumente sehen
CREATE POLICY document_access ON documents
    FOR SELECT
    TO public
    USING (
        owner_id = current_setting('app.user_id')::INTEGER
        OR id IN (
            SELECT document_id 
            FROM document_shares 
            WHERE shared_with_user_id = current_setting('app.user_id')::INTEGER
        )
    );

-- Richtlinie: Benutzer können eigene Dokumente oder solche mit Schreibberechtigung ändern
CREATE POLICY document_modify ON documents
    FOR UPDATE
    TO public
    USING (
        owner_id = current_setting('app.user_id')::INTEGER
        OR id IN (
            SELECT document_id 
            FROM document_shares 
            WHERE shared_with_user_id = current_setting('app.user_id')::INTEGER
              AND permission = 'write'
        )
    );

Administrativer Zugriff und RLS

Tabellenbesitzer und Superuser können RLS umgehen, es sei denn, Sie erzwingen es. Behandeln Sie administrative Pfade als separate Designentscheidung, nicht als nachträglichen Gedanken.

Option 1: BYPASSRLS-Rollenattribut

CREATE ROLE admin_user WITH LOGIN PASSWORD 'secure_password';
ALTER ROLE admin_user BYPASSRLS;

Verwenden Sie dies sparsam. BYPASSRLS ist mächtig und sollte nicht normalen Anwendungsrollen gewährt werden.

Option 2: Permissive Richtlinien für Admins

-- Permissive Richtlinie für Admin-Rolle erstellen
CREATE POLICY admin_all_access ON documents
    FOR ALL
    TO admin
    USING (true)
    WITH CHECK (true);

Option 3: RLS für Tabellenbesitzer erzwingen

ALTER TABLE documents FORCE ROW LEVEL SECURITY;

FORCE ROW LEVEL SECURITY zwingt auch Tabellenbesitzer, Richtlinien zu befolgen. Das ist normalerweise sicherer für anwendungsbesessene Tabellen. Verwenden Sie für Wartungsarbeiten eine separate kontrollierte Rolle, anstatt RLS in einem Ad-hoc-Skript zu deaktivieren.

Richtlinientypen: USING vs WITH CHECK

USING-Klausel

Steuert, welche vorhandenen Zeilen sichtbar/änderbar sind:

CREATE POLICY select_own ON documents
    FOR SELECT
    USING (owner_id = current_setting('app.user_id')::INTEGER);

-- Benutzer kann nur Zeilen SELECTieren, bei denen owner_id übereinstimmt

WITH CHECK-Klausel

Steuert, welche neuen/aktualisierten Zeilen erlaubt sind:

CREATE POLICY insert_own ON documents
    FOR INSERT
    WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);

-- Benutzer kann nur Zeilen INSERTieren, bei denen owner_id übereinstimmt

Kombinierte Richtlinien

CREATE POLICY update_own ON documents
    FOR UPDATE
    USING (owner_id = current_setting('app.user_id')::INTEGER)
    WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);

Leistungsüberlegungen

1. Richtlinien-Spalten indizieren

-- Indizes für in Richtlinien verwendete Spalten erstellen
CREATE INDEX idx_documents_owner ON documents(owner_id);
CREATE INDEX idx_documents_tenant ON documents(tenant_id);
CREATE INDEX idx_documents_department ON documents(department);

2. Abfragepläne analysieren

EXPLAIN ANALYZE
SELECT * FROM documents
WHERE title LIKE '%report%';

-- Prüfen, ob der Richtlinienfilter effizient angewendet wird

3. Vorsicht bei Unterabfragen in Richtlinien

Unterabfragen sind manchmal das richtige Modell für gemeinsamen Zugriff, benötigen aber Indizes und Tests. Eine Richtlinie, die für jede Kandidatenzeile eine andere Tabelle abfragt, kann teuer werden.

Riskant ohne Indizes:

CREATE POLICY slow_policy ON documents
    USING (
        id IN (
            SELECT document_id
            FROM shares
            WHERE user_id = current_setting('app.user_id')::INTEGER
        )
    );

Oft klarer mit unterstützenden Indizes:

CREATE POLICY faster_policy ON documents
    USING (
        EXISTS (
            SELECT 1 FROM shares 
            WHERE document_id = documents.id 
              AND user_id = current_setting('app.user_id')::INTEGER
        )
    );

Erstellen Sie Indizes wie shares(user_id, document_id) und überprüfen Sie den Plan mit EXPLAIN (ANALYZE, BUFFERS).

Überwachung und Prüfung

Aktive Richtlinien anzeigen

-- Alle RLS-Richtlinien auflisten
SELECT 
    schemaname,
    tablename,
    policyname,
    permissive,
    roles,
    cmd,
    qual,
    with_check
FROM pg_policies
ORDER BY tablename, policyname;

Richtlinienänderungen prüfen

Verfolgen Sie Richtlinien-DDL in Migrationen und Datenbank-Prüfprotokollen. Wenn Ihre Umgebung Ereignis-Trigger verwendet, testen Sie diese sorgfältig gegen Ihre PostgreSQL-Version und schließen Sie CREATE POLICY, ALTER POLICY und DROP POLICY-Ereignisse ein. Verlassen Sie sich nicht auf ein ungetestetes Trigger-Snippet für Compliance-Nachweise.

Richtlinienwirksamkeit testen

-- Testfunktion erstellen
CREATE OR REPLACE FUNCTION test_user_access(
    test_user_id INTEGER,
    test_tenant_id INTEGER
)
RETURNS TABLE(document_id INTEGER, title VARCHAR) AS $$
BEGIN
    -- Sitzungsvariablen setzen
    PERFORM set_config('app.user_id', test_user_id::text, false);
    PERFORM set_config('app.tenant_id', test_tenant_id::text, false);
    
    -- Zugängliche Dokumente zurückgeben
    RETURN QUERY SELECT id, documents.title FROM documents;
END;
$$ LANGUAGE plpgsql SECURITY INVOKER;

-- Zugriff testen
SELECT * FROM test_user_access(123, 5);

Fehlerbehebung

Problem 1: Richtlinien werden nicht angewendet

Prüfen, ob RLS aktiviert ist:

SELECT tablename, rowsecurity 
FROM pg_tables 
WHERE schemaname = 'public' AND tablename = 'documents';

Aktivieren, falls deaktiviert:

ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

Problem 2: Sitzungsvariablen nicht gesetzt

Aktuelle Einstellungen debuggen:

SHOW app.user_id;
SHOW app.tenant_id;

-- Oder current_setting verwenden
SELECT current_setting('app.user_id', true);

Problem 3: Leistungsverschlechterung

Langsame Richtlinien identifizieren:

EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM documents WHERE title = 'test';

-- Nach "Filter"-Operationen mit Richtlinienbedingungen suchen

Lösung: Richtlinien vereinfachen oder Indizes hinzufügen

Problem 4: Unerwarteter Zugriff verweigert

Als bestimmten Benutzer testen:

SET ROLE regular_user;
SELECT set_config('app.user_id', '123', false);

SELECT * FROM documents;
-- Zeigt, was dieser Benutzer tatsächlich sehen kann

RESET ROLE;

Best Practices

  1. Sitzungsvariablen immer setzen: Stellen Sie sicher, dass current_setting()-Werte befüllt sind
  2. Richtlinien-Spalten indizieren: Erstellen Sie Indizes für alle in USING/WITH CHECK verwendeten Spalten
  3. Gründlich testen: Überprüfen Sie Richtlinien mit verschiedenen Rollen und Szenarien
  4. Richtlinien einfach halten: Komplexe Logik beeinträchtigt die Leistung
  5. EXPLAIN verwenden: Analysieren Sie Abfragepläne mit aktiviertem RLS
  6. Richtlinien dokumentieren: Kommentieren Sie die Richtlinienlogik für die Wartbarkeit
  7. Aufgaben trennen: Verwenden Sie verschiedene Richtlinien für SELECT, INSERT, UPDATE, DELETE
  8. Leistung überwachen: Richten Sie Warnungen für langsame Abfragen ein

Fazit

Beginnen Sie mit einer einfachen Richtlinie: Mandantenisolierung für Ihre risikoreichsten Tabellen. Setzen Sie den Sitzungskontext mit set_config, indizieren Sie jede von Richtlinien verwendete Spalte und testen Sie Lese- und Schreibvorgänge als echte Anwendungsrollen.

Sobald die grundlegende Grenze funktioniert, fügen Sie Eigentümer-, Manager- oder gemeinsame Zugriffsrichtlinien nur dort hinzu, wo das Produkt sie benötigt. Kleine, getestete Richtlinien sind einfacher zu durchschauen als eine clevere Regel, die versucht, Ihr gesamtes Berechtigungsmodell zu kodieren.