Fortgeschrittene PostgreSQL-rollenbasierte Zugriffskontrolle (RBAC): Row-Level Security und Richtlinienmanagement
Einführung
PostgreSQL Row-Level Security (RLS) bietet eine feingranulare Zugriffskontrolle auf Zeilenebene und ermöglicht es Ihnen, komplexe Autorisierungsregeln direkt in der Datenbank zu erzwingen. Dies erübrigt das Filtern auf der Anwendungsebene und gewährleistet die Datensicherheit, unabhängig davon, wie Anwendungen auf die Datenbank zugreifen.
Warum Row-Level Security?
Probleme traditioneller Ansätze
Filtern auf Anwendungsebene:
# Bei jeder Abfrage muss an das Filtern gedacht werden
query = "SELECT * FROM documents WHERE tenant_id = %s"
results = db.execute(query, [current_user_tenant_id])
Probleme:
- Filter werden leicht vergessen (Sicherheitslücken)
- Code-Duplizierung in der gesamten Anwendung
- Kein Schutz bei direktem Datenbankzugriff
- Komplexe Audit-Anforderungen
Vorteile von Row-Level Security
- Automatische Erzwingung: Richtlinien werden transparent angewendet
- Zentralisierte Regeln: Sicherheitslogik an einem Ort
- Multi-Tenant-Isolierung: Perfekt für SaaS-Anwendungen
- Audit-Compliance: Integrierte Sicherheitsgarantien
- Leistung: Effiziente Abfrageplanung
Grundlegendes RLS-Setup
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()
);
-- Row-Level Security 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 (Policies) 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("SET app.user_id = %s", [current_user_id])
# Jetzt werden Abfragen automatisch gefiltert
cursor.execute("SELECT * FROM documents")
# Gibt nur Dokumente zurück, bei denen owner_id = current_user_id
Multi-Tenant SaaS-Anwendung
Vollständiges Multi-Tenant-Setup
-- Mandanten-Tabelle erstellen
CREATE TABLE tenants (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Benutzer-Tabelle 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(
"SET app.tenant_id = %s",
[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('SET app.tenant_id = $1', [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();
});
Fortgeschrittene Richtlinienmuster
1. Hierarchische Zugriffskontrolle
-- Benutzer können Dokumente ihrer Abteilung und öffentliche Dokumente 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 (Shared Access Lists)
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'
)
);
Umgehen von RLS für administrative Aufgaben
Option 1: BYPASSRLS Rollen-Attribut
-- Superuser-ähnlichen Zugriff gewähren
CREATE ROLE admin_user WITH LOGIN PASSWORD 'sicheres_passwort';
ALTER ROLE admin_user BYPASSRLS;
-- Admin kann alle Zeilen unabhängig von Richtlinien sehen
Option 2: Freizügige Richtlinien für Admins
-- Freizügige 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 temporär deaktivieren
-- Für Wartungsskripte
BEGIN;
ALTER TABLE documents DISABLE ROW LEVEL SECURITY;
-- Administrative Operationen durchführen
UPDATE documents SET archived = true WHERE created_at < NOW() - INTERVAL '1 year';
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
COMMIT;
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_user_id());
-- Benutzer kann nur Zeilen SELEKTIEREN, bei denen die owner_id übereinstimmt
WITH CHECK-Klausel
Steuert, welche neuen/aktualisierten Zeilen zulässig sind:
CREATE POLICY insert_own ON documents
FOR INSERT
WITH CHECK (owner_id = current_user_id());
-- Benutzer kann nur Zeilen EINFÜGEN, bei denen die owner_id übereinstimmt
Kombinierte Richtlinien
CREATE POLICY update_own ON documents
FOR UPDATE
USING (owner_id = current_user_id()) -- Kann nur eigene Zeilen aktualisieren
WITH CHECK (owner_id = current_user_id()); -- Kann die Eigentümerschaft nicht ändern
Performance-Überlegungen
1. Spalten in Richtlinien 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 '%bericht%';
-- Prüfen, ob der Richtlinienfilter effizient angewendet wird
3. Unterabfragen in Richtlinien vermeiden
Schlecht - Unterabfrage wird für jede Zeile ausgeführt:
CREATE POLICY slow_policy ON documents
USING (
id IN (SELECT document_id FROM shares WHERE user_id = current_user_id())
);
Besser - EXISTS verwenden:
CREATE POLICY faster_policy ON documents
USING (
EXISTS (
SELECT 1 FROM shares
WHERE document_id = documents.id
AND user_id = current_user_id()
)
);
Bestens - JOIN in der Anwendung oder Materialized View verwenden:
CREATE MATERIALIZED VIEW user_accessible_documents AS
SELECT d.*, s.user_id AS shared_with
FROM documents d
LEFT JOIN shares s ON d.id = s.document_id;
CREATE INDEX ON user_accessible_documents(shared_with);
Überwachung und Auditierung
Aktive Richtlinien anzeigen
-- Alle RLS-Richtlinien auflisten
SELECT
schemaname,
tablename,
policyname,
permissive,
roles,
cmd,
qual,
with_check
FROM pg_policies
ORDER BY tablename, policyname;
Auditierung von Richtlinienänderungen
-- Audit-Log für Richtlinienänderungen erstellen
CREATE TABLE policy_audit_log (
id SERIAL PRIMARY KEY,
table_name VARCHAR(100),
policy_name VARCHAR(100),
action VARCHAR(20),
changed_by VARCHAR(50),
changed_at TIMESTAMP DEFAULT NOW()
);
-- Trigger-Funktion zum Protokollieren von Änderungen
CREATE OR REPLACE FUNCTION log_policy_changes()
RETURNS event_trigger AS $$
BEGIN
INSERT INTO policy_audit_log (table_name, policy_name, action, changed_by)
SELECT
objid::regclass::text,
object_identity,
'CREATED',
current_user
FROM pg_event_trigger_ddl_commands()
WHERE command_tag = 'CREATE POLICY';
END;
$$ LANGUAGE plpgsql;
CREATE EVENT TRIGGER log_policy_create
ON ddl_command_end
WHEN TAG IN ('CREATE POLICY')
EXECUTE FUNCTION log_policy_changes();
Wirksamkeit von Richtlinien 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, true);
PERFORM set_config('app.tenant_id', test_tenant_id::text, true);
-- Zugängliche Dokumente zurückgeben
RETURN QUERY SELECT id, documents.title FROM documents;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 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: Performance-Einbußen
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 verweigerter Zugriff
Als spezifischer Benutzer testen:
SET ROLE regular_user;
SET app.user_id = '123';
SELECT * FROM documents;
-- Zeigt, was dieser Benutzer tatsächlich sehen kann
RESET ROLE;
Best Practices
- Sitzungsvariablen immer setzen: Sicherstellen, dass
current_setting()-Werte befüllt sind. - Spalten in Richtlinien indizieren: Indizes für alle Spalten erstellen, die in USING/WITH CHECK verwendet werden.
- Gründlich testen: Richtlinien mit verschiedenen Rollen und Szenarien verifizieren.
- Richtlinien einfach halten: Komplexe Logik beeinträchtigt die Performance.
- EXPLAIN verwenden: Abfragepläne mit aktiviertem RLS analysieren.
- Richtlinien dokumentieren: Richtlinienlogik für die Wartbarkeit kommentieren.
- Zuständigkeiten trennen: Unterschiedliche Richtlinien für SELECT, INSERT, UPDATE, DELETE verwenden.
- Performance überwachen: Benachrichtigungen für langsame Abfragen einrichten.
Fazit
PostgreSQL Row-Level Security bietet eine robuste, datenbankgestützte Zugriffskontrolle:
- Automatisches Filtern: Sicherheit wird transparent angewendet.
- Multi-Tenant-Isolierung: Ideal für SaaS-Anwendungen.
- Flexible Richtlinien: Unterstützung komplexer Autorisierungsregeln.
- Zentralisierte Sicherheit: Logik in der Datenbank, nicht verstreut im Code.
Beginnen Sie mit einer einfachen Mandantentrennung, testen Sie die Auswirkungen auf die Performance und erweitern Sie das System dann nach Bedarf um anspruchsvollere Autorisierungsmuster.