Модель безопасности¶
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 — Публичный доступ¶
Колонка, опубликованная с областью public, обслуживается каждой роли организации, включая роли, чей domain_access не достигает домена таблицы; такая роль видит таблицу только с её публичными колонками. Ничто не публично, пока регистрирующий таблицу этого не скажет. (REQ-1959) См. Публичные колонки.
Уровень 2 — Доступ к доменам¶
Каждая роль несёт список domain_access из id доменов. Запрос, затрагивающий таблицу вне этих доменов, отклоняется до выполнения. (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 можно обойти двумя способами. Первый — это возможность: роль, у которой есть ignore_relationships, соединяет отношения, которые каталог не покрывает. Из предустановленных системных ролей её имеет только modeler — роль исследования, задача которой — определить модель, а не применять её. (REQ-1297) У analyst её нет.
Второй — исключение с двумя условиями, оба из которых должны выполняться:
- Флаг роли —
relationship_guard: falseв определении роли (по умолчанию:true). - Исключение в запросе — SQL содержит комментарий
--relationship-guard=false.
Один только флаг роли не обходит V002; один только комментарий не обходит V002.
Режим высокой безопасности фиксирует защиту. При security.mode: high ни один обход не действует: ignore_relationships игнорируется, relationship_guard: false игнорируется, и каждое соединение должно существовать в каталоге одобренных связей. (REQ-693) Это намеренная избыточность — производственная роль, которой возможность выдана по ошибке, всё равно не может выйти из модели.
Путь GraphQL — V002 всегда пропускается для запросов GraphQL. Связи, определённые в SDL, одобрены заранее по своей природе; проверка избыточна и не применяется.
Пути SQL и Cypher — V002 активен по умолчанию. И endpoint_dev.py, и cypher_router.py применяют проверку с двумя условиями перед вызовом validate_sql.
Путь pgwire — та же проверка с двумя условиями, что и в SQL. Комментарий --relationship-guard=false удаляется из запроса перед выполнением; до базы данных он не доходит.
Эти уровни сочетаются. У роли с доступом к доменам, 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). Из системных ролей есть только у modeler, а в режиме высокой безопасности полностью игнорируется |
admin |
Суперпользователь — даёт всё |
Наследование ролей¶
Роли могут наследовать от одной родительской роли через parent_role_id, заданный в конфигурации или на странице Безопасность как «Наследует от». (REQ-215) Цепочка сворачивается при запуске: потомок получает объединение возможностей и доступа к доменам своих предков, каждое предоставление колонки, метрики, функции и webhook, называющее предка, и правила RLS предков для каждой таблицы с приоритетом потомка — ближайшая роль в цепочке с правилом для таблицы даёт предикат этой таблицы, а правило, сохранённое для потомка, заменяет правило родителя для этой таблицы. (REQ-1677)
roles:
- id: basic_user
description: Queries the public domain.
capabilities: [query_development]
domain_access: [public]
- id: analyst
description: Analyses sales; reads full results.
capabilities: [full_results]
domain_access: [sales, analytics]
parent_role_id: basic_user # inherits query_development + public domain
Роли: предоставленные, производные и мета¶
В модели доступа Provisa работают вместе три вида ролей. Страница «Роли» показывает первые два вида вместе; метароль никогда не показывается.
Предоставленные роли¶
Предоставленная роль назначается пользователю напрямую. Она несёт права: возможности, доступ к доменам, записи маскировки, фильтры строк. Наследование (parent_role_id) сворачивает права предка в потомка при запуске. Права только добавляются; вычитания нет. Чтобы ограничить читателя, предоставляйте составной записи, которая называет и роль, и условие, либо поставьте запись маски перед разрешающей. (REQ-215, REQ-1677)
Истекающие и неиспользуемые назначения¶
Назначение, которое хранит Provisa, может закончиться. Назначение со сроком действия перестаёт действовать с этого момента для любого запроса. Роль может указать inactivity_days: назначение этой роли, от которого ни один запрос не зависел дольше этого срока, считая от более позднего из последнего использования и создания, удаляется. Роли из утверждений (claims) или сопоставлений ролей поставщика удостоверений ограничивает по времени поставщик; задайте их срок там. Производные роли и метароли никогда не истекают. (REQ-2077)
Запрос зависел от роли, если эта роль одна обслужила его или, когда запрос действует от имени нескольких ролей, если роли предоставлена таблица, которую прочитала инструкция. Действие от имени «Все» не считает использованной каждую имеющуюся роль. Последнее использование назначения записывается не чаще раза в день.
Каждые 15 минут проход каждой организации удаляет истёкшие и неиспользуемые назначения и записывает каждое в административный журнал как role.expired или role.removed_inactive. Там, где настроена исходящая почта, включая развёртывание с одной организацией, владелец и администраторы организации получают письмо за auth.role_grant_notice_days дней (по умолчанию 7) до окончания назначения, а администраторы получают письмо, когда назначение удалено; если почта не настроена, запись в журнале служит подтверждением. Эти уведомления отправляются независимо от почтовых настроек пользователя.
Производные роли¶
Производная роль происходит из атрибутов пользователя. Организация объявляет, какие атрибуты становятся ролями (derived_roles), каждый с описанием. Для каждого объявленного атрибута a, значение v которого дают атрибуты пользователя, у пользователя есть родительская роль a и её роль значения a__v: region = eu даёт region и region__eu. Нет значения — нет ни одной из этих ролей.
Разрешение атрибутов имеет значение. Собственная запись организации об участнике побеждает то, что даёт провайдер идентичности: если оба называют один атрибут, используется значение организации, а значение провайдера помечается как переопределённое.
Производные роли не несут прав и никогда не назначаются вручную. Список предоставления, маска или правило строк называет их точно так же, как предоставленную роль; запрос называет их в X-Provisa-Role и действует от их имени. (REQ-2057)
На странице «Роли» каждый объявленный атрибут — это родительская роль с пометкой производная, а её роли значений перечислены под ней по мере появления их значений. + Роль → Производная от атрибута объявляет такую роль. Её панель показывает атрибут, его описание, уже встреченные значения и каждую маску, правило строк и предоставление, которые называют его или одну из его ролей значений. Элементов управления предоставлением у неё нет. Описание роли значения генерируется: «Есть, когда region пользователя равен eu». (REQ-2078, REQ-2079)
Объявите их в конфигурации списком:
derived_roles:
- attribute: region
description: The subsidiary a member works for; region__eu marks EU staff.
Подумайте о JWT: его claims — это факты о человеке. Производные роли превращают выбранные факты в роли, чтобы предоставление могло обращаться к ним без создания роли на каждую комбинацию.
Метароли¶
Когда запрос называет больше одной роли, Provisa строит эфемерную метароль с идентификатором meta:<ids sorted, joined by +>. Она — единственный потомок каждой роли названного набора. Весь конвейер затем выполняется с ней как с обычной одиночной ролью.
Права — это объединение прав каждого участника. Фильтры строк — это OR разрешённых фильтров каждого участника; если у какого-либо участника, читающего таблицу, нет фильтра, его нет и у метароли — отсутствие фильтра означает все строки. Метароль никогда не сохраняется, не перечисляется и не экспортируется. Она пересобирается из своих участников при изменении модели. (REQ-1620)
Столбцы каждой строки маскируются и показываются по ролям, чей фильтр строк допустил эту строку: из этих ролей применяется наименее ограничительное чтение столбца, а роль, не допустившая строку, никак не влияет на то, как она читается. Маски применяются там, где читается таблица, до любого соединения или агрегации, поэтому агрегация читает замаскированные значения. Записи проверяются так же: столбец записывается в строку только там, где роль, предоставляющая этот столбец, допускает строку. (REQ-1620)
Запись аудита называет каждую действующую роль, а не id метароли. Поскольку метароль никогда не сохраняется, у неё нет собственного описания: везде, где оно показывается, её описание — это описания её участников, каждый id со своим. (REQ-2080)
Описания¶
Каждая роль и каждое правило строк говорят, для чего они нужны. Проверка доступа читает, кто может видеть что и почему. Роль или правило, которые никто не может объяснить, никто не может и проверить, поэтому описание обязательно: роль, производная родительская роль или правило строк с пустым описанием отклоняются загрузкой конфигурации, API администрирования и UI с указанием причины. Списки «Роли» и «Правила строк» показывают каждое описание, а их панели подробностей показывают и редактируют его. (REQ-2080, REQ-2081)
У роли одно описание, а не по одному на домен. Если общая роль не описывает свои обязанности в конкретном домене, этому домену нужна собственная роль, обычно потомок общей роли (parent_role_id), которая наследует её права и описывает, что она там делает.
Как запрос называет роли¶
Клиент отправляет одну роль или набор через запятую в X-Provisa-Role. Каждая названная роль должна быть у пользователя. Роль, не назначенная идентичности, отклоняется по имени.
Права и данные следуют только за ролями, от которых действует запрос: иметь роль — не значит действовать от неё. Одна роль — это эта роль; две или больше действуют как их метароль. Действующая роль, которая приносила бы с собой права каждой имеющейся роли, ничего бы не значила, поэтому запрос получает наименьшие привилегии, а запись аудита называет ровно то, что действовало.
Названные производные роли входят как квалификаторы. У квалификатора нет собственных прав, доступа к доменам, масок или фильтров строк, поэтому он не участник прав метароли: как участник без масок он снял бы маску с каждой колонки, а как не читающий ни одной таблицы — завершил бы любое OR фильтров строк. Он входит только в предоставления: предоставление или запись маски, называющая region__eu, достигает запроса, который называет этот квалификатор вместе с предоставленной ролью. (REQ-2057)
MCP-агент никогда не действует за пределами выданных ему ролей. Удалённый агент называет только роли, которыми владеет субъект его токена. Локальный stdio-агент называет только роли из набора, закреплённого в mcp.role (PROVISA_MCP_ROLE), и действует как этот набор, если не называет ни одной; любая другая роль отклоняется по имени. (REQ-2075)
Агент спрашивает, от какой роли он действует, с помощью MCP-инструмента describe_role. Ответ — эта роль и то, для чего она нужна (метароль — с каждым из её участников), а также роли, на которые агент может переключиться, каждая со своим описанием. Он никогда не содержит правила строк, маски, предоставления или условия. Provisa никогда не сообщает агенту, почему результат был сужен, и результат не несёт отметки «отфильтровано»: данные роли — это всё её представление, а причина того, чего не хватает, побуждала бы его выведывать. (REQ-2082)
Что означает +¶
+ встречается в двух местах, и в каждом значит своё.
В записи предоставления. analyst+region__eu в списке visible_to или to означает, что читатель имеет их все — analyst И region__eu одновременно. Запись, которая не является составной, — это одна роль. Между записями списка правило — любая: читатель имеет предоставление, если у него есть любая запись. И внутри, ИЛИ между.
В id метароли. meta:analyst+engineer — серверный идентификатор метароли набора {analyst, engineer}. Её права — объединение обоих. Клиент никогда не называет id метароли напрямую; он называет роли, которые имеет, а сервер строит метароль.
Правила колонок ABAC как простой RBAC¶
Политика на основе атрибутов спрашивает: «может ли этот пользователь с этими атрибутами видеть эту колонку?» RBAC спрашивает: «какие роли могут видеть эту колонку?» Составные предоставления и производные роли делают это одним вопросом.
«Снимать маску с этого email только для аналитика в ЕС» — это запись маски {to: [analyst+region__eu], mask: null}. Любой читатель, у которого есть и роль analyst, и производная роль region__eu, читает колонку без маски. Никакого языка политик. Никакой роли на комбинацию.
Компромисс — вычисление вместо перечисления. Небольшой каталог ролей снижает затраты на сопровождение, но «кто может видеть этот email из ЕС?» становится вычисляемым ответом: составные записи оцениваются по назначениям и claims каждого пользователя. Provisa может ответить в обе стороны, потому что модель — одно определение. Проверка доступа в редакторе колонок (POST /admin/masks/check) выполняет эту оценку для выбранного набора ролей.
Преимущества¶
- Правило колонки ABAC становится записью предоставления RBAC.
- Небольшой каталог ролей вместо роли на каждую комбинацию.
- Одно определение, согласующее правила каждого региона.
- Наименьшие привилегии и точная запись аудита на каждый запрос.
Рекомендации¶
- Держите каталог ролей настолько малым, насколько позволяет организация.
- Наследуйте внутри одной оси (старшинство, уровень допуска). Комбинируйте по независимым осям (роль + регион, роль + отдел).
- Ставьте самые специфичные записи маски первыми — более ранняя запись побеждает, даже если у читателя есть более поздняя.
- Проверяйте действующий доступ через
POST /admin/masks/checkперед выпуском комбинации в продакшен.
Модель разрешений колонок¶
У каждой колонки есть модель разрешений, управляющая чтением, записью и маскировкой для каждой роли. (REQ-042, REQ-249)
Видимость в три уровня¶
| Уровень | Условие | Результат |
|---|---|---|
| Скрыта | Роли нет в visible_to |
Колонки нет в схеме и каталоге |
| Маскирована | Роль в visible_to; у первой записи маски, которую она имеет, есть mask |
Колонка видна, значение в выводе заменено |
| Без маски | Роль в visible_to; у первой записи маски, которую она имеет, нет mask, или у неё нет ни одной |
Полный доступ на чтение |
Публичные колонки¶
У каждой колонки есть scope, задаваемый в файле модели, через API администратора или на странице Таблицы. Три области упорядочены по тому, кого они обслуживают: restricted, затем domain, затем public. Редактор таблицы (table_registration в домене таблицы) может сузить область колонки. Её расширение — restricted в domain или public, domain в public — обслуживает колонку большему числу ролей и требует права, которого требует её предоставление: column_grant или access_config в домене таблицы. Регистрируемая колонка сравнивается с domain, поэтому её регистрация как public — это расширение. Для колонки с чувствительным тегом любое изменение области требует sensitive_data. Отклонённое изменение называет колонку и право. (REQ-1959, REQ-1943, REQ-1944)
| Область | Обслуживается |
|---|---|
domain (по умолчанию) |
Ролям, которые достигают домена таблицы, как говорит visible_to. Пустой visible_to — это каждая такая роль. |
public |
Тем же ролям и также ролям, которые не достигают домена таблицы. |
restricted |
Только ролям, которые называет visible_to. Пустой visible_to — никто. |
Публикация колонки снимает условие домена для этой колонки и ничего больше:
- Роли вне домена таблица обслуживается только с её публичными колонками. Остальные колонки таблицы и таблицы, которые ничего не публикуют, остаются вне её схемы, каталога и SQL.
- Собственный
visible_toколонки по-прежнему решает, кому: публичная колонка, предоставленная одной роли, публична только для этой роли. - Маски, фильтры строк, лимиты строк и защита связей действуют так же, как внутри домена. Без фильтра строк для роли опубликованные колонки читаются без фильтра; фильтр строк на таблице или её домене по-прежнему действует.
- Связь обслуживается, когда обслуживаются колонки обоих её концов.
- Колонка-параметр таблицы API (аргумент пути или запроса) — это не данные, и она ничего не публикует.
- Домен
opsтребует явного предоставления для каждой колонки (REQ-1133); ничто в нём не обслуживается между доменами, какой бы ни была область.
Правило одинаково на каждой поверхности, потому что каждая читает скомпилированную схему роли: GraphQL, SQL (HTTP, pgwire, Flight, JDBC), Cypher и Bolt, gRPC, REST и JSON:API, каталоги Flight и MCP и списки администратора. Область, не являющаяся одним из трёх значений, отклоняется при сохранении колонки.
Разрешения на запись¶
Чтение колонки определяется visible_to (пусто означает, что читать могут все роли). Запись определяется для таблицы через write_grants, а не для колонки. По умолчанию у таблицы нет разрешений, поэтому ни одна роль не может в неё писать. (REQ-2074)
Разрешение на запись указывает, что роль может записать в таблицу одной операцией (insert, update или delete):
| Поле | Назначение |
|---|---|
columns |
Колонки, которые INSERT может перечислить, а UPDATE — задать |
rows |
Строки, которых может касаться UPDATE или DELETE (SQL-предикат) |
check |
Условие, которому должна удовлетворять каждая строка, записанная INSERT или оставленная UPDATE (SQL-предикат) |
presets |
Колонки, которые разрешение задаёт само: из переменной сеанса, литерала или момента записи. Инструкция не может их передать. |
Если опустить rows или check, разрешение берёт фильтр строк роли на чтение для этой таблицы, так что роль пишет те строки, которые читает. "TRUE" означает любую строку.
Единое допущение решает судьбу каждой записи на любой поверхности в таком порядке: у роли есть право write; у неё есть разрешение на операцию над таблицей; каждая колонка, которую пишет инструкция, указана в разрешении; пресеты добавляются; rows присоединяется через AND к WHERE; а check вычисляется на новых строках до того, как что-либо отправлено источнику. Отказ называет таблицу, роль и операцию. (REQ-2074)
Более широким инструкциям нужны более широкие разрешения. TRUNCATE требует разрешения delete на все строки. MERGE требует разрешений insert и update на все строки. INSERT ... ON CONFLICT DO UPDATE требует разрешения update на все строки без пресетов. COPY требует разрешения insert без пресетов.
GraphQL предлагает каждой роли поле мутации ровно для тех операций, на которые у неё есть разрешения, с колонками разрешения. Дочерняя роль наследует разрешения родителя; собственные идут первыми. Мета-роль держит свёртку разрешений своих участников: объединение колонок, OR для rows и check и только те пресеты, которые разделяют все участники. В административном интерфейсе раздел Запись таблицы перечисляет и редактирует её разрешения.
Пример¶
tables:
- source_id: hr-pg
domain_id: people
schema: public
table: employees
columns:
- name: id
visible_to: [] # all can read
- name: email
visible_to: [admin, analyst, viewer]
masks:
- to: [admin] # admin sees raw value (no mask)
- to: ["*"] # everyone else sees it masked
mask: {type: regex, pattern: "(.).*@", replace: "$1***@"}
- name: salary
visible_to: [admin, hr]
masks:
- to: [admin, hr] # admin and hr see real value
- to: ["*"] # all other visible roles see 0
mask: {type: constant, value: 0}
- name: created_at
visible_to: [] # all can read
write_grants:
- role: admin
operation: update
columns: [email]
rows: "TRUE"
- role: hr
operation: update
columns: [salary]
- role: hr
operation: insert
columns: [email, salary]
presets:
- {column: created_at, source: now}
В этом примере:
email: admin видитalice@example.comи может обновить его в любой строке; analyst и viewer видятa***@example.comи писать не могутsalary: admin и hr видят реальное значение; hr может обновлять его в тех строках, которые hr может читать, потому что разрешение не задаётrows; любая другая роль с доступом на чтение видит0created_at: читать может любой, ни одна роль его не передаёт, а вставка от hr задаёт его равным моменту записи
Авторизация мутаций¶
Зарегистрированные мутации (удалённый GraphQL, OpenAPI, gRPC, Hasura) защищены двумя независимыми проверками. (REQ-868) Роль может вызвать мутацию, только если у неё есть глобальная возможность write И команда назначена ей через visible_to (пустой список назначает её каждой роли). (REQ-868) У команды нет списка writable_by; конфигурация отклоняет такой список по имени. [tool-verified: provisa/security/mutation_authz.py command_assigned, admit_command; provisa/core/models.py _one_role_list]
Мутации классифицируются как записи по контракту, а не по заявлению вызывающего. (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')"
description: Analysts read only their own region's orders.
Термин current_setting('provisa.<name>') разрешается по переменным сеанса, которые связывает запрос: user_id из аутентифицированной идентичности, role из действующей роли и каждый скалярный claim токена под его именем в нижнем регистре, дефисы как подчёркивания. Переменная сеанса Hasura сохраняет собственное имя: X-Hasura-User-Id — это hasura_user_id, читаемая из claim x-hasura-user-id токена (на верхнем уровне или внутри объекта claims Hasura, например https://hasura.io/jwt/claims) или записанная для участника как атрибут hasura_user_id. Поэтому импортированный фильтр Hasura по X-Hasura-User-Id читает provisa.hasura_user_id. provisa.user_id — всегда вошедшая идентичность; ни один claim её не заменяет. Настроенные session_vars роли — это базовые константы; привязки запроса накладываются поверх них. Роль, наследующая от другой, берёт константы своих предков под своими собственными, для каждого имени — значение ближайшей роли. user_id, role, region и org связываются запросом или узлом и не могут быть заданы как константы. В незащищённом режиме заголовок x-provisa-session-<name> связывает <name>. Переменная, нигде не связанная, разрешается в NULL и не совпадает ни с одной строкой. (REQ-1682)
Константа роли — обратное к производной роли. Производная роль превращает значение, которое есть у участника, в роль; константа роли даёт каждому запросу, действующему от этой роли, значение, которое правило строк читает так же, как claim. Константа роли может быть ссылкой ${env:NAME} или ${secret:NAME}, которая разрешается тем же механизмом, что и любая другая ссылка, когда её использует запрос; модель хранит ссылку, а не полученное значение, а ссылка, которая ни во что не разрешается, приводит к ошибке запроса с названием константы. Разрешённая константа входит в запрос. Собственные записи Provisa о запросе покрываются централизованным скрытием; собственные журналы федеративного источника вне досягаемости Provisa. (REQ-2086)
Маскировка на уровне колонок¶
Маскировка задаётся как упорядоченный список для каждой колонки. Первая запись, которую имеет читатель, решает, что он видит. (REQ-2059, REQ-263)
| Тип маски | Поддерживаемые типы колонок | SQL-выражение |
|---|---|---|
regex |
Строка (varchar, char, text) | REGEXP_REPLACE(col, pattern, replace) |
constant |
Любой | Литеральное значение (NULL, 0, своя строка) |
truncate |
Дата/метка времени | DATE_TRUNC(precision, col) |
У каждой записи есть список to, называющий читателей, на которых она направлена, и необязательная mask. Запись без mask означает «без маски» для этих читателей. * в to совпадает с любым читателем.
Выражение маски переносится в проекцию SQL SELECT. База данных возвращает маскированное значение; немаскированные данные никогда не передаются по сети для маскированных ролей. (REQ-263) Маскированные колонки также блокируются в предложениях WHERE и HAVING (защита предикатов уровня 5), чтобы не допустить вывода значений через фильтрацию. (REQ-263, REQ-531)
Маска действует на все имена столбца, но не на связанный столбец. Столбец, прочитанный по псевдониму или по физическому имени, маскируется одинаково. Маска не скрывает ключ, который утверждённая связь соединяет со столбцом, видимым роли: соединение сравнивает настоящие значения, поэтому связанный столбец показывает то, что содержит замаскированный ключ. Замаскируйте и связанный столбец или не утверждайте связь для этой роли. (REQ-263, REQ-019)
Нет записи — нет маски. Читатель, у которого нет ни одной записи, читает колонку без маски. Завершайте список записью *, когда к каждому читателю должна применяться какая-либо маска. Редактор колонок предупреждает, если в списке нет записи * (unlisted_unmasked).
Запрет порядком. Более ранняя запись побеждает, даже если у читателя есть и более поздняя, более разрешающая. Чтобы подрядчики всегда видели зарплату под маской, какие бы ещё роли у них ни были, поставьте запись подрядчиков перед записью без маски. Предоставленная роль никогда не теряет того, что даёт родитель через наследование, но запись маски, стоящая в списке раньше, применяется первой — так здесь работает ограничение.
Метароли. Цепочка метароли включает роли каждого участника, их предков и все названные производные роли. Запись, предоставленная любому участнику, достигает метароли. Запись перед ней, которую имеет другой участник, всё равно применяется первой.
Проверка доступа. POST /admin/masks/check принимает список масок и необязательный набор ролей и возвращает, какая запись применяется для этих ролей, плюс записи, которых не может достичь ни один читатель. Защищено masking_config.
# email column: analyst+region__eu sees raw; everyone else sees masked
masks:
- to: [analyst+region__eu] # composite: analyst AND in EU region
- to: ["*"] # all others
mask: {type: regex, pattern: "(.).*@", replace: "$1***@"}
# hire_date column: hr sees raw; all other visible roles see year only
masks:
- to: [hr]
- to: ["*"]
mask: {type: truncate, precision: year}
Governance ответов действий¶
Ответ отслеживаемой функции или webhook регулируется как строки таблицы. (REQ-1679) Строки, которые возвращает действие, связываются как отношение, колонки которого — объявленный выходной контракт действия (output_columns для функции, inline_return_type для webhook или свойства объекта массивного return_schema функции), и это отношение проходит тот же этап governance, что и каждое чтение таблицы, через CTE VALUES со строками. (REQ-1679) Ничего из RLS, маскировки или видимости для действий заново не реализуется.
Применяются три вещи, в том же порядке, что и для таблицы. (REQ-1679)
- Фильтр строк: правило RLS роли, сохранённое для действия по имени (
upsertRlsRuleсactionName), или, если у действия нет собственного, правило домена домена действия. Предикат проверяется по контракту при сохранении так же, как правило таблицы. (REQ-1676) - Видимость колонок: колонка контракта, объявляющая
visible_to, удаляется для роли, которую список не называет; колонка, которая ничего не объявляет, — часть публичной формы действия и остаётся. - Маски: колонка контракта несёт
masks, тот же упорядоченный список масок, что и колонка таблицы. (REQ-2059)
Наследование ролей разрешает цепочку для всех трёх. (REQ-1677) Ответ, возвращающий колонку вне объявленного контракта, отклоняется с 502, а не пропускается без регулирования. Действие, которое не объявляет контракта колонок, возвращает скаляр, и связывать ему нечего; оно возвращается как есть. Тестовый вызов администратора сообщает применённые фильтры, исключения и маски, а регулируемая инструкция записывается в журнал аудита запросов.
Выборка¶
Все роли видят результаты выборки (по умолчанию: 100 строк), если у них нет возможности full_results. (REQ-554) Управляется переменной окружения PROVISA_SAMPLE_SIZE. (REQ-554)
Журнал аудита¶
Каждый запрос, затрагивающий актив домена, записывается в query_audit_log. (REQ-596, REQ-613) Журнал аудита принадлежит организации, а не платформе: собственный реестр платформы с организациями, пользователями и приглашениями не содержит записей аудита. (REQ-1922) query_audit_log каждой организации — это таблица в собственной схеме этой организации, в хранилище, которое организация называет для своей записи в каждом выбранном ею регионе. Организация, не объявившая регионов, хранит эту схему в базе данных плоскости управления развёртывания, рядом со своей моделью.
Каждая строка фиксирует tenant_id (организация), user_id, role_id, хеш SHA-256 текста запроса, сам текст запроса в зашифрованном виде (REQ-689), table_ids и table_names, source, status_code, duration_ms, маршрут и число строк, применённые к инструкции ограничения и logged_at. Текст запроса шифруется ключом организации, если организация его задала, иначе — ключом развёртывания (REQ-1574). Развёртывание, в котором не настроен провайдер шифрования, хранит его в незашифрованном виде. Текст никогда не раскрывается в запрашиваемых операционных таблицах и расшифровывается только при авторизованных чтениях администратора.
Журнал на уровне базы данных только для добавления: правила PostgreSQL превращают DELETE и UPDATE над query_audit_log в пустые операции. (REQ-596, REQ-613) Два индекса, (tenant_id, logged_at) и (user_id, logged_at), поддерживают запросы соответствия по организации и по пользователю за интервалы времени. (REQ-596, REQ-613)
Хранение: строки хранятся бессрочно. В Provisa нет настройки срока хранения, и журнал аудита никогда не очищается; поскольку DELETE — пустая операция, удаление старых строк означает, что оператор удаляет или архивирует данные таблицы вне Provisa.
Редактирование¶
Всё, что Provisa записывает о своей работе, проходит через один список правил редактирования на организацию, прежде чем куда-либо записаться. Встроенные правила Provisa — начальный набор списка; организация добавляет свои. Каждый секрет, который хранит Provisa, и каждый секрет, на который ссылается модель, скрывается без правила, которое бы его называло: значение, полученное из ${secret:…} или из собственного хранилища Provisa, никогда не появляется в записи в открытом виде.
Список применяется на каждом выходе, куда пишет Provisa:
- каждый span и каждая запись журнала, в SDK OpenTelemetry самого Provisa, до того как их увидит любой экспортёр; включая стандартный вывод;
- таблицы аудита, при записи каждой строки;
- сообщения об ошибках, которые получает клиент.
Федеративный источник ведёт собственные журналы, которые Provisa не пишет и до которых не может дотянуться. Записи источника об инструкциях, которые ему отправляет Provisa, защищает этот источник.
Ограничение частоты¶
Лимиты частоты для каждой роли настраиваются в 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-1174)
У каждой инструкции есть оценка сложности: отношения, которые она читает, её соединения, колонки, которые она выбирает (* считается как колонки, которые он обозначает), и вложенные в неё запросы. Отношение, прочитанное из удалённого API-источника (OpenAPI, удалённый GraphQL, удалённый gRPC), считается за 10, потому что каждое чтение расходует бюджет, который удалённая система задаёт для всех пользователей этого источника.
Лимит — это max_query_complexity в rate_limit роли. Общеорганизационная настройка limits.max_query_complexity — потолок для каждой роли: роль может задать более низкий лимит, а более высокий не действует. По умолчанию ни один не задан.
Аутентификация¶
Подключаемые провайдеры аутентификации: (REQ-120)
| Провайдер | Учётные данные | Сценарий |
|---|---|---|
none |
Заголовок X-Provisa-Role | Только разработка |
basic |
Локальные учётные записи (bcrypt) + сессионный JWT | Автономные развёртывания |
firebase |
ID-токен Firebase | Продакшен |
keycloak |
JWT Keycloak | Предприятие |
oauth / oidc |
JWT OIDC | PingFed, Okta, Azure AD, Auth0 |
ldap |
Имя пользователя и пароль из каталога (LDAP или Active Directory) | Корпоративные каталоги (REQ-1265) |
saml |
Подписанное утверждение SAML 2.0, затем сессионный JWT | Корпоративный единый вход (REQ-1265) |
simple |
bcrypt + JWT | Тестирование |
ldap выполняет вход по схеме «поиск, затем привязка»: Provisa привязывается как служебная учётная запись, находит единственную запись, на которую указывает имя пользователя, затем привязывается как эта запись с предъявленным паролем, так что пароль проверяет сам каталог. Группы пользователя становятся его ролями. Учётная запись и её группы перечитываются каждый раз, когда возвращается токен сессии, поэтому пользователь, удалённый из каталога или из группы, теряет этот доступ до истечения срока токена. Поддерживаются и ldaps://, и StartTLS.
saml — это Web Browser SSO, инициируемый поставщиком услуг (SP). XML-подпись ответа проверяется по метаданным поставщика удостоверений, а claims читаются только из подписанного элемента. Проверяются Issuer, Audience, URL ACS, срок действия и InResponseTo. Неинициированные (инициированные IdP) ответы, зашифрованные утверждения, документы с DOCTYPE, множественные утверждения и подписи SHA-1 отклоняются. Сессия SAML повторно у поставщика удостоверений не проверяется: пользователь, удалённый там, сохраняет доступ до истечения срока токена сессии.
Отображение ролей: claims идентичности → роль Provisa через настраиваемые правила. (REQ-120) Поле assignments_source управляет тем, откуда берутся назначения ролей: claims читает их из claims токена JWT (по умолчанию), provisa — из внутреннего хранилища назначений Provisa. (REQ-551) Мультиарендное развёртывание (multitenancy: true) само назначает роли, по организациям, поэтому должно использовать assignments_source: provisa: с claims оно не запускается, а изменение настроек, которое привело бы к этому сочетанию, отклоняется. Одноарендное развёртывание может использовать любой вариант.
Суперпользователь, настроенный в provisa.yaml (имя пользователя плюс пароль из секрета окружения), всегда получает роль admin и все возможности независимо от настроенного провайдера — путь начальной загрузки для первоначальной настройки. (REQ-125)
Поверхности и учётные данные¶
Каждая поверхность аутентифицируется через один и тот же контракт провайдера, поэтому учётные данные, работающие на одной, работают на всех, где их может передать протокол. (REQ-124, REQ-1263) Эта таблица — единственный справочник; документация отдельных поверхностей её не повторяет.
| Поверхность | Пароль | Токен провайдера | Персональный токен доступа | Клиентский сертификат (mTLS) |
|---|---|---|---|---|
| HTTP (REST, JSON:API, GraphQL) | Authorization: Basic |
Authorization: Bearer |
Authorization: Bearer |
через терминирующий прокси |
| pgwire | поле пароля (открытый текст или SCRAM) | поле пароля, развёртывания OIDC | поле пароля | да |
| Bolt | схема basic |
схема bearer |
схема bearer |
да |
| Arrow Flight | — | token в рукопожатии или полезной нагрузке тикета; заголовок вызова authorization: Bearer для list_flights, get_flight_info и get_schema |
то же | да |
| gRPC | — | метаданные authorization |
метаданные authorization |
да |
Airport (расширение DuckDB airport, по выбору) |
— | authorization: Bearer (auth_token секрета) |
то же | — |
| MCP | — | Authorization: Bearer |
Authorization: Bearer |
через терминирующий прокси |
Единственный вариант аутентификации расширения DuckDB airport — bearer-auth_token его секрета, поэтому клиент DuckDB предъявляет там токен провайдера или персональный токен доступа и действует от роли, в которую разрешается его идентичность. Клиент, который может задавать заголовки вызова, может запросить одну имеющуюся роль или набор через запятую в x-provisa-role. (REQ-1120, REQ-1263)
Отказ сообщает свой вид на каждой из этих поверхностей. Отсутствующие, отклонённые или непригодные учётные данные — это отказ аутентификации: HTTP 401, gRPC UNAUTHENTICATED, Arrow Flight и Airport Unauthenticated. Действительные учётные данные, запрашивающие действие от роли, которой у идентичности нет, — это отказ авторизации, с названием роли: HTTP 403, gRPC PERMISSION_DENIED, Arrow Flight и Airport Unauthorized. (REQ-273, REQ-1263) Одно исключение находится в клиентской библиотеке Arrow, а не на сервере: отказ вызова Flight get_schema доходит до клиента Python как общая ошибка Arrow с причиной сервера, но без её вида; list_flights, get_flight_info и do_get передают вид.
Там, где в ячейке —, протокол не несёт поля имени пользователя, к которому можно было бы приложить пароль; это покрывают формы токенов. pgwire — зеркальный случай: в стартовом пакете одно поле секрета и нет схемы, поэтому метод выбирает то, чем секрет является: PAT распознаётся по префиксу, секрет читается как bearer-токен, если настроенный провайдер — провайдер токенов, а всё прочее — пароль. Выбор делается один раз — учётные данные, которые отклонил выбранный валидатор, не проверяются повторно другим.
Матрицу обеспечивает tests/unit/test_auth_surface_conformance.py, который вызывает настоящую точку входа проверки каждой поверхности и падает, если поверхность добавлена без строки.
Персональные токены доступа¶
PAT — это долгоживущий bearer-секрет, который пользователь создаёт для клиента, не способного пройти интерактивный вход, — скрипта, BI-инструмента, драйвера. (REQ-1263) Он несёт собственные организацию и роль, и каждая поверхность разрешает его через тот же валидатор, поэтому ни одной поверхности не нужно знать, что такое PAT.
Форма передачи — provisa_pat_, за которым следуют 43 символа base64, безопасных для URL. Префикс — это то, что направляет предъявленный секрет в хранилище токенов, а не к провайдеру идентичности, и делает утёкший токен находимым в журналах и репозиториях.
- Хранение — хранится только SHA-256 секрета. Сам секрет показывается ровно один раз, при создании, и не может быть восстановлен. Перечень несёт отображаемый префикс и метки времени жизненного цикла, но никогда работающие учётные данные.
- Выпуск и отзыв —
POST /auth/tokens,GET /auth/tokens,DELETE /auth/tokens/{token_hash}, а также раздел самообслуживания в собственном профиле пользователя в интерфейсе администратора. Создание и отзыв учётных данных — действие владельца токена. - Атрибуция — проверенный PAT разрешается в учётную запись владельца: id пользователя, email и отображаемое имя. Поэтому строка аудита или отчёт об использовании, записанные под PAT, называют человека, а не учётные данные. Какой из токенов этого человека действовал, передаётся отдельно, в
raw_claims["token_name"]. - Срок действия — у токена может быть срок действия; просроченный токен отклоняется при проверке. Удаление членства пользователя отзывает вместе с ним его токены.
SCRAM-SHA-256 в pgwire¶
С провайдером basic настройка auth.scram: true заставляет pgwire предлагать SASL (код аутентификации 10) с механизмом SCRAM-SHA-256, так что пароль доказывается, а не передаётся. (REQ-1394) Привязка канала (SCRAM-SHA-256-PLUS) не предлагается.
SCRAM нужен верификатор RFC 5802, который нельзя вывести из хеша bcrypt. Верификатор записывается всякий раз, когда пароль проходит в открытом виде — при регистрации, входе, смене пароля, сбросе администратором, — поэтому развёртывание, включившее SCRAM, собирает верификаторы по мере следующей аутентификации пользователей, и первое SCRAM-подключение каждого пользователя следует за его следующим вводом пароля. Пользователю, у которого ещё нет верификатора, отвечают имитационным обменом, неотличимым от настоящего, чтобы сеть не раскрывала, кто уже перешёл.
Взаимный TLS¶
Проверка клиентского сертификата переносит первую проверку в рукопожатие TLS: вызывающий без сертификата, подписанного CA развёртывания, никогда не достигает уровня учётных данных. (REQ-1228) Она доступна в pgwire, Bolt, gRPC и Arrow Flight — четырёх транспортах, которые сами завершают свой TLS.
| Переменная | Значение |
|---|---|
PROVISA_MTLS_CLIENT_CA |
PEM-пакет CA, которым разрешено подписывать клиентские сертификаты |
PROVISA_MTLS_MODE |
required (по умолчанию, как только задан CA) или optional |
PROVISA_MTLS_BIND_PRINCIPAL |
Если true, общее имя сертификата должно совпадать с именем пользователя, под которым затем аутентифицируется соединение |
Переопределения для отдельных протоколов следуют тому же именованию, что и настройки TLS. Ничего не выводится: режим, заданный без CA, отказывается запускаться, а нераспознанный режим отказывается запускаться, а не читается как самый безопасный соседний — развёртывание, которое считает, что требует клиентские сертификаты, но не требует, в худшем положении, чем то, которое не запускается.
Версии TLS¶
Каждый слушатель, на котором Provisa завершает TLS (HTTP API и UI, pgwire, Bolt, MCP, gRPC и Arrow Flight), принимает TLS 1.2 и TLS 1.3 и отклоняет TLS 1.0 и 1.1. Provisa не задаёт собственную версию протокола или список шифров. Слушатели HTTP, pgwire и Bolt используют серверные значения по умолчанию модуля ssl Python (минимум TLS 1.2). gRPC и Arrow Flight используют значения по умолчанию gRPC (минимум TLS 1.2). Версии не настраиваются.
Ограничение попыток входа¶
Подбор паролей не зависит от протокола: одну и ту же учётную запись можно атаковать по HTTP, pgwire и Bolt. Поэтому счётчик находится на уровне проверки учётных данных, а не на какой-либо одной поверхности, так что блокировка, полученная где угодно, действует везде. (REQ-1393)
Оно включено по умолчанию — пять неудач за пять минут блокируют субъекта на пятнадцать минут — и настраивается в auth.login_throttle. Заблокированный субъект отклоняется ещё до проверки учётных данных, а успешная аутентификация очищает историю этого субъекта.
Ключ — это принципал, которого несёт протокол. Поверхность только с bearer не несёт принципала, поэтому ключ — дайджест самих учётных данных; это не даёт повторять плохой токен без ограничений. Хранилище — в пределах процесса, поэтому развёртывание с несколькими воркерами API допускает до max_attempts на воркер — ограничение тормозит подбор, а не является распределённой квотой.
Зондирование ролей¶
Указание роли, которой у вызывающего нет, считается обнаруженным событием безопасности высокой важности на каждой поверхности: в HTTP API, MCP, pgwire, Bolt, Arrow Flight, службе Flight airport и gRPC. Легитимный клиент называет роли, которые ему выданы; клиент, называющий другую, либо неисправен, либо зондирует. (REQ-2076)
Запрос отклоняется. Вызывающему сообщается только «not permitted», без сведений о том, существует ли роль, а соединение или сеанс завершается там, где он есть: соединение pgwire, сеанс Bolt, вызов gRPC, сеанс MCP. Исключение — HTTP API с любыми учётными данными, кроме личного токена доступа: браузер предъявляет там свой сеанс или токен своего поставщика удостоверений, поэтому отказ по-прежнему называет роль.
Что происходит дальше, зависит от учётных данных и транспорта:
| Учётные данные | Транспорт | Приостанавливается |
|---|---|---|
| Личный токен доступа | любой | токен |
| Токен, подписанный внешним поставщиком удостоверений | MCP, pgwire, Bolt, Flight, gRPC | доступ субъекта к организации по токену |
| Токен, подписанный внешним поставщиком удостоверений | HTTP API | ничего |
| Сеанс или пароль, которые проверяет само развёртывание | любой | ничего |
| Локальный сеанс MCP (без bearer) | MCP | ничего; событие записывается в журнал оператора |
HTTP API — исключение, потому что браузер предъявляет тот же bearer-токен, что и API-клиент, и сервер не может их различить, а вкладка, всё ещё называющая роль, которую пользователь уже потерял (после того как администратор изменил его роли или после смены организации), — обычное дело. MCP и остальные транспорты никогда не являются сеансом браузера. Там событие отклоняется, записывается и сообщается как любое другое; не выполняется только приостановка.
Приостановленный токен отклоняется так же, как отозванный. Субъект, чей доступ по токену приостановлен, получает отказ в MCP, pgwire, Bolt, Flight и gRPC в этой организации и по-прежнему может входить в интерфейс. Снять приостановку может только администратор; по истечении времени она не снимается. Команда → Приостановленный доступ показывает каждую приостановку с событием, которое её вызвало, и снимает её (GET /admin/orgs/{org}/suspensions, POST /admin/orgs/{org}/suspensions/lift).
Каждое событие записывается в административный журнал организации (действие security.role_probe) с субъектом, идентификатором учётных данных, адресом клиента, транспортом, конечной точкой или инструментом, названной ролью, имеющимися ролями, временем и идентификаторами последних проверенных аудитом запросов субъекта. Их текст остаётся в журнале аудита запросов и читается с правом view_governance.
Администраторы организации получают письмо там, где развёртывание отправляет почту. Событие также отправляется в формате JSON на webhook безопасности оператора, если задан security.webhook_url. То, что покидает организацию, называет субъекта, идентификатор учётных данных, транспорт, конечную точку, названную роль, время и важность, со ссылкой на административное представление; оно не содержит ни запросов, ни имеющихся ролей. Уведомление, которое не удалось доставить, записывается в журнал и никогда не отменяет отказ или приостановку.
Указание организации¶
В мультиарендном режиме каждый запрос называет организацию, для которой он предназначен. Принадлежность ровно к одной организации её не называет: запрос, который не называет никакой, отклоняется, и отказ говорит, что отправить. (REQ-1235) Каждая поверхность называет организацию так:
| Поверхность | Как клиент называет организацию |
|---|---|
| HTTP (интерфейс, REST, GraphQL) | Собственное имя хоста организации, acme.provisa.dev, или заголовок X-Org-Provisa на хосте плоскости управления |
| pgwire | Имя базы данных (psql -d acme, поле базы данных BI-инструмента) или собственное имя хоста организации, набранное по TLS |
| Bolt | Имя базы данных, <org>.provisa_<role> (или <org>.provisa_ops_<role>), или собственное имя хоста организации, набранное по TLS |
| Arrow Flight | "org" в тикете |
| gRPC | Заголовок метаданных x-provisa-org |
| MCP (транспорт HTTP) | Собственное имя хоста организации или заголовок X-Org-Provisa |
| Клиент Python | org= в ProvisaClient, connect() и adbc_connect(); ?org= в URL SQLAlchemy |
Клиент pgwire, который набирает имя хоста по TLS, отправляет его в TLS ClientHello, и Provisa читает организацию оттуда (REQ-1234). Когда имя хоста и имя базы данных оба называют организацию и различаются, соединение отклоняется; ни одно не имеет приоритета. Имя базы данных provisa, используемое по умолчанию, не называет организацию, и BI-инструмент, подключённый к организации, показывает
эту организацию как свою базу данных. В Bolt SHOW DATABASES перечисляет по одному <org>.provisa_<role> для каждой организации, к которой
принадлежит пользователь; просто provisa_<role> в мультиарендном режиме отклоняется, а сеанс остаётся в
организации, которую назвал первой.
Имя — это просьба, а не предоставление. Везде оно попадает в один и тот же резолвер, который отклоняет организацию, участником которой аутентифицированный принципал не является и на которую не имеет межорганизационного права.
Персональный токен доступа выпускается для одной организации и сам её называет. Он открывает только эту организацию: запрос, называющий другую, отклоняется, даже если владелец токена принадлежит обеим.
У одноарендного развёртывания нет организации, которую нужно называть. Ни одна поверхность там не читает для этого имя хоста, имя базы данных или заголовок.
Секреты между организациями¶
Секреты организации читает только эта организация. В собственном хранилище Provisa каждый секрет хранится под своей организацией. В центральном сервисе (Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) развёртывания со многими организациями каждое чтение выполняется внутри префикса читающей организации, а ссылка, которая вышла бы за его пределы, отклоняется. (REQ-1557) Префиксы и шаги по переносу существующих секретов под них описаны в разделе Секреты.
Телеметрия разных организаций¶
В развёртывании, которое разделяют многие организации, операционные сигналы ("ops"."traces", а на движке Trino — метрики и журналы рядом с ним) — это одно хранилище, куда поступает работа каждой организации. Каждый span записывает организацию, для которой он был начат, сохранённую как tenant_id, и каждое чтение сигналов возвращает только строки читающей организации, какой бы ни была роль. Span, начатый вне работы какой-либо организации (например, при запуске сервера), не принадлежит ни одной и никем не читается. Администратор платформы не имеет доступа к данным ни в одной организации и не читает ни одну из них через Provisa. (REQ-1349, REQ-1327)
Операционные сигналы хранятся бессрочно на обоих движках, как и журнал аудита: уплотнение переписывает файлы, в которых они хранятся, и не удаляет ни одной их строки. В Trino это таблицы Iceberg каталога otel. На нативном движке (DuckDB) сервер складывает собственные записи трассировки в виде файлов Parquet в своей папке телеметрии, а движок читает их под тем же именем, "ops"."traces", с представлением происхождения обновлений "ops"."mv_refresh_lineage" поверх них; оба читаются по тому же правилу для каждой организации. (REQ-862)
Сигналы хранятся в своём хранилище в открытом виде. Нагрузка, которой нужно больше, чем граница по организациям внутри одного развёртывания, работает в выделенном развёртывании; особо чувствительная или регулируемая — в самостоятельно размещённом.
Фоновая работа разных организаций¶
Процесс, обслуживающий много организаций, выполняет фоновую работу каждой от имени этой организации. Обработка изменений каждой организации (цикл событий, который загружает изменения источников и обновляет представления поверх них), её построения реплик и срабатывающие от них триггеры таблиц принадлежат её собственному рантайму: изменение обрабатывает задание её собственной организации, оно читает свой источник с адресом и учётными данными этой организации и пишет в хранилище этой организации. Каждая среда организации держит собственный слот логической репликации PostgreSQL и публикацию, названные по организации, среде и источнику, поэтому две организации, одинаково назвавшие источник в одной базе, читают каждая свои изменения.
Резидентность данных¶
В развёртывании с регионами таблица может назвать один регион своим домом, и её персональные данные хранятся и обрабатываются там. (REQ-1921, REQ-1922)
- Только домашний регион строит и обновляет реплику таблицы. Любой другой регион читает эту реплику на месте, с правилами строк и столбцов читателя, применяемыми в домашнем регионе, поэтому из него выходит только то, что читателю разрешено видеть.
- Домашний регион, чья реплика не построена или чьи хранилища недоступны, отказывает в чтении (
query.home_region_unavailable). Источник никогда не читается вместо этого напрямую из другого региона. - Черновик просматривается только там, где он живёт. Таблица-черновик, называющая регион, читается для предпросмотра её ответственного только на узлах этого региона и отклоняется в других местах; копия, которую загружает предпросмотр, хранится там до её выпуска. Копия таблицы (черновика или опубликованной), которую держит любой регион, когда её дом теперь другой регион, выводится из обращения и затем удаляется.
- Чтобы задать, изменить или убрать регион таблицы, нужно право
data_residencyдля затронутых регионов, помимо владения таблицей через её домен. Перенос может пройти через черновик, так что в промежутке таблица нигде не обслуживается.
Правила строк и столбцов, написанные на регионе отвечающего узла (current_setting('provisa.region')) и на атрибуте участников region, решают, что переходит между регионами. Регионы подробно описывают регионы, назначение дома и эти правила.
Укрепление развёртывания¶
Учётные данные хранилищ и адрес, на котором они слушают¶
У встроенных хранилищ нет учётных данных по умолчанию. Плоскость управления PostgreSQL, Redis, MinIO, административная учётная запись Grafana и общий секрет кластера Trino получают каждый значение, сгенерированное для развёртывания:
- Запускающие средства (
provisa start, установщики для рабочего стола и сервера) генерируют их один раз и хранят в~/.provisa/store-credentials.env(режим 600). Значение, уже заданное в окружении, используется как есть, а вторичный узел, использующий хранилища главного, отказывается запускаться без значений главного. - Модули Terraform генерируют их через
random_passwordи передают одни и те же значения каждому узлу. Они хранятся в состоянии Terraform, поэтому защищайте состояние так же, как сами пароли. - Helm-чарт не задаёт ничего. Ему нужен созданный вами Secret, имя которого задано в
credentials.existingSecret, с ключамиpg-password,redis-password,minio-access-key,minio-secret-key,grafana-admin-passwordиtrino-shared-secret.
Redis требует свой пароль, а пароль роли PostgreSQL задаётся заново при каждом запуске, чтобы ранее созданный том следовал текущему паролю развёртывания. Хранилища, федеративный движок и службы телеметрии публикуют свои порты на одном адресе, PROVISA_STORE_BIND_IP: 127.0.0.1 на отдельном узле или частный адрес главного узла, когда к нему подключаются вторичные. Административная учётная запись Grafana получает сгенерированный пароль; стек compose позволяет анонимным посетителям просматривать дашборды, а Helm-чарт не допускает анонимных посетителей.
Шифрование транспорта¶
Каждый обслуживаемый Provisa слушатель работает по TLS (см. Версии TLS). Серверный узел использует сертификат из PROVISA_TLS_CERT/PROVISA_TLS_KEY или генерирует самоподписанный. Helm-чарт по умолчанию работает по TLS (tls.enabled: true), используя Secret типа kubernetes.io/tls, имя которого задано в tls.secretName.
Встроенный PostgreSQL¶
Настольный и нативный уровни запускают PostgreSQL встроенным. В macOS и Linux он слушает unix-сокет внутри своего каталога данных (режим 700), доступный только учётной записи, которой принадлежит каталог. По TCP (Windows, где он слушает 127.0.0.1) каждое соединение должно предъявить пароль, сгенерированный для каталога данных и хранящийся в нём (provisa.password, режим 600). Он никогда не запускается от root.
Федеративный движок (Trino)¶
- Между узлами. Координатор и его рабочие узлы аутентифицируют друг друга общим секретом развёртывания, сгенерированным для него (
internal-communication.shared-secret). Шифрование их трафика — опция оператора, по умолчанию выключенная: пока она выключена, трафик остаётся внутри частной сети или кластера развёртывания. Включайте её там, где требование соответствия или нулевого доверия предписывает шифрование внутри развёртывания. Trino документирует, что оно замедляет запросы с интенсивным обменом данными (распределённые соединения, агрегации, оконные функции) от 10% до более чем 100%. В развёртывании compose это настройкаengine.trino_internal_tls(Deployment settings → Engine), рядом с которой сеть движка и закреплённый адрес координатора; она применяется при следующем запуске. В Helm-чарте это значениеtrino.internalTls; chart генерирует CA и сертификат, которые используют узлы, сохраняет их между обновлениями и адресует каждый узел по DNS-имени его пода. Движки размещённой платформы — одноузловые координаторы, без трафика между узлами, и они отклоняют эту опцию. (REQ-2072) - Буферизуемые данные. Отказоустойчивое выполнение шифрует буферизуемые данные обмена ключом, генерируемым для каждого запроса и отбрасываемым по его завершении (
fault-tolerant-execution.exchange-encryption-enabled=true, задано явно). Устаревший сброс на диск не используется. - Организации на одном движке. Каталоги каждой организации принадлежат ей и названы по организации, а выражения каждой организации выполняются в её собственной группе ресурсов.
- Каталоги и учётные данные в них хранятся только в памяти движка. Provisa создаёт каждый каталог Trino во время работы, и каждый хранит свойства подключения своего источника, включая пароли. Каждый движок, который запускает Provisa, держит их в памяти (
catalog.store=memory): ни один каталог не записывается на диск движка и ни один не монтируется из файла. Перезапущенный координатор возвращается без каталогов; каждый процесс Provisa замечает это при следующем запросе, а наблюдатель движка — в течение минуты, и заново создаёт обслуживаемые им каталоги. Источнику Kafka с учётными данными SASL нужен один файл: коннектор Kafka в Trino читает свои настройки клиента только из файла, поэтому Provisa записывает их, включая пароль, вkafka-client/<catalog>.properties. Файл доступен для чтения только пользователю Trino (режим 0600 в каталоге 0700). В стеке контейнеров каталог — это том в памяти, общий для API и движка. На движках размещённой платформы это собственный Secret Kubernetes движка, хранящийся в памяти узла и доступный для чтения только группе Trino (режим 0440). В обоих случаях файл никогда не попадает на диск. На размещённых движках источник Kafka должен указывать реестр схем: источник, столбцы топиков которого объявляет только модель, отклоняется с указанием имени при сохранении или применении, так как его описаниям таблиц понадобился бы каталог, который нельзя выдать работающему движку. При запуске стека удаляются файлы каталогов и клиента Kafka, оставленные на диске прежними выпусками.
Источники на общем развёртывании¶
На развёртывании, где много организаций, источники организации могут обращаться только к публичным адресам: никогда к частной сети платформы, где находятся её плоскость управления, кеш и объектное хранилище. Проверка выполняется при сохранении источника — в UI, через административный API или при применении конфигурации, — а также при каждом соединении и каждом HTTP-перенаправлении. Источники демонстрационной песочницы дополнительно привязаны к адресу, по которому они были проверены. Собственная организация развёртывания, источники которой настраивает оператор, этим не ограничена.
Шифрование в покое и собственные ключи организации¶
Шифрование под собственным ключом организации доступно и выключено, пока организация его не включит. Ничего не шифруется под ключом организации, пока администратор организации не создаст его.
Ключ шифрования организации (REQ-1574) оборачивает секреты организации, её зашифрованные колонки и текст запросов в её журнале аудита. Администратор организации создаёт его в Admin → Org Secrets → Org Key или через PUT /admin/org-encryption: без тела Provisa генерирует ключ, который никогда не показывается, а тело key_b64 задаёт 32 байта вашего собственного. GET /admin/org-encryption сообщает только его отпечаток. Без ключа организации данные организации оборачиваются ключом развёртывания (encryption.provider в provisa.yaml). Развёртывание, где не настроен провайдер шифрования, хранит зашифрованные колонки и текст запросов аудита в незашифрованном виде; секреты в открытом виде не хранятся никогда (см. Секреты).
Папка организации (REQ-1992) содержит её загруженные и принятые файлы, файлы сброса её движка и её временные файлы. Администратор организации включает её шифрование в Admin → Org Secrets → File folder encryption или через PUT /admin/org-files ({"encrypted": true, "key_reference": "${secret:NAME}"}). POST /admin/org-files/generate-key один раз возвращает новый 256-битный ключ, который нужно сохранить в хранилище секретов. Ключ остаётся в собственном хранилище организации (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault или HashiCorp Vault): Provisa хранит только ссылку, а gocryptfs держит файлы папки на диске зашифрованными этим ключом. Для этого нужен FUSE, то есть хост или контейнер Linux с /dev/fuse; в macOS настройка отклоняется.
На самоуправляемом развёртывании, которое обычно запускает одну организацию на собственных движках, включение обоих шифрует секреты, файлы, файлы сброса и временные файлы этой организации под её ключами:
- Движок DuckDB, который запускает организация, сбрасывает данные в папку организации.
- Строки, которые извлекает фильтр строк, и загруженный код размещаются во временной области папки организации.
- Отказоустойчивое выполнение Trino шифрует буферизуемые данные обмена ключом, генерируемым для каждого запроса и отбрасываемым по его завершении (
fault-tolerant-execution.exchange-encryption-enabled=true). Provisa не включает устаревший сброс Trino на диск. - Trino держит каталоги организации с их учётными данными в памяти и не записывает ни один на диск (
catalog.store=memory); настройки клиента SASL источника Kafka лежат в файле, который читает только пользователь Trino, в стеке контейнеров — в памяти (см. Движок федерации). - Федеративный движок PostgreSQL записывает свои временные файлы там, где их хранит этот сервер PostgreSQL, под его собственным шифрованием хранилища, а не в папку организации.
Общая и выделенные размещённые платформы¶
Общая размещённая платформа изолирует организации логически. Одно развёртывание обслуживает всех, а данные каждой организации остаются её собственными благодаря governance, собственным схемам, каталогам и группам ресурсов на движке, собственным ключам, ключам для каждого запроса для буферизуемых данных движка и отказу в доступе её источников к частным адресам платформы. Она рекомендуется для экспериментов и малого бизнеса. Выделенная размещённая платформа физически отделена: собственные машины, сеть и хранилища, обслуживающие одного клиента. Для особо чувствительных или регулируемых нагрузок размещайте Provisa самостоятельно внутри своего периметра, где ваши собственные операторы владеют каждым ключом и каждым хостом. У размещённых платформ сегодня нет отчёта SOC 2. Мы изучаем SOC 2 и сообщим о ходе работ. Если вам нужен SOC 2, мы рекомендуем самостоятельное размещение.
Python в браузере¶
Вкладки Python и Notebook в Explore запускают Python в браузере посетителя. Каждое чтение уходит на сервер от имени вошедшего пользователя, с его действующей ролью, организацией и средой, и управляется точно так же, как та же инструкция от любого другого клиента.
Режим высокой безопасности¶
security.mode: high в provisa.yaml утверждает одну гарантию: бэкенд Provisa никогда не обрабатывает данные в открытом виде. (REQ-693) Каждая значимая колонка зашифрована в источнике, и прочитать её может только клиент, у которого есть ключ расшифровки. У этой гарантии есть последствия, которые развёртывание должно предусмотреть.
Что делает режим:
- Endpoint-ы данных требуют доказательства расшифровки на стороне клиента. Всё под
/data/возвращает 403, если вызывающий не предъявляет заголовокX-Provisa-KMS-Key— признак клиента JDBC или Python, настроенного на локальную расшифровку. Браузер или REST-потребитель открытого текста такого ключа не несёт и отклоняется. Контроль — это запрет по умолчанию на всё дерево: маршрут, добавленный завтра, защищён в день выпуска, а исключение нужно обосновать. - Endpoint-ы метаданных схемы остаются открытыми.
/data/sdl,/data/introspection,/data/schema-version,/data/domains,/data/protoи/data/compileне возвращают данных строк, а клиент должен прочитать схему — в том числе какие поля@encrypted— прежде чем сможет подключиться. - gRPC и Arrow Flight продолжают обслуживать, с тем же доказательством. Это транспорты, которыми на деле пользуются шифрующие клиенты; их закрытие оставило бы развёртывание высокой безопасности без протокола передачи. Вызов данных в любом из них должен нести тот же ключ KMS как метаданные вызова.
- pgwire, Bolt и MCP не запускаются. Ни у одного из трёх нет рукопожатия на соединение, способного нести контекст расшифровки: набор строк pgwire и результат Cypher передаются открытым текстом, а вызов инструмента MCP отдаёт свои результаты модели как текст. Настроенный порт любого из них отклоняется при запуске, а не обслуживается.
- Защиту связей нельзя обойти. И
ignore_relationships, иrelationship_guard: falseигнорируются; см. Governance связей.
Как проверить, что развёртывание в этом режиме: журнал запуска называет его, запрос /data/sql без ключа KMS отвечает 403 с сообщением, называющим REQ-693, а порты pgwire, Bolt и MCP не слушают.
Хук утверждения ABAC¶
Необязательный внешний хук политики, срабатывающий перед выполнением запроса. (REQ-203) Если он настроен, Provisa обращается к вашему движку политик с идентичностью пользователя, ролями, таблицами, колонками и операцией. Ответ определяет, продолжится ли запрос. (REQ-203) Команда, зарегистрированная с Требует одобрения, передаётся тому же хуку перед каждым вызовом; см. Вызовы команд. (REQ-1924)
Область действия¶
Хук срабатывает только когда запрос затрагивает включённую таблицу или источник — никаких накладных расходов для всего остального. (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;
map<string, string> session_vars = 6;
string command = 7; // a command call's name; empty for a query
string arguments_json = 8; // a command call's arguments as a JSON object
}
message ApprovalResponse {
bool approved = 1;
string reason = 2;
}
Запрос / ответ¶
Все три транспорта несут одинаковую полезную нагрузку: (REQ-246)
| Поле | Тип | Описание |
|---|---|---|
user |
string | Идентичность аутентифицированного пользователя |
roles |
string[] | Роли Provisa пользователя |
tables |
string[] | Id таблиц, на которые ссылается запрос |
columns |
string[] | Колонки, выбранные в запросе |
operation |
string | "query" или "mutation"; "command" для вызова команды |
command |
string | Имя команды при вызове команды; пусто для запроса |
arguments |
object | Аргументы, с которыми вызывается команда; пусто для запроса |
Транспорты webhook и Unix-сокет обмениваются JSON, с command и arguments в качестве ключей. В gRPC аргументы передаются как текст JSON arguments_json. Ответ должен содержать approved (bool) и, по желанию, reason (string). (REQ-246)
Вызовы команд¶
Команда с установленным requires_approval (переключатель Требует одобрения в форме команды) перед каждым вызовом идёт к хуку, на любой поверхности. Она выполняется, только если хук одобряет. Запрос несёт operation: "command", имя команды и её аргументы; tables и columns пусты. (REQ-1924) [tool-verified: provisa/api/data/action_exec.py _require_approval (operation="command", tables=[], columns=[], command=fn["name"], arguments=args)]
{
"user": "analyst",
"roles": ["analyst"],
"tables": [],
"columns": [],
"operation": "command",
"command": "create_issue",
"arguments": {"input": {"title": "Crash on save"}}
}
session_vars; values shown are placeholders]
Два отказа, оба 403:
functions.approval_unavailable: хук не настроен. В отличие от запроса, команда, которой нужно одобрение, без него никогда не пропускается.functions.approval_denied: хук ответилapproved: false. Причина — в сообщении.
Тайм-аут и запасной вариант¶
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) Автоматический выключатель (по умолчанию: размыкается после 5 последовательных ошибок, полуоткрыт через 30 с) предотвращает каскадные сбои из-за медленного endpoint хука. (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)
Полное описание сервиса секретов — хранилища, синтаксис ссылок и провайдеры — см. в разделе Секреты.