Модель безопасности¶
Provisa применяет многоуровневую модель безопасности для каждого языка запросов (GraphQL, SQL, Cypher) и каждого транспорта (REST, gRPC, Arrow Flight, JDBC, WebSocket). (REQ-001, REQ-266) Governance применяется единообразно — не существует пути запроса, который бы его обходил. (REQ-002, REQ-266)
Уровни применяются по порядку. Запрос должен пройти каждый уровень, прежде чем будет оценён следующий.
Многоуровневая модель¶
Уровень 0 — Фильтрация интроспекции¶
Схема и каталог, представленные роли, содержат только таблицы из её списка domain_access и столбцы, прошедшие правила visible_to для каждого столбца. (REQ-039) Объекты вне доступа роли невидимы на этапе обнаружения — их нельзя запросить, автодополнить или вывести их существование. (REQ-039) Это применяется к схеме GraphQL, каталогу SQL и браузеру схемы редактора запросов. (REQ-039, REQ-363)
См. Видимость схемы.
Уровень 1 — Публичный доступ¶
Таблицы в доменах без ограничения domain_access видны всем аутентифицированным identity без дополнительной конфигурации. Нулевое трение для по-настоящему публичных данных.
Уровень 2 — Доступ к домену¶
Каждая роль несёт список domain_access с идентификаторами доменов. Запрос, затрагивающий таблицу вне этих доменов, отклоняется до выполнения. (REQ-038, REQ-039) Это грубая граница владения — роль HR не может достичь таблиц финансов независимо от того, как написан SQL. (REQ-002)
См. Модель прав.
Уровень 3 — Безопасность на уровне строк¶
После подтверждения доступа к домену, предложения WHERE для каждой таблицы и роли внедряются в каждый SELECT во время выполнения. (REQ-041, REQ-263) Предложения оцениваются относительно сырых данных. Региональный менеджер, запрашивающий общую таблицу заказов, видит только строки своего региона даже при SELECT *. (REQ-264)
См. Безопасность на уровне строк (RLS).
Уровень 4 — Видимость и маскирование столбцов¶
Столбцы со списком visible_to, исключающим запрашивающую роль, удаляются из вывода запроса. (REQ-040, REQ-263) Столбцы с правилом маскирования получают заменённые значения — редактирование по regex, замена константой или усечение — прежде чем результаты покинут сервер. (REQ-263) Маскирование применяется во всех языках запросов и форматах вывода. (REQ-263)
См. Модель разрешений столбцов и Маскирование на уровне столбцов.
Уровень 5 — Защита предикатов¶
Замаскированные столбцы отклоняются из предложений WHERE и HAVING. (REQ-263) Без этого вызывающая сторона могла бы вывести немаскированное значение, бинарно перебирая его в фильтре, даже если вывод замаскирован. Отклонение применяется на этапе разбора запроса, до выполнения. (REQ-531)
Governance связей (V002)¶
Условия JOIN в SQL должны соответствовать зарегистрированной, утверждённой связи между таблицами. (REQ-001) Неутверждённые соединения отклоняются. Каждая связь несёт человекочитаемую причину и описание — руководство как для пользователей, так и для автономных агентов о том, почему существует путь обхода. Это политика governance, а не жёсткая граница безопасности: уровни 2–5 действуют независимо от структуры соединения, поэтому намеренный обход не раскрывает данные, которые роль не могла бы получить через два отдельных запроса. Попытки обхода регистрируются и подлежат аудиту.
Механизмы обхода — V002 может быть обойдён только при одновременном выполнении двух независимых условий:
- Флаг роли —
relationship_guard: falseв определении роли (по умолчанию:true). [tool-verified:provisa/core/models.py:349] - Отказ для отдельного запроса — SQL содержит комментарий
--relationship-guard=false. [tool-verified:provisa/compiler/params.py:80]
Оба условия должны присутствовать. Один только флаг роли не обходит V002; один только комментарий не обходит V002.
Путь GraphQL — V002 безусловно пропускается для запросов GraphQL. Связи, определённые в SDL, предварительно утверждены по замыслу; проверка избыточна и не применяется. [tool-verified: provisa/api/data/endpoint.py:468]
Пути SQL и Cypher — V002 активен по умолчанию. И endpoint_dev.py, и cypher_router.py применяют проверку из двух условий перед вызовом validate_sql. [tool-verified: provisa/api/data/endpoint_dev.py:127, provisa/api/rest/cypher_router.py:260]
Путь pgwire — та же проверка из двух условий, что и для SQL. Комментарий --relationship-guard=false удаляется из запроса перед выполнением; он не доходит до базы данных. [tool-verified: provisa/pgwire/_pipeline.py:60]
Эти уровни компонуются. Роль с доступом к домену, RLS и замаскированными столбцами имеет все пять ограничений активными одновременно. Добавление нового источника данных, столбца или связи не требует обновления каждого правила — каждый уровень настраивается независимо и применяется автоматически к любому запросу, затрагивающему управляемые объекты.
Модель прав¶
Независимо назначаемые возможности с опциональной иерархией ролей через parent_role_id. admin предоставляет всё. (REQ-042)
| Возможность | Описание |
|---|---|
source_registration |
Регистрация источников данных |
table_registration |
Регистрация таблиц, столбцов |
create_relationship |
Определение связей FK |
access_config |
Настройка RLS, маскирования |
query_development |
Выполнение запросов |
write |
Вызов зарегистрированных мутаций (грубые врата; см. Авторизация мутаций) |
full_results |
Обход ограничений выборки |
ignore_relationships |
Обход governance связей (V002) |
admin |
Суперпользователь — предоставляет всё |
Наследование ролей¶
Роли могут наследовать возможности и доступ к домену от родительской роли через parent_role_id. (REQ-215) Иерархия сплющивается при запуске — дочерние роли объединяют возможности и доступ к домену родителя со своими собственными. (REQ-215)
roles:
- id: basic_user
capabilities: [query_development]
domain_access: [public]
- id: analyst
capabilities: [full_results]
domain_access: [sales, analytics]
parent_role_id: basic_user # inherits query_development + public domain
Модель разрешений столбцов¶
Каждый столбец имеет четырёхпольную модель разрешений, управляющую доступом к чтению, записи и маскированием для каждой роли. (REQ-042, REQ-249)
Трёхуровневая видимость¶
| Уровень | Условие | Результат |
|---|---|---|
| Скрыт | Роль не в visible_to |
Столбец отсутствует в SDL GraphQL |
| Замаскирован | Роль в visible_to, есть правило маскирования, роль не в unmasked_to |
Столбец виден, но данные замаскированы в SQL |
| Немаскирован | Роль в visible_to И роль в unmasked_to (или нет правила маскирования) |
Полный доступ на чтение |
Разрешения на запись¶
| Поле | Пусто означает | Назначение |
|---|---|---|
visible_to |
Все роли могут читать | Управляет тем, кто видит столбец (замаскированный или нет) |
unmasked_to |
Ни одна роль не видит немаскированное значение | Управляет тем, кто обходит маскирование |
writable_by |
Ни одна роль не может писать | Управляет тем, кто может изменять (INSERT/UPDATE) |
Разрешение на запись применяется в конвейере мутаций. Роль, не входящая в writable_by, получает ошибку 403 при попытке записи в ограниченный столбец. (REQ-033, REQ-034)
Пример¶
columns:
- name: email
visible_to: [admin, analyst, viewer]
writable_by: [admin]
unmasked_to: [admin]
mask_type: regex
mask_pattern: "(.).*@"
mask_replace: "$1***@"
- name: salary
visible_to: [admin, hr]
writable_by: [hr]
unmasked_to: [admin, hr]
mask_type: constant
mask_value: "0"
- name: created_at
visible_to: [] # all can read
writable_by: [] # nobody can write (auto-set)
В этом примере:
email: admin видитalice@example.comи может редактировать; analyst/viewer видятa***@example.comsalary: admin и hr видят реальное значение; hr может редактировать; все остальные роли вообще не видят столбецcreated_at: все могут читать, никто не может писать
Авторизация мутаций¶
Зарегистрированные мутации (удалённые GraphQL, OpenAPI, gRPC, Hasura) защищены двумя независимыми проверками. (REQ-867, REQ-868) Роль может вызвать мутацию, только если она обладает глобальной возможностью write И присутствует в списке writable_by этой мутации. (REQ-868) Пустой writable_by означает запрет по умолчанию — ни одна роль не может её вызвать. (REQ-867)
Мутации классифицируются как записи по контракту, а не по объявлению вызывающей стороны. (REQ-869) SELECT, ссылающийся на функцию типа мутация, повышается до записи и подлежит той же двухврательной проверке, поэтому вызывающая сторона не может вызвать мутацию, замаскировав её под чтение. (REQ-869) Переклассификация мутации в безопасную для чтения требует возможности access_config и записывается как решение governance; отказа для отдельного запроса не существует. (REQ-870)
Видимость схемы¶
Схемы GraphQL для каждой роли скрывают неавторизованное содержимое: (REQ-039)
- Доступ к домену: роль видит таблицы только в своих доменах
domain_access("*"= все) (REQ-039) - Видимость столбцов: столбцы, не входящие в
visible_toдля роли, опускаются из SDL (REQ-039) - Неавторизованные таблицы/столбцы не появляются в схеме (REQ-039)
Безопасность на уровне строк (RLS)¶
Внедрение предложения SQL WHERE для каждой таблицы и роли. Применяется после компиляции, до выполнения. (REQ-041, REQ-263)
rls_rules:
- table_id: orders
role_id: analyst
filter: "region = current_setting('provisa.user_region')"
Фильтр объединяется через AND с предложением WHERE запроса. Работает как для запросов, так и для мутаций (UPDATE/DELETE). (REQ-035, REQ-041)
Маскирование на уровне столбцов¶
Маскирование определяется один раз для столбца — это свойство столбца, а не роли. Поле unmasked_to управляет тем, какие роли его обходят. (REQ-249)
| Тип маски | Поддерживаемые типы | SQL-выражение |
|---|---|---|
regex |
Строка (varchar, char, text) | REGEXP_REPLACE(col, pattern, replace) |
constant |
Любой | Литеральное значение (NULL, 0, пользовательское) |
truncate |
Дата/Timestamp | DATE_TRUNC(precision, col) |
Маскирование встраивается в проекцию SQL SELECT — база данных возвращает замаскированные данные. (REQ-263) Немаскированные данные никогда не пересекают провод для замаскированных ролей. (REQ-263) Замаскированные столбцы также блокируются в предложениях WHERE и HAVING (защита предикатов уровня 5), чтобы предотвратить вывод немаскированного значения через фильтрацию. (REQ-263, REQ-531)
Выборка¶
Все роли видят выборочные результаты (по умолчанию: 100 строк), если у них нет возможности full_results. (REQ-554) Управляется через переменную окружения PROVISA_SAMPLE_SIZE. (REQ-554)
Аудит-логирование¶
Каждый запрос, затрагивающий актив домена, записывается в неизменяемый (append-only) query_audit_log. (REQ-596, REQ-613) Каждая строка фиксирует tenant_id, user_id, role_id, хэш SHA-256 текста запроса, table_ids, source, status_code, duration_ms и logged_at. (REQ-596) Текст запроса никогда не хранится дословно — только его хэш. (REQ-596)
Журнал неизменяем на уровне базы данных: правила PostgreSQL блокируют DELETE и UPDATE. (REQ-596, REQ-613) Два индекса — (tenant_id, logged_at) и (user_id, logged_at) — поддерживают запросы соответствия по временным диапазонам в рамках арендатора и для отдельного пользователя. (REQ-596, REQ-613)
Когда включено шифрование, столбец хэша текста запроса хранится зашифрованным и расшифровывается только при авторизованных чтениях администратором. (REQ-689)
Ограничение частоты запросов¶
Ограничения частоты для каждой роли настраиваются в provisa.yaml: максимум запросов в секунду, максимум одновременных подписок SSE и максимум одновременных потоков Arrow Flight. (REQ-369) Ограничения применяются на уровне API до компиляции или выполнения; запросы сверх лимита отклоняются с HTTP 429 и заголовком Retry-After. (REQ-369)
Сервис NL-запросов (POST /query/nl) имеет независимый лимит через nl.rate_limit (запросов в минуту на роль). Запросы сверх лимита отклоняются до какого-либо вызова LLM. (REQ-370)
Состояние ограничения частоты хранится в Redis (cache.redis_url) как счётчик скользящего окна — без состояния для каждого экземпляра — поэтому лимиты действуют на всех горизонтальных экземплярах Provisa. (REQ-371)
Аутентификация¶
Подключаемые провайдеры аутентификации: (REQ-120)
| Провайдер | Тип токена | Сценарий использования |
|---|---|---|
none |
Заголовок X-Provisa-Role | Разработка |
firebase |
ID-токен Firebase | Продакшен |
keycloak |
JWT Keycloak | Предприятие |
oauth |
OIDC JWT | PingFed, Okta, Azure AD, Auth0 |
simple |
bcrypt + JWT | Тестирование |
Сопоставление ролей: утверждения identity → роль Provisa через настраиваемые правила. (REQ-120) Поле assignments_source управляет тем, откуда поступают назначения ролей: claims считывает их из утверждений JWT-токена (по умолчанию), provisa считывает их из внутреннего хранилища назначений Provisa. (REQ-551)
Суперпользователь, настроенный в provisa.yaml (имя пользователя плюс пароль из секрета окружения), всегда получает роль admin и все возможности независимо от настроенного провайдера — путь для начальной настройки при загрузке. (REQ-125)
Хук утверждения ABAC¶
Опциональный внешний хук политики, срабатывающий перед выполнением запроса. (REQ-203) При настройке Provisa обращается к вашему движку политик с identity пользователя, ролями, таблицами, столбцами и операцией. Ответ определяет, продолжится ли запрос. (REQ-203)
Область действия¶
Хук срабатывает только когда запрос затрагивает таблицу или источник в области действия — нулевые накладные расходы для всего остального. (REQ-204)
| Конфигурация | Эффект |
|---|---|
auth.approval_hook.scope: all |
Каждый запрос запускает хук |
sources[].approval_hook: true |
Все таблицы этого источника запускают хук |
tables[].approval_hook: true |
Эта таблица запускает хук |
Протоколы¶
Поддерживаются три транспорта: (REQ-246)
| Тип | Сценарий использования | Поле конфигурации |
|---|---|---|
webhook |
Любой HTTP-совместимый сервис политик (OPA, пользовательский) | url |
unix_socket |
OPA или сайдкар политик на той же машине | socket_path + url |
grpc |
Высокопроизводительный совместно размещённый сервис политик | url (host:port) |
Транспорт gRPC использует контракт provisa.auth.ApprovalService, определённый в provisa/auth/approval.proto. Реализуйте этот сервис в вашем движке политик: (REQ-246)
service ApprovalService {
rpc Evaluate (ApprovalRequest) returns (ApprovalResponse);
}
message ApprovalRequest {
string user = 1;
repeated string roles = 2;
repeated string tables = 3;
repeated string columns = 4;
string operation = 5;
}
message ApprovalResponse {
bool approved = 1;
string reason = 2;
}
Канал gRPC постоянен — один канал на экземпляр Provisa, повторно используемый для всех вызовов к этому эндпоинту хука. (REQ-555)
Запрос / Ответ¶
Все три транспорта несут одну и ту же полезную нагрузку: (REQ-246)
| Поле | Тип | Описание |
|---|---|---|
user |
string | Identity аутентифицированного пользователя |
roles |
string[] | Роли Provisa пользователя |
tables |
string[] | Идентификаторы таблиц, на которые ссылается запрос |
columns |
string[] | Столбцы, выбранные в запросе |
operation |
string | "query" или "mutation" |
Транспорты webhook и Unix socket обмениваются JSON. Ответ должен включать approved (bool) и опционально reason (string). (REQ-246)
Таймаут и запасной вариант¶
auth:
approval_hook:
type: grpc # webhook | grpc | unix_socket
url: "localhost:50051"
timeout_ms: 500 # default 5000
fallback: deny # allow | deny — applied on timeout or error
scope: "" # "" = use per-table/per-source flags; "all" = every query
При таймауте или ошибке транспорта применяется политика fallback. (REQ-247) Автоматический выключатель (circuit breaker) (по умолчанию: открывается после 5 последовательных сбоев, полуоткрывается через 30 с) предотвращает каскадные сбои от медленного эндпоинта хука. (REQ-556)
Пример конфигурации¶
auth:
approval_hook:
type: webhook
url: "http://opa.internal:8181/v1/data/provisa/allow"
timeout_ms: 300
fallback: deny
sources:
- id: analytics_pg
approval_hook: true # all tables on this source require hook approval
tables:
- id: salary_data
approval_hook: true # this table always requires hook approval
Секреты¶
Учётные данные используют синтаксис ${env:VAR_NAME}, разрешаемый во время выполнения. (REQ-557) Пароли никогда не хранятся в БД конфигурации. (REQ-557)