Модель безопасности¶
Каждый запрос проходит одно и то же управление доступом, на каком бы языке он ни был написан (GraphQL, SQL, Cypher) и каким бы путём ни пришёл (REST, gRPC, Arrow Flight, JDBC, WebSocket). Обойти его нельзя.
Проверки выполняются по порядку. Запрос должен пройти каждую, прежде чем дело дойдёт до следующей.
Многоуровневая модель¶
Уровень 0 — Фильтрация интроспекции¶
Роль видит только те таблицы, до которых достаёт её domain_access, и те столбцы, которые разрешают её правила visible_to. Всего остального для этой роли не существует: его нельзя запросить, автодополнить или угадать. Схема GraphQL, каталог SQL и обозреватель схемы в редакторе запросов показывают одно и то же суженное представление.
См. Видимость схемы.
Уровень 1 — Публичный доступ¶
Столбец, опубликованный с областью public, отдаётся каждой роли организации, даже той, чей domain_access не достаёт до домена таблицы. Такая роль видит таблицу с её публичными столбцами и ничего больше. Ничто не становится публичным, пока этого не решит тот, кто зарегистрировал таблицу. См. Публичные столбцы.
Уровень 2 — Доступ по доменам¶
У каждой роли есть список domain_access с идентификаторами доменов, и запрос, затрагивающий таблицу другого домена, отклоняется ещё до выполнения. Это грубая граница владения. Роль отдела кадров не доберётся до финансовых таблиц, как бы ни был написан SQL.
См. Модель прав.
Уровень 3 — Безопасность на уровне строк¶
Когда доступ по доменам подтверждён, фильтр строк роли для каждой таблицы добавляется в каждый SELECT как предикат WHERE и вычисляется по исходным данным. Региональный менеджер, выполнивший SELECT * по общей таблице заказов, всё равно получит только строки своего региона.
См. Безопасность на уровне строк (RLS).
Уровень 4 — Видимость и маскирование столбцов¶
Столбец, в списке visible_to которого нет вашей роли, исключается из результата. У маскированного столбца значения заменяются до того, как покинут сервер, — регулярным выражением, константой или усечением. Это действует во всех языках запросов и всех форматах вывода.
См. Модель прав на столбцы и Маскирование столбцов.
Уровень 5 — Защита предикатов¶
Фильтровать по маскированному столбцу нельзя: WHERE и HAVING отвергают его уже при разборе запроса. Иначе вызывающий мог бы восстановить настоящее значение двоичным поиском, фильтр за фильтром, хотя каждый увиденный им результат был замаскирован.
Управление связями (V002)¶
Соединение в SQL должно идти по зарегистрированной и одобренной связи, любое другое соединение отклоняется. У каждой связи есть причина и описание, которые объясняют людям и агентам, зачем этот путь существует. Считайте это политикой управления, а не границей безопасности. Уровни 2–5 действуют при любых соединениях, поэтому обход проверки не раскрывает ничего, чего роль не смогла бы прочитать двумя отдельными запросами. Попытки обхода записываются в журнал.
Пройти мимо V002 можно двумя способами. Первый — возможность: роль с ignore_relationships может соединять отношения, которых нет в каталоге. Из предустановленных ролей она есть только у modeler, потому что её задача — выстроить модель; у analyst её нет.
Второму нужны два условия сразу:
- Флаг роли:
relationship_guard: falseв определении роли (по умолчаниюtrue). - Отказ на уровне запроса: комментарий
--relationship-guard=falseв SQL.
По отдельности ни одно из них ничего не делает.
Режим повышенной безопасности закрепляет проверку. При security.mode: high не работает ни один из способов. ignore_relationships и relationship_guard: false игнорируются, и каждое соединение должно быть в одобренном каталоге. Избыточность намеренная: рабочая роль, получившая эту возможность по ошибке, всё равно не сможет выйти за пределы модели.
GraphQL пропускает V002. Его связи берутся из модели, поэтому уже одобрены.
SQL и Cypher применяют V002 по умолчанию, с описанным выше отказом по двум условиям.
pgwire применяет ту же проверку, что и SQL. Комментарий --relationship-guard=false удаляется, прежде чем запрос попадёт в базу данных.
Уровни складываются. У роли с доступом по доменам, фильтрами строк и маскированными столбцами всё это действует одновременно. Новый источник, столбец или связь не требуют править каждое правило: каждый уровень настраивается отдельно и применяется к любому запросу, который доходит до того, чем он управляет.
Модель прав¶
Независимо назначаемые возможности с необязательной иерархией ролей через parent_role_id. admin даёт всё.
| Возможность | Описание |
|---|---|
source_registration |
Регистрировать источники данных |
table_registration |
Регистрировать таблицы и колонки |
create_relationship |
Определять связи FK |
access_config |
Настраивать RLS и маскировку |
query_development |
Выполнять запросы |
write |
Вызывать зарегистрированные мутации (грубый контроль; см. Авторизация мутаций) |
full_results |
Обходить ограничения выборки |
ignore_relationships |
Обходить governance связей (V002). Из системных ролей есть только у modeler, а в режиме высокой безопасности полностью игнорируется |
admin |
Суперпользователь — даёт всё |
Наследование ролей¶
Роли могут наследовать от одной родительской роли через parent_role_id, заданный в конфигурации или на странице Безопасность как «Наследует от». Цепочка сворачивается при запуске: потомок получает объединение возможностей и доступа к доменам своих предков, каждое предоставление колонки, метрики, функции и webhook, называющее предка, и правила RLS предков для каждой таблицы с приоритетом потомка. Ближайшая роль в цепочке с правилом для таблицы даёт предикат этой таблицы, а правило, сохранённое для потомка, заменяет правило родителя для этой таблицы.
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) сворачивает права предка в потомка при запуске. Права только добавляются; вычитания нет. Чтобы ограничить читателя, предоставляйте составной записи, которая называет и роль, и условие, либо поставьте запись маски перед разрешающей.
Истекающие и неиспользуемые назначения¶
Назначение, которое хранит Provisa, может закончиться. Назначение со сроком действия перестаёт действовать с этого момента для любого запроса. Роль может указать inactivity_days: назначение этой роли, от которого ни один запрос не зависел дольше этого срока, считая от более позднего из последнего использования и создания, удаляется. Роли из утверждений (claims) или сопоставлений ролей поставщика удостоверений ограничивает по времени поставщик; задайте их срок там. Производные роли и метароли никогда не истекают.
Запрос зависел от роли, если эта роль одна обслужила его или, когда запрос действует от имени нескольких ролей, если роли предоставлена таблица, которую прочитала инструкция. Действие от имени «Все» не считает использованной каждую имеющуюся роль. Последнее использование назначения записывается не чаще раза в день.
Каждые 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 и действует от их имени.
На странице «Роли» каждый объявленный атрибут — это родительская роль с пометкой производная, а её роли значений перечислены под ней по мере появления их значений. + Роль → Производная от атрибута объявляет такую роль. Её панель показывает атрибут, его описание, уже встреченные значения и каждую маску, правило строк и предоставление, которые называют его или одну из его ролей значений. Элементов управления предоставлением у неё нет. Описание роли значения генерируется: «Есть, когда region пользователя равен eu».
Объявите их в конфигурации списком:
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 разрешённых фильтров каждого участника; если у какого-либо участника, читающего таблицу, нет фильтра, его нет и у метароли — отсутствие фильтра означает все строки. Метароль никогда не сохраняется, не перечисляется и не экспортируется. Она пересобирается из своих участников при изменении модели.
Столбцы каждой строки маскируются и показываются по ролям, чей фильтр строк допустил эту строку: из этих ролей применяется наименее ограничительное чтение столбца, а роль, не допустившая строку, никак не влияет на то, как она читается. Маски применяются там, где читается таблица, до любого соединения или агрегации, поэтому агрегация читает замаскированные значения. Записи проверяются так же: столбец записывается в строку только там, где роль, предоставляющая этот столбец, допускает строку.
Запись аудита называет каждую действующую роль, а не id метароли. Поскольку метароль никогда не сохраняется, у неё нет собственного описания: везде, где оно показывается, её описание — это описания её участников, каждый id со своим.
Описания¶
Каждая роль и каждое правило строк говорят, для чего они нужны. Проверка доступа читает, кто может видеть что и почему. Роль или правило, которые никто не может объяснить, никто не может и проверить, поэтому описание обязательно: роль, производная родительская роль или правило строк с пустым описанием отклоняются загрузкой конфигурации, API администрирования и UI с указанием причины. Списки «Роли» и «Правила строк» показывают каждое описание, а их панели подробностей показывают и редактируют его.
У роли одно описание, а не по одному на домен. Если общая роль не описывает свои обязанности в конкретном домене, этому домену нужна собственная роль, обычно потомок общей роли (parent_role_id), которая наследует её права и описывает, что она там делает.
Как запрос называет роли¶
Клиент отправляет одну роль или набор через запятую в X-Provisa-Role. Каждая названная роль должна быть у пользователя. Роль, не назначенная идентичности, отклоняется по имени.
Права и данные следуют только за ролями, от которых действует запрос: иметь роль — не значит действовать от неё. Одна роль — это эта роль; две или больше действуют как их метароль. Действующая роль, которая приносила бы с собой права каждой имеющейся роли, ничего бы не значила, поэтому запрос получает наименьшие привилегии, а запись аудита называет ровно то, что действовало.
Названные производные роли входят как квалификаторы. У квалификатора нет собственных прав, доступа к доменам, масок или фильтров строк, поэтому он не участник прав метароли: как участник без масок он снял бы маску с каждой колонки, а как не читающий ни одной таблицы — завершил бы любое OR фильтров строк. Он входит только в предоставления: предоставление или запись маски, называющая region__eu, достигает запроса, который называет этот квалификатор вместе с предоставленной ролью.
MCP-агент никогда не действует за пределами выданных ему ролей. Удалённый агент называет только роли, которыми владеет субъект его токена. Локальный stdio-агент называет только роли из набора, закреплённого в mcp.role (PROVISA_MCP_ROLE), и действует как этот набор, если не называет ни одной; любая другая роль отклоняется по имени.
Агент спрашивает, от какой роли он действует, с помощью MCP-инструмента describe_role. Ответ — эта роль и то, для чего она нужна (метароль — с каждым из её участников), а также роли, на которые агент может переключиться, каждая со своим описанием. Он никогда не содержит правила строк, маски, предоставления или условия. Provisa никогда не сообщает агенту, почему результат был сужен, и результат не несёт отметки «отфильтровано»: данные роли — это всё её представление, а причина того, чего не хватает, побуждала бы его выведывать.
Что означает +¶
+ встречается в двух местах, и в каждом значит своё.
В записи предоставления. 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перед выпуском комбинации в продакшен.
Модель разрешений колонок¶
У каждой колонки есть модель разрешений, управляющая чтением, записью и маскировкой для каждой роли.
Видимость в три уровня¶
| Уровень | Условие | Результат |
|---|---|---|
| Скрыта | Роли нет в 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. Отклонённое изменение называет колонку и право.
| Область | Обслуживается |
|---|---|
domain (по умолчанию) |
Ролям, которые достигают домена таблицы, как говорит visible_to. Пустой visible_to — это каждая такая роль. |
public |
Тем же ролям и также ролям, которые не достигают домена таблицы. |
restricted |
Только ролям, которые называет visible_to. Пустой visible_to — никто. |
Публикация колонки снимает условие домена для этой колонки и ничего больше:
- Роли вне домена таблица обслуживается только с её публичными колонками. Остальные колонки таблицы и таблицы, которые ничего не публикуют, остаются вне её схемы, каталога и SQL.
- Собственный
visible_toколонки по-прежнему решает, кому: публичная колонка, предоставленная одной роли, публична только для этой роли. - Маски, фильтры строк, лимиты строк и защита связей действуют так же, как внутри домена. Без фильтра строк для роли опубликованные колонки читаются без фильтра; фильтр строк на таблице или её домене по-прежнему действует.
- Связь обслуживается, когда обслуживаются колонки обоих её концов.
- Колонка-параметр таблицы API (аргумент пути или запроса) — это не данные, и она ничего не публикует.
- Домен
opsтребует явного предоставления для каждой колонки; ничто в нём не обслуживается между доменами, какой бы ни была область.
Правило одинаково на каждой поверхности, потому что каждая читает скомпилированную схему роли: GraphQL, SQL (HTTP, pgwire, Flight, JDBC), Cypher и Bolt, gRPC, REST и JSON:API, каталоги Flight и MCP и списки администратора. Область, не являющаяся одним из трёх значений, отклоняется при сохранении колонки.
Разрешения на запись¶
Чтение колонки определяется visible_to (пусто означает, что читать могут все роли). Запись определяется для таблицы через write_grants, а не для колонки. По умолчанию у таблицы нет разрешений, поэтому ни одна роль не может в неё писать.
Разрешение на запись указывает, что роль может записать в таблицу одной операцией (insert, update или delete):
| Поле | Назначение |
|---|---|
columns |
Колонки, которые INSERT может перечислить, а UPDATE — задать |
rows |
Строки, которых может касаться UPDATE или DELETE (SQL-предикат) |
check |
Условие, которому должна удовлетворять каждая строка, записанная INSERT или оставленная UPDATE (SQL-предикат) |
presets |
Колонки, которые разрешение задаёт само: из переменной сеанса, литерала или момента записи. Инструкция не может их передать. |
Если опустить rows или check, разрешение берёт фильтр строк роли на чтение для этой таблицы, так что роль пишет те строки, которые читает. "TRUE" означает любую строку.
Единое допущение решает судьбу каждой записи на любой поверхности в таком порядке: у роли есть право write; у неё есть разрешение на операцию над таблицей; каждая колонка, которую пишет инструкция, указана в разрешении; пресеты добавляются; rows присоединяется через AND к WHERE; а check вычисляется на новых строках до того, как что-либо отправлено источнику. Отказ называет таблицу, роль и операцию.
Более широким инструкциям нужны более широкие разрешения. 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) проходит две проверки. У роли должна быть глобальная возможность write, а команда должна быть назначена ей через visible_to; пустой список назначает её всем ролям. Списка writable_by у команды нет, и конфигурация отклоняет его, называя по имени.
Является ли операция записью, решает её контракт, а не то, что говорит вызывающий. SELECT, вызывающий функцию вида «мутация», считается записью и проходит те же две проверки, так что выдать мутацию за чтение не получится. Пометить мутацию как безопасную для чтения можно только с возможностью access_config, и это записывается как решение по управлению. Ни один запрос не может от этого отказаться.
Видимость схемы¶
У каждой роли своя схема GraphQL. В ней есть только таблицы из доменов её domain_access ("*" — все домены), а столбец, отсутствующий в visible_to для роли, отсутствует и в SDL. Чего роль не может прочитать, того она не видит.
Безопасность на уровне строк (RLS)¶
Фильтр строк — это предикат WHERE для одной таблицы и одной роли, добавляемый после компиляции и до выполнения.
rls_rules:
- table_id: orders
role_id: analyst
filter: "region = current_setting('provisa.user_region')"
description: Analysts read only their own region's orders.
Фильтр присоединяется через AND к условию WHERE запроса — как при чтении, так и при UPDATE и DELETE.
Фильтр читает запрос через current_setting('provisa.<name>'). Запрос связывает user_id с вошедшей в систему личностью, role — с действующей ролью, а также каждое скалярное утверждение (claim) токена — в нижнем регистре, с подчёркиваниями вместо дефисов. provisa.user_id — всегда вошедшая в систему личность, и никакое утверждение её не заменит.
Переменные сеанса Hasura сохраняют свои имена. X-Hasura-User-Id — это hasura_user_id, читаемое из утверждения x-hasura-user-id токена (на верхнем уровне или внутри объекта утверждений Hasura, например https://hasura.io/jwt/claims) либо из записанного для участника атрибута hasura_user_id. Поэтому импортированный фильтр Hasura по X-Hasura-User-Id читает provisa.hasura_user_id.
Под значениями запроса лежат настроенные session_vars роли, которые запрос переопределяет. Наследующая роль получает константы предков под своими собственными, и для каждого имени побеждает ближайшая роль. user_id, role, region и org берутся из запроса или узла и не могут быть настроены как константы. В незащищённом режиме заголовок x-provisa-session-<name> связывает <name>. Переменная, нигде не связанная, равна NULL, и фильтр по ней не совпадает ни с одной строкой.
Константа роли работает наоборот по сравнению с производной ролью. Производная роль превращает значение, которое есть у участника, в роль. Константа роли даёт значение каждому запросу, выполненному от имени роли, и правило строк читает его так же, как утверждение. Константа может быть ссылкой ${env:NAME} или ${secret:NAME}, которую разрешает тот же механизм, что и любую другую ссылку, когда запрос её использует. Модель хранит ссылку, а не значение, и ссылка, которая ни во что не разрешается, приводит к ошибке запроса с указанием константы. После разрешения значение становится частью запроса: собственные записи Provisa покрывает централизованное редактирование, но собственные журналы федеративного источника Provisa недоступны.
Маскировка на уровне колонок¶
Маскировка задаётся как упорядоченный список для каждой колонки. Первая запись, которую имеет читатель, решает, что он видит.
| Тип маски | Поддерживаемые типы колонок | SQL-выражение |
|---|---|---|
regex |
Строка (varchar, char, text) | REGEXP_REPLACE(col, pattern, replace) |
constant |
Любой | Литеральное значение (NULL, 0, своя строка) |
truncate |
Дата/метка времени | DATE_TRUNC(precision, col) |
У каждой записи есть список to, называющий читателей, на которых она направлена, и необязательная mask. Запись без mask означает «без маски» для этих читателей. * в to совпадает с любым читателем.
Выражение маски переносится в проекцию SQL SELECT. База данных возвращает маскированное значение; немаскированные данные никогда не передаются по сети для маскированных ролей. Маскированные колонки также блокируются в предложениях WHERE и HAVING (защита предикатов уровня 5), чтобы не допустить вывода значений через фильтрацию.
Маска действует на все имена столбца, но не на связанный столбец. Столбец, прочитанный по псевдониму или по физическому имени, маскируется одинаково. Маска не скрывает ключ, который утверждённая связь соединяет со столбцом, видимым роли: соединение сравнивает настоящие значения, поэтому связанный столбец показывает то, что содержит замаскированный ключ. Замаскируйте и связанный столбец или не утверждайте связь для этой роли.
Нет записи — нет маски. Читатель, у которого нет ни одной записи, читает колонку без маски. Завершайте список записью *, когда к каждому читателю должна применяться какая-либо маска. Редактор колонок предупреждает, если в списке нет записи * (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 ответов действий¶
То, что возвращает отслеживаемая функция или вебхук, управляется так же, как строки таблицы. Provisa связывает возвращённые строки в отношение, столбцы которого — объявленный вывод действия: output_columns для функции, inline_return_type для вебхука или свойства объекта в массиве return_schema функции. Затем это отношение проходит тот же этап управления, что и любое чтение таблицы, через VALUES CTE со строками, так что безопасность строк, маскирование и видимость для действий заново не реализуются.
Действуют три правила, в том же порядке, что и для таблицы:
- Фильтр строк. Правило RLS роли, сохранённое для действия по его имени (
upsertRlsRuleсactionName), или правило домена действия, если собственного у него нет. Предикат проверяется по контракту при сохранении, как правило для таблицы. - Видимость столбцов. Столбец контракта, объявляющий
visible_to, исключается для любой роли, которой нет в списке. Столбец без такого объявления входит в публичную форму действия и остаётся. - Маски. У столбца контракта есть
masks— тот же упорядоченный список, что и у столбца таблицы.
Наследование ролей разрешает цепочку для всех трёх правил. Если ответ возвращает столбец вне объявленного контракта, вызов завершается ошибкой 502, а не пропускает его без управления. Действие, не объявившее столбцов, возвращает скаляр, которому нечего связывать, и он возвращается как есть. Пробный вызов в админке сообщает, какие фильтры, исключения и маски сработали, а управляемый оператор попадает в журнал аудита запросов.
Выборка¶
Каждая роль получает выборку результатов, по умолчанию 100 строк, если у неё нет возможности full_results. Размер задаётся переменной окружения PROVISA_SAMPLE_SIZE.
Журнал аудита¶
Каждый запрос, затрагивающий актив домена, записывается в query_audit_log. Журнал принадлежит организации, а не платформе: собственный реестр платформы с организациями, пользователями и приглашениями записей аудита не содержит. Журнал каждой организации — это таблица в её схеме, в хранилище, которое организация указывает для своих записей в каждом выбранном регионе. Организация без регионов хранит его в базе данных плоскости управления развёртывания, рядом со своей моделью.
Строка содержит tenant_id (организацию), user_id, role_id, хеш SHA-256 текста запроса, сам текст запроса (в зашифрованном виде), table_ids и table_names, source, status_code, duration_ms, маршрут и число строк, что было применено к оператору, и logged_at. Текст шифруется ключом организации, если она его задала, иначе ключом развёртывания. Развёртывание без поставщика шифрования хранит его незашифрованным. В любом случае он не появляется в доступных для запросов служебных таблицах и расшифровывается только для разрешённых чтений администратором.
Журнал допускает только добавление, на уровне самой базы данных: правила PostgreSQL превращают DELETE и UPDATE над query_audit_log в пустые операции. Два индекса, (tenant_id, logged_at) и (user_id, logged_at), ускоряют проверки соответствия по организации и по пользователю за период.
Строки хранятся бессрочно. У Provisa нет настройки срока хранения, и журнал никогда не очищается, а поскольку DELETE ничего не делает, удалить старые строки может только оператор, удалив или заархивировав данные таблицы вне Provisa.
Редактирование¶
Всё, что Provisa записывает о своей работе, проходит через список правил редактирования, прежде чем куда-либо записаться. Текст, совпавший с правилом, заменяется на [REDACTED:<rule>] с именем сработавшего правила.
Список состоит из четырёх частей:
- Встроенные шаблоны. Номера карт, проходящие проверку Луна, номера социального страхования США, bearer-токены, токены доступа Provisa, ключи AWS, GCP, Azure, GitHub и Slack, а также закрытые ключи. Это начальный набор, а не гарантия.
- Правила развёртывания, действующие во всех организациях. Для их изменения нужно право
platform_settings. - Собственные правила организации. Для их изменения нужно право
org_settings. - Автоматическое правило для секретов организации: каждое значение, которое собственное хранилище Provisa держит для организации, каждый
${secret:NAME}, который называют модель и настройки организации (на любом бэкенде секретов), и каждое значение${secret:…}или${user:…}с момента его первого разрешения. Называть их в правилах не нужно.
Правило — это либо именованное регулярное выражение, либо именованная ссылка ${secret:NAME} / ${env:NAME}, которая совпадает со значением, в которое разрешается ссылка. Ссылка, которая не разрешается, записывается в журнал на уровне ERROR по имени и показывается как неразрешённая; остальные правила продолжают действовать. Значения короче четырёх символов не распознаются.
Provisa не обязан знать каждый конфиденциальный формат. Встроенные шаблоны покрывают распространённые формы; организация добавляет шаблоны для собственных форматов, например табельных номеров, идентификаторов счетов или внутренних имён хостов. Редактирование выполняется по мере возможности: значение, записанное в форме, которую не описывает ни одно правило, проходит без изменений.
Правила находятся на странице Секреты организации, в разделе Правила редактирования. На странице перечислены встроенные шаблоны, правила развёртывания (редактируемые с platform_settings), правила организации, секреты автоматического правила только по именам и каждая неразрешённая ссылка. Правила загружаются при запуске сервера и при построении среды выполнения организации и перезагружаются при изменении модели или настроек, а также при сохранении или удалении секрета организации.
Список применяется на каждом выходе, в который пишет Provisa:
- каждый span — как одна отредактированная копия, передаваемая каждому экспортёру, — и представление последних span;
- каждая запись лога в конвейере логов OpenTelemetry в Provisa и каждая строка, которую пишет обработчик logging, включая стандартный вывод и логи контейнеров;
- текст запроса в журнале аудита — до хеширования и шифрования, так что ни текст, ни его хеш не раскрывают распознанное значение; а также подробности административного журнала;
- сообщения об ошибках, которые клиент получает по HTTP, pgwire, gRPC, Arrow Flight и MCP.
Телеметрия и ошибки клиентов в момент записи не привязаны к одной организации, поэтому редактируются по правилам всех организаций вместе. Две ошибки проходят без редактирования: тело ошибки HTTP больше 1 МиБ и ошибка потока Arrow Flight, сбой которого произошёл после начала отправки.
Федеративный источник ведёт собственные журналы, которые Provisa не пишет и до которых не может дотянуться. Записи источника об инструкциях, которые ему отправляет Provisa, защищает этот источник.
Статистика через MCP¶
Статистические инструменты MCP (describe, correlate, regress, compare, forecast) выполняют SQL, заданный агентом, через управляемый конвейер от роли агента, поэтому правила строк, маскирование и редактирование действуют на то, что они анализируют. Статистические инструменты возвращают только результаты, поэтому строки, стоящие за анализом, никогда не попадают в контекст модели: минимизация данных по умолчанию.
То, что агент может узнать, ограничивает контроль доступа. Чтобы держать личности вне досягаемости агента, предоставьте его роли только те столбцы, которые никого не идентифицируют; роль, которая может прочитать столбец, может его агрегировать, а агрегаты по малым группам всё равно могут выявить отдельных людей.
Покрытие чувствительных столбцов¶
Представление sensitive_coverage в домене meta перечисляет каждый чувствительный столбец организации с объявленными им фиктивным значением и синтетическим правилом и статусом: covered, no_fake или inadequate, если не пройдена одна из проверок адекватности Provisa. Проверки ищут фиктивное значение, оставляющее реальные значения без изменений, фиктивное значение, вид которого не соответствует содержимому столбца, и фиктивное значение или правило, читающее реальное значение другого чувствительного столбца. Распорядитель данных видит столбцы без фиктивного значения в сведениях каждого окружения, а окружение Test (fake) отказывается отдавать данные, пока у каждого чувствительного столбца его нет. Проверки и представление описаны в разделе Создание тестовых окружений.
Суждения на проверке¶
Решения в области управления данными опираются на суждение, а суждение может оказаться ошибочным или устареть, когда меняются факты, на которых оно основывалось. Каждое управленческое суждение может нести обоснование: тег на объекте, фиктивное значение и синтетическое правило столбца, запись маски, правило строк и право на запись. Обоснование содержит причину, на которую опирались, цель, цитируемые именованные основания и дату пересмотра; всё это необязательно. У назначения тега причина — его собственный reason; у правила строк цель — его обязательное описание. Provisa записывает, кто принял решение, когда и на основании каких данных профиля: последнего запуска профиля таблицы и его сводки. Это записывается при каждом сохранении, которое меняет решение или его обоснование. Сохранение без обоснования оставляет сохранённое, а файл модели несёт каждое обоснование в блоке why:.
Именованные основания — это контекст, который данные показать не могут: регулирование, политика, стратегия. Суждение ссылается на них по id из короткого списка (/admin/judgments/bases). Обладатель access_config объявляет основание изменённым с примечанием, и каждое суждение, которое на него ссылается и было принято раньше, сразу считается устаревшим.
Проверка запускается каждый час для каждой организации, в конце каждого запуска профиля для профилируемой таблицы и сразу, когда основание объявлено изменённым. Всё найденное она помещает в одну очередь:
| Категория | Обнаруживается, когда |
|---|---|
| Ошибочно | не пройдена проверка адекватности фиктивного значения чувствительного столбца или синтетическое правило чувствительного столбца копирует реальные значения |
| Устарело по данным | профиль решённого столбца дрейфовал (по собственным порогам профайлера), изменились его правдоподобный тип, семейство или тип данных, в таблицу добавлен столбец, решённый столбец удалён или его тип изменён, или принявший решение больше не член организации |
| Устарело по контексту | цитируемое основание объявлено изменённым после решения, наступила дата пересмотра или прошёл expires_on назначения тега |
| Не решено | у чувствительного столбца нет фиктивного значения, или в таблицу с чувствительным тегом или фиктивным значением добавлен столбец |
| Стоит взглянуть | учётные данные источника хранятся как значение, а не как ссылка; источник тестового окружения читает подключение prod; аутентификация не настроена; администратор начальной настройки не закреплён; listener работает без TLS; роль или источник охватывает все домены; истёкшее назначение тега всё ещё действует |
Последняя категория наблюдается, а не навязывается: ничто в проверке ничего не отклоняет. GET /admin/judgments/findings перечисляет открытые находки, которые вызывающий может видеть; находка на чувствительном столбце требует sensitive_data, любая другая — view_governance. Подтвердить (POST /admin/judgments/findings/{id}/reaffirm) записывает суждение заново с сегодняшними данными и требует права, которое изменяет решение. Находка о конфигурации отклоняется с примечанием (.../dismiss, access_config). Находка, которую проверка больше не видит, закрывается. Как свидетельство для аудита, представление judgment_review в домене ops связывает каждую находку с обоснованием её суждения и запрашивается с учётом доступа по ролям и доменам.
Ограничение частоты¶
Лимиты для каждой роли задаются в provisa.yaml: запросы в секунду, одновременные подписки SSE и одновременные потоки Arrow Flight. Они проверяются в API, прежде чем что-либо будет скомпилировано или выполнено, и запрос сверх лимита получает HTTP 429 с заголовком Retry-After.
Запросы на естественном языке (POST /query/nl) имеют собственный лимит, nl.rate_limit, — запросов в минуту на роль. Запрос сверх него отклоняется до любого обращения к LLM.
Счётчики хранятся в Redis (cache.redis_url) как скользящие окна, без состояния в отдельных экземплярах, поэтому лимит действует, сколько бы экземпляров Provisa вы ни запустили.
Лимит сложности запроса¶
Лимит частоты ограничивает, сколько запросов отправляет роль. Лимит сложности ограничивает, что может запросить одна инструкция.
У каждой инструкции есть оценка сложности: отношения, которые она читает, её соединения, колонки, которые она выбирает (* считается как колонки, которые он обозначает), и вложенные в неё запросы. Отношение, прочитанное из удалённого API-источника (OpenAPI, удалённый GraphQL, удалённый gRPC), считается за 10, потому что каждое чтение расходует бюджет, который удалённая система задаёт для всех пользователей этого источника.
Лимит — это max_query_complexity в rate_limit роли. Общеорганизационная настройка limits.max_query_complexity — потолок для каждой роли: роль может задать более низкий лимит, а более высокий не действует. По умолчанию ни один не задан.
Оценка измеряется после разбора запроса и до его регулирования или выполнения, поэтому один лимит действует на каждом интерфейсе, через который может прийти инструкция: GraphQL, SQL по HTTP, pgwire, Arrow Flight, gRPC, Cypher, JSON:API и MCP. Инструкция сверх лимита отклоняется с её оценкой, лимитом и тем, что она запрашивала. Интерфейсы HTTP отвечают 413. Отказ записывается как отказ политики.
Аутентификация¶
Подключаемые провайдеры аутентификации:
| Провайдер | Учётные данные | Сценарий |
|---|---|---|
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) | Корпоративные каталоги |
saml |
Подписанное утверждение SAML 2.0, затем сессионный JWT | Корпоративный единый вход |
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 через настраиваемые правила. Поле assignments_source управляет тем, откуда берутся назначения ролей: claims читает их из claims токена JWT (по умолчанию), provisa — из внутреннего хранилища назначений Provisa. Мультиарендное развёртывание (multitenancy: true) само назначает роли, по организациям, поэтому должно использовать assignments_source: provisa: с claims оно не запускается, а изменение настроек, которое привело бы к этому сочетанию, отклоняется. Одноарендное развёртывание может использовать любой вариант.
Суперпользователь, настроенный в provisa.yaml (имя пользователя плюс пароль из секрета окружения), всегда получает роль admin и все возможности независимо от настроенного провайдера, путь начальной загрузки для первоначальной настройки.
Поверхности и учётные данные¶
Каждый интерфейс проверяет вызывающих через одного и того же провайдера, поэтому учётные данные, работающие в одном, работают во всех — везде, где протоколу есть куда их передать.
| Поверхность | Пароль | Токен провайдера | Персональный токен доступа | Клиентский сертификат (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.
Каждый интерфейс сообщает, какой именно отказ вы получили. Отсутствующие, отвергнутые или непригодные учётные данные дают отказ аутентификации: HTTP 401, gRPC UNAUTHENTICATED, Arrow Flight и Airport Unauthenticated. Действительные учётные данные, запрашивающие роль, которой у личности нет, дают отказ авторизации с указанием роли: HTTP 403, gRPC PERMISSION_DENIED, Arrow Flight и Airport Unauthorized. Одно исключение возникает в клиентской библиотеке Arrow, а не на сервере. Когда отклоняется вызов Flight get_schema, Python-клиент видит общую ошибку Arrow, где есть причина от сервера, но нет вида отказа; list_flights, get_flight_info и do_get вид передают.
Знак — в таблице означает, что в протоколе нет поля имени пользователя, к которому можно привязать пароль, и его покрывают формы с токеном. pgwire — обратный случай. В его стартовом пакете одно поле секрета и нет схемы, поэтому способ определяет сам секрет: персональный токен доступа узнаётся по префиксу, секрет читается как bearer-токен, если настроенный провайдер выдаёт токены, а всё остальное считается паролем. Выбор делается один раз, и учётные данные, отвергнутые выбранным валидатором, другим не проверяются.
Персональные токены доступа¶
Персональный токен доступа — это долгоживущий bearer-секрет, который вы создаёте для клиента, не способного пройти интерактивный вход: скрипта, BI-инструмента, драйвера. В нём есть собственные организация и роль, и каждый интерфейс проверяет его одним и тем же валидатором, так что ни одному не нужно знать, что это такое.
В сети он выглядит как provisa_pat_ и 43 символа base64, безопасных для URL. Именно префикс направляет предъявленный секрет в хранилище токенов, а не к провайдеру идентификации, и благодаря ему утёкший токен легко найти в журналах и репозиториях.
Хранится только SHA-256 секрета. Сам секрет вы видите один раз, при создании, и восстановить его потом нельзя; в списке ваших токенов показываются префикс для отображения и даты, но не рабочие учётные данные. Создавать, просматривать и отзывать свои токены можно через POST /auth/tokens, GET /auth/tokens и DELETE /auth/tokens/{token_hash} или в разделе токенов вашего профиля в админке.
Действительный токен разрешается в учётную запись владельца — с идентификатором пользователя, электронной почтой и отображаемым именем, — поэтому запись аудита или отчёт об использовании, сделанные под токеном, называют человека, а не учётные данные. Какой из его токенов сработал, записывается отдельно, в raw_claims["token_name"]. У токена может быть срок действия, после которого он отклоняется, а удаление членства пользователя отзывает и его токены.
SCRAM-SHA-256 в pgwire¶
С провайдером basic настройка auth.scram: true заставляет pgwire предлагать SASL (код аутентификации 10) с механизмом SCRAM-SHA-256, так что пароль доказывается, а не передаётся. Привязка канала (SCRAM-SHA-256-PLUS) не предлагается.
SCRAM нужен верификатор RFC 5802, который нельзя вывести из хеша bcrypt. Верификатор записывается всякий раз, когда пароль проходит в открытом виде (при регистрации, входе, смене пароля, сбросе администратором,) поэтому развёртывание, включившее SCRAM, собирает верификаторы по мере следующей аутентификации пользователей, и первое SCRAM-подключение каждого пользователя следует за его следующим вводом пароля. Пользователю, у которого ещё нет верификатора, отвечают имитационным обменом, неотличимым от настоящего, чтобы сеть не раскрывала, кто уже перешёл.
Взаимный TLS¶
Проверка клиентского сертификата переносит первую проверку в рукопожатие TLS: вызывающий без сертификата, подписанного CA развёртывания, никогда не достигает уровня учётных данных. Она доступна в 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. Поэтому счётчик находится на уровне проверки учётных данных, а не на какой-либо одной поверхности, так что блокировка, полученная где угодно, действует везде.
Оно включено по умолчанию (пять неудач за пять минут блокируют субъекта на пятнадцать минут) и настраивается в auth.login_throttle. Заблокированный субъект отклоняется ещё до проверки учётных данных, а успешная аутентификация очищает историю этого субъекта.
Ключ. Это принципал, которого несёт протокол. Поверхность только с bearer не несёт принципала, поэтому ключ — дайджест самих учётных данных; это не даёт повторять плохой токен без ограничений. Хранилище — в пределах процесса, поэтому развёртывание с несколькими воркерами API допускает до max_attempts на воркер — ограничение тормозит подбор, а не является распределённой квотой.
Зондирование ролей¶
Указание роли, которой у вызывающего нет, считается обнаруженным событием безопасности высокой важности на каждой поверхности: в HTTP API, MCP, pgwire, Bolt, Arrow Flight, службе Flight airport и gRPC. Легитимный клиент называет роли, которые ему выданы; клиент, называющий другую, либо неисправен, либо зондирует.
Запрос отклоняется. Вызывающему сообщается только «not permitted», без сведений о том, существует ли роль, а соединение или сеанс завершается там, где он есть: соединение pgwire, сеанс Bolt, вызов gRPC, сеанс MCP. Исключение — HTTP API с любыми учётными данными, кроме личного токена доступа: браузер предъявляет там свой сеанс или токен своего поставщика удостоверений, поэтому отказ по-прежнему называет роль.
Что происходит дальше, зависит от учётных данных и транспорта:
| Учётные данные | Транспорт | Приостанавливается |
|---|---|---|
| Токен посетителя демо-песочницы | HTTP API | ничего |
| Личный токен доступа | любой | токен |
| Токен, подписанный внешним поставщиком удостоверений | MCP, pgwire, Bolt, Flight, gRPC | доступ субъекта к организации по токену |
| Токен, подписанный внешним поставщиком удостоверений | HTTP API | ничего |
| Сеанс или пароль, которые проверяет само развёртывание | любой | ничего |
| Локальный сеанс MCP (без bearer) | MCP | ничего; событие записывается в журнал оператора |
HTTP API — исключение, потому что браузер предъявляет тот же bearer-токен, что и API-клиент, и сервер не может их различить, а вкладка, всё ещё называющая роль, которую пользователь уже потерял (после того как администратор изменил его роли или после смены организации), — обычное дело. MCP и остальные транспорты никогда не являются сеансом браузера. Там событие отклоняется, записывается и сообщается как любое другое; не выполняется только приостановка. Токен посетителя демо-песочницы — это личный токен доступа, но он единственные учётные данные вкладки браузера посетителя, поэтому в HTTP API с ним обращаются как с сеансом: отказ и запись в журнал, без приостановки.
Приостановленный токен отклоняется так же, как отозванный. Субъект, чей доступ по токену приостановлен, получает отказ в 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. То, что покидает организацию, называет субъекта, идентификатор учётных данных, транспорт, конечную точку, названную роль, время и важность, со ссылкой на административное представление; оно не содержит ни запросов, ни имеющихся ролей. Уведомление, которое не удалось доставить, записывается в журнал и никогда не отменяет отказ или приостановку.
Указание организации¶
В мультиарендном режиме каждый запрос называет организацию, для которой он предназначен. Принадлежность ровно к одной организации её не называет: запрос, который не называет никакой, отклоняется, и отказ говорит, что отправить. Каждая поверхность называет организацию так:
| Поверхность | Как клиент называет организацию |
|---|---|
| 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 читает организацию оттуда. Когда имя хоста и имя базы данных оба называют организацию и различаются, соединение отклоняется; ни одно не имеет приоритета. Имя базы данных provisa, используемое по умолчанию, не называет организацию, и BI-инструмент, подключённый к организации, показывает
эту организацию как свою базу данных. В Bolt SHOW DATABASES перечисляет по одному <org>.provisa_<role> для каждой организации, к которой
принадлежит пользователь; просто provisa_<role> в мультиарендном режиме отклоняется, а сеанс остаётся в
организации, которую назвал первой.
Имя — это просьба, а не предоставление. Везде оно попадает в один и тот же резолвер, который отклоняет организацию, участником которой аутентифицированный принципал не является и на которую не имеет межорганизационного права.
Персональный токен доступа выпускается для одной организации и сам её называет. Он открывает только эту организацию: запрос, называющий другую, отклоняется, даже если владелец токена принадлежит обеим.
У одноарендного развёртывания нет организации, которую нужно называть. Ни одна поверхность там не читает для этого имя хоста, имя базы данных или заголовок.
Секреты между организациями¶
Секреты организации читает только эта организация. В собственном хранилище Provisa каждый секрет хранится под своей организацией. В центральном сервисе (Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) развёртывания со многими организациями каждое чтение выполняется внутри префикса читающей организации, а ссылка, которая вышла бы за его пределы, отклоняется. Префиксы и шаги по переносу существующих секретов под них описаны в разделе Секреты.
Телеметрия разных организаций¶
В развёртывании, которое разделяют многие организации, операционные сигналы ("ops"."traces", а на движке Trino — метрики и журналы рядом с ним) — это одно хранилище, куда поступает работа каждой организации. Каждый span записывает организацию, для которой он был начат, сохранённую как tenant_id, и каждое чтение сигналов возвращает только строки читающей организации, какой бы ни была роль. Span, начатый вне работы какой-либо организации (например, при запуске сервера), не принадлежит ни одной и никем не читается. Администратор платформы не имеет доступа к данным ни в одной организации и не читает ни одну из них через Provisa.
Операционные сигналы хранятся бессрочно на обоих движках, как и журнал аудита: уплотнение переписывает файлы, в которых они хранятся, и не удаляет ни одной их строки. В Trino это таблицы Iceberg каталога otel. На нативном движке (DuckDB) сервер складывает собственные записи трассировки в виде файлов Parquet в своей папке телеметрии, а движок читает их под тем же именем, "ops"."traces", с представлением происхождения обновлений "ops"."mv_refresh_lineage" поверх них; оба читаются по тому же правилу для каждой организации.
Сигналы хранятся в своём хранилище в открытом виде. Нагрузка, которой нужно больше, чем граница по организациям внутри одного развёртывания, работает в выделенном развёртывании; особо чувствительная или регулируемая — в самостоятельно размещённом.
Фоновая работа разных организаций¶
Процесс, обслуживающий много организаций, выполняет фоновую работу каждой от имени этой организации. Обработка изменений каждой организации (цикл событий, который загружает изменения источников и обновляет представления поверх них), её построения реплик и срабатывающие от них триггеры таблиц принадлежат её собственному рантайму: изменение обрабатывает задание её собственной организации, оно читает свой источник с адресом и учётными данными этой организации и пишет в хранилище этой организации. Каждая среда организации держит собственный слот логической репликации PostgreSQL и публикацию, названные по организации, среде и источнику, поэтому две организации, одинаково назвавшие источник в одной базе, читают каждая свои изменения.
Резидентность данных¶
В развёртывании с регионами таблица может назвать один регион своим домом, и её персональные данные хранятся и обрабатываются там.
- Только домашний регион строит и обновляет реплику таблицы. Любой другой регион читает эту реплику на месте, с правилами строк и столбцов читателя, применяемыми в домашнем регионе, поэтому из него выходит только то, что читателю разрешено видеть.
- Домашний регион, чья реплика не построена или чьи хранилища недоступны, отказывает в чтении (
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-имени его пода. Движки размещённой платформы — одноузловые координаторы, без трафика между узлами, и они отклоняют эту опцию. - Буферизуемые данные. Отказоустойчивое выполнение шифрует буферизуемые данные обмена ключом, генерируемым для каждого запроса и отбрасываемым по его завершении (
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-перенаправлении. Источники демонстрационной песочницы дополнительно привязаны к адресу, по которому они были проверены. Собственная организация развёртывания, источники которой настраивает оператор, этим не ограничена.
Шифрование в покое и собственные ключи организации¶
Шифрование под собственным ключом организации доступно и выключено, пока организация его не включит. Ничего не шифруется под ключом организации, пока администратор организации не создаст его.
Ключ шифрования организации оборачивает секреты организации, её зашифрованные колонки и текст запросов в её журнале аудита. Администратор организации создаёт его в Admin → Org Secrets → Org Key или через PUT /admin/org-encryption: без тела Provisa генерирует ключ, который никогда не показывается, а тело key_b64 задаёт 32 байта вашего собственного. GET /admin/org-encryption сообщает только его отпечаток. Без ключа организации данные организации оборачиваются ключом развёртывания (encryption.provider в provisa.yaml). Развёртывание, где не настроен провайдер шифрования, хранит зашифрованные колонки и текст запросов аудита в незашифрованном виде; секреты в открытом виде не хранятся никогда (см. Секреты).
Папка организации содержит её загруженные и принятые файлы, файлы сброса её движка и её временные файлы. Администратор организации включает её шифрование в 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, мы рекомендуем самостоятельное размещение.
Python в браузере¶
Вкладки Python и Notebook в Explore запускают Python в браузере посетителя. Каждое чтение уходит на сервер от имени вошедшего пользователя, с его действующей ролью, организацией и средой, и управляется точно так же, как та же инструкция от любого другого клиента.
Режим высокой безопасности¶
security.mode: high в provisa.yaml утверждает одну гарантию: бэкенд Provisa никогда не обрабатывает данные в открытом виде. Каждая значимая колонка зашифрована в источнике, и прочитать её может только клиент, у которого есть ключ расшифровки. У этой гарантии есть последствия, которые развёртывание должно предусмотреть.
Что делает режим:
- 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 с сообщением, называющим режим повышенной безопасности, а порты pgwire, Bolt и MCP не слушают.
Хук утверждения ABAC¶
Необязательный внешний хук политики, срабатывающий перед выполнением запроса. Если он настроен, Provisa обращается к вашему движку политик с идентичностью пользователя, ролями, таблицами, колонками и операцией. Ответ определяет, продолжится ли запрос. Команда, зарегистрированная с Требует одобрения, передаётся тому же хуку перед каждым вызовом; см. Вызовы команд.
Область действия¶
Хук срабатывает только тогда, когда запрос затрагивает включённую таблицу или источник, поэтому любой другой запрос выполняется без дополнительных затрат.
| Конфигурация | Эффект |
|---|---|
auth.approval_hook.scope: all |
Каждый запрос вызывает хук |
sources[].approval_hook: true |
Все таблицы этого источника вызывают хук |
tables[].approval_hook: true |
Эта таблица вызывает хук |
Протоколы¶
Поддерживаются три транспорта:
| Тип | Сценарий | Поле конфигурации |
|---|---|---|
webhook |
Любой сервис политик с HTTP (OPA, собственный) | url |
unix_socket |
OPA или сайдкар политик на той же машине | socket_path + url |
grpc |
Высокопроизводительный сервис политик рядом | url (host:port) |
Транспорт gRPC использует контракт provisa.auth.ApprovalService, определённый в provisa/auth/approval.proto. Реализуйте этот сервис в своём движке политик:
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;
}
Канал gRPC постоянный: один канал на экземпляр Provisa, повторно используемый для всех вызовов этого endpoint хука.
Запрос / ответ¶
Все три транспорта несут одинаковую полезную нагрузку:
| Поле | Тип | Описание |
|---|---|---|
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).
Вызовы команд¶
Команда с установленным requires_approval (переключатель Требует одобрения в форме команды) перед каждым вызовом идёт к хуку, на любой поверхности. Она выполняется, только если хук одобряет. Запрос несёт operation: "command", имя команды и её аргументы; tables и columns пусты.
{
"user": "analyst",
"roles": ["analyst"],
"tables": [],
"columns": [],
"operation": "command",
"command": "create_issue",
"arguments": {"input": {"title": "Crash on save"}}
}
Два отказа, оба 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. Автоматический выключатель (по умолчанию: размыкается после 5 последовательных ошибок, полуоткрыт через 30 с) предотвращает каскадные сбои из-за медленного endpoint хука.
Пример конфигурации¶
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}, разрешаемый во время выполнения. Пароли никогда не хранятся в базе данных конфигурации.
Полное описание сервиса секретов (хранилища, синтаксис ссылок и провайдеры) см. в разделе Секреты.