Продвинутое управление доступом на основе ролей в PostgreSQL: защита на уровне строк и управление политиками
Используйте безопасность на уровне строк PostgreSQL с политиками RBAC для изоляции арендаторов, проверки владения и более безопасного доступа к приложению.
Расширенное управление доступом на основе ролей в PostgreSQL: безопасность на уровне строк и управление политиками
Безопасность на уровне строк (RLS) в PostgreSQL позволяет вашей базе данных фильтровать строки на основе текущей роли и контекста сессии. Если ваше приложение обслуживает несколько арендаторов или смешивает доступ администратора, менеджера и обычного пользователя, RLS может предотвратить превращение одного пропущенного WHERE tenant_id = ... в утечку данных.
RLS не заменяет авторизацию приложения. Он предоставляет вам страховочную сетку на уровне базы данных для наиболее чувствительной границы: какие строки может читать или записывать соединение.
Зачем нужна безопасность на уровне строк?
Проблемы традиционного подхода
Фильтрация на уровне приложения:
# Каждый запрос должен помнить о фильтрации
query = "SELECT * FROM documents WHERE tenant_id = %s"
results = db.execute(query, [current_user_tenant_id])
Проблемы:
- Легко забыть фильтры (уязвимости безопасности)
- Дублирование кода в приложении
- Отсутствие защиты при прямом доступе к базе данных
- Сложные требования к аудиту
Преимущества безопасности на уровне строк
- Автоматическое применение: Политики применяются прозрачно
- Централизованные правила: Логика безопасности в одном месте
- Изоляция арендаторов: Идеально для SaaS-приложений
- Соответствие аудиту: Встроенные гарантии безопасности
- Производительность: Эффективное планирование запросов
Базовая настройка RLS
Шаг 1: Включение RLS для таблицы
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()
);
-- Включить безопасность на уровне строк
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
Шаг 2: Создание ролей
-- Создание различных ролей пользователей
CREATE ROLE regular_user;
CREATE ROLE department_manager;
CREATE ROLE admin;
-- Предоставление доступа к таблице
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;
Шаг 3: Создание политик
-- Политика: Пользователи видят только свои документы
CREATE POLICY user_own_documents ON documents
FOR SELECT
TO regular_user
USING (owner_id = current_setting('app.user_id')::INTEGER);
-- Политика: Пользователи могут вставлять документы, принадлежащие им
CREATE POLICY user_insert_own ON documents
FOR INSERT
TO regular_user
WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);
-- Политика: Пользователи могут обновлять свои документы
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);
Шаг 4: Установка переменных сессии
В коде приложения:
import psycopg2
# Подключение как роль
conn = psycopg2.connect(
host="localhost",
database="mydb",
user="regular_user",
password="password"
)
# Установка контекста пользователя
cursor = conn.cursor()
cursor.execute("SELECT set_config('app.user_id', %s, false)", [str(current_user_id)])
# Теперь запросы автоматически фильтруются
cursor.execute("SELECT * FROM documents")
# Возвращает только документы, где owner_id = current_user_id
Мультитенантное SaaS-приложение
Полная настройка мультитенантности
-- Создание таблицы арендаторов
CREATE TABLE tenants (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Создание таблицы пользователей
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()
);
-- Создание таблицы данных приложения
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
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Политика: Пользователи видят только заказы своего арендатора
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);
Интеграция с приложением
Пример на Django:
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:
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;
}
}
// Использование
app.use(async (req, res, next) => {
if (req.user) {
req.dbClient = await setTenantContext(req.user.tenantId);
}
next();
});
Продвинутые шаблоны политик
1. Иерархический контроль доступа
-- Пользователи могут видеть документы своего отдела и публичные
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. Доступ на основе времени
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;
-- Политика: Пользователи видят только активные подписки
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. Доступ на основе ролей со сложной логикой
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;
-- Сложная политика видимости
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. Списки общего доступа
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;
-- Политика: Пользователи видят свои или общие документы
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
)
);
-- Политика: Пользователи могут изменять свои документы или те, к которым есть доступ на запись
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'
)
);
Административный доступ и RLS
Владельцы таблиц и суперпользователи могут обходить RLS, если вы не принудите его. Относитесь к административным путям как к отдельному проектному решению, а не как к запоздалой мысли.
Вариант 1: Атрибут роли BYPASSRLS
CREATE ROLE admin_user WITH LOGIN PASSWORD 'secure_password';
ALTER ROLE admin_user BYPASSRLS;
Используйте это экономно. BYPASSRLS является мощным и не должен предоставляться обычным ролям приложения.
Вариант 2: Разрешающие политики для администраторов
-- Создание разрешающей политики для роли администратора
CREATE POLICY admin_all_access ON documents
FOR ALL
TO admin
USING (true)
WITH CHECK (true);
Вариант 3: Принудительный RLS для владельцев таблиц
ALTER TABLE documents FORCE ROW LEVEL SECURITY;
FORCE ROW LEVEL SECURITY заставляет владельцев таблиц также следовать политикам. Обычно это безопаснее для таблиц, принадлежащих приложению. Для обслуживания используйте отдельную контролируемую роль вместо отключения RLS в ad hoc скрипте.
Типы политик: USING vs WITH CHECK
Предложение USING
Определяет, какие существующие строки видны/изменяемы:
CREATE POLICY select_own ON documents
FOR SELECT
USING (owner_id = current_setting('app.user_id')::INTEGER);
-- Пользователь может только SELECT строки, где owner_id совпадает
Предложение WITH CHECK
Определяет, какие новые/обновленные строки разрешены:
CREATE POLICY insert_own ON documents
FOR INSERT
WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);
-- Пользователь может только INSERT строки, где owner_id совпадает
Комбинированные политики
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);
Соображения производительности
1. Индексация столбцов политик
-- Создание индексов для столбцов, используемых в политиках
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. Анализ планов запросов
EXPLAIN ANALYZE
SELECT * FROM documents
WHERE title LIKE '%report%';
-- Проверьте, эффективно ли применяется фильтр политики
3. Будьте осторожны с подзапросами в политиках
Подзапросы иногда являются правильной моделью для общего доступа, но они требуют индексов и тестирования. Политика, которая проверяет другую таблицу для каждой строки-кандидата, может стать дорогостоящей.
Рискованно без индексов:
CREATE POLICY slow_policy ON documents
USING (
id IN (
SELECT document_id
FROM shares
WHERE user_id = current_setting('app.user_id')::INTEGER
)
);
Часто понятнее с поддерживающими индексами:
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
)
);
Создайте индексы, такие как shares(user_id, document_id), и проверьте план с помощью EXPLAIN (ANALYZE, BUFFERS).
Мониторинг и аудит
Просмотр активных политик
-- Список всех политик RLS
SELECT
schemaname,
tablename,
policyname,
permissive,
roles,
cmd,
qual,
with_check
FROM pg_policies
ORDER BY tablename, policyname;
Аудит изменений политик
Отслеживайте DDL политик в миграциях и журналах аудита базы данных. Если ваша среда использует триггеры событий, тщательно тестируйте их на вашей версии PostgreSQL и включайте события CREATE POLICY, ALTER POLICY и DROP POLICY. Не полагайтесь на непроверенный фрагмент триггера для доказательства соответствия.
Тестирование эффективности политик
-- Создание тестовой функции
CREATE OR REPLACE FUNCTION test_user_access(
test_user_id INTEGER,
test_tenant_id INTEGER
)
RETURNS TABLE(document_id INTEGER, title VARCHAR) AS $$
BEGIN
-- Установка переменных сессии
PERFORM set_config('app.user_id', test_user_id::text, false);
PERFORM set_config('app.tenant_id', test_tenant_id::text, false);
-- Возврат доступных документов
RETURN QUERY SELECT id, documents.title FROM documents;
END;
$$ LANGUAGE plpgsql SECURITY INVOKER;
-- Тестирование доступа
SELECT * FROM test_user_access(123, 5);
Устранение неполадок
Проблема 1: Политики не применяются
Проверьте, включен ли RLS:
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public' AND tablename = 'documents';
Включите, если отключен:
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
Проблема 2: Переменные сессии не установлены
Отладка текущих настроек:
SHOW app.user_id;
SHOW app.tenant_id;
-- Или используйте current_setting
SELECT current_setting('app.user_id', true);
Проблема 3: Ухудшение производительности
Определение медленных политик:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM documents WHERE title = 'test';
-- Ищите операции "Filter" с условиями политик
Решение: Упростите политики или добавьте индексы
Проблема 4: Неожиданный отказ в доступе
Тестирование как конкретный пользователь:
SET ROLE regular_user;
SELECT set_config('app.user_id', '123', false);
SELECT * FROM documents;
-- Показывает, что этот пользователь может видеть
RESET ROLE;
Лучшие практики
- Всегда устанавливайте переменные сессии: Убедитесь, что значения
current_setting()заполнены - Индексируйте столбцы политик: Создавайте индексы для всех столбцов, используемых в USING/WITH CHECK
- Тщательно тестируйте: Проверяйте политики с разными ролями и сценариями
- Держите политики простыми: Сложная логика влияет на производительность
- Используйте EXPLAIN: Анализируйте планы запросов с включенным RLS
- Документируйте политики: Комментируйте логику политик для удобства сопровождения
- Разделяйте задачи: Используйте разные политики для SELECT, INSERT, UPDATE, DELETE
- Мониторьте производительность: Настройте оповещения для медленных запросов
Вывод
Начните с одной простой политики: изоляция арендаторов для ваших таблиц с самым высоким риском. Установите контекст сессии с помощью set_config, индексируйте каждый столбец, используемый политиками, и тестируйте чтение и запись как реальные роли приложения.
Как только базовая граница заработает, добавляйте политики владения, менеджера или общего доступа только там, где это необходимо продукту. Небольшие, протестированные политики легче анализировать, чем одно хитроумное правило, которое пытается закодировать всю вашу модель разрешений.