← Knowledge Store

ACL и личности

knowledge-store · workzone

Knowledge Store — дом прав и личностей, которые Harvester захватывает из источников и пишет сюда, а → Query Engine применяет на выдаче. Страница держит модель: как из вошедшего пользователя платформа доходит до разрешённых ему сущностей — через личности и права источника, — и где проходит граница v1/v2. Права сохраняются в оригинальных терминах источника, без интерпретации в платформенные уровни.

Резолв-джойн: от пользователя к разрешённым сущностям

Права применяются на запросе как pre-filter: кандидаты сужаются по доступу до ранжирования. Чтобы ответить «что доступно этому пользователю», платформа проходит цепочку — от платформенного аккаунта к его личностям, их членствам в группах источника и к гранту на сущности. Само применение — → Query Engine; здесь — звенья и их хранение.

users identity.user_id identity
identity source_principal.identity_id source_principal
source_principal group_membership source_group
source_group entity_acl entity

Прямой грант на сущность конкретному человеку короткозамыкает цепочку через entity_acl.source_principal_id, минуя группу. Грант scope = 'public' видят все — он добавляется к фильтру напрямую, в обход цепочки членства.

Harvester
пишет
Захватывает права и людей из источника, делает upsert в эти таблицы теми же режимами синхронизации, что и контент.
Knowledge Store
хранит
Реляционный source of truth прав и личностей; строит резолв-джойн и исполняет pre-filter рядом с векторами — одним SQL с поиском по смыслу.
Query Engine
применяет
Передаёт личность вызывающего на запросе — резолв-джойн и фильтр исполняет KS, Query Engine их не дублирует.
Личности: человек источника → личность → аккаунт

Тот же API коннектора, что отдаёт контент, отдаёт и людей источника. Каждый человек источника — source_principal; одинаковые по email сводятся в одну identity. Если личность ещё и входит в платформу — identity мостом ссылается на Auth · users.

1 Личность Каноническая личность — цель свода.
identity сведённые личности
UNIQUE (lower(email)) UNIQUE partial (user_id) FK → users
id BigInteger PK
email Text NOT NULLUNIQUEIDX ключ свода · нормализованный lower(email)
display_name Text NULL отображаемое имя
user_id BigInteger FK→usersNULLUNIQUE мост к платформенному аккаунту · 1:1 закреплён UNIQUE partial · NULL = личность без входа в платформу
created_at DateTime(tz) DEFAULT now()
updated_at DateTime(tz) DEFAULT now() + trigger
Мост к Auth, не дубль
«Личность» из источников и «учётка платформы» — разные сущности. identity.user_id — единственная связь между ними: заполнен, когда личность ещё и входит в платформу. Каналы входа — в Auth · identity_mapping (Slack-бот source='slack', SSO Okta / Azure); здесь — Jira / Slack / Confluence как контент-источники. Тот же Slack носит две роли — вход (Auth) и контент (здесь), не путать.
Заполнение моста — авто-связь по email → Auth & Security → Harvester
identity.user_id ставится автоматически по точному lower(email) — связь строго 1:1: закреплена UNIQUE partial на user_id, а обе таблицы к тому же уникальны по email, так что коллизий нет (в отличие от свода source_principal → identity). Срабатывает синхронно в двух точках: когда в платформе заводится аккаунт (инвайт принят, setup, создание / смена email админом) — Auth просит связать существующую identity того же email; и когда Harvester делает upsert identity — связывает с уже существующим users. Так вошедший сразу видит все свои аккаунты в источниках, без ожидания синхронизации. Операцию связывания держит Knowledge Store — владелец identity; Auth и Harvester её вызывают. Операция идемпотентна и транзакционна: повторный вызов с тем же email и аккаунтом — no-op (держат UNIQUE-ключи и upsert по lower(email)), а связывание идёт одной транзакцией в общей базе — без промежутка, где мост поставлен наполовину. Исход явный: либо связано, либо совпадения нет (личности этого email ещё нет, или адрес входа ≠ адрес источника) — несвязанное уходит на ручной разбор, не теряется молча.
email входа ≠ email источника
Если в источнике человек под другим адресом (alice.smith@corp против alice@corp входа), авто-связь по точному email не сработает — identity остаётся без моста (user_id = NULL). Такую личность администратор привязывает к нужному пользователю вручную на Admin · Identity Mapping — тем же путём, что и несведённые source_principal. Авто-сведение по разным адресам (deep entity resolution) — v2, шаг Curation Pass; см. границу итерации.
2 Человек источника Учётка как её отдаёт источник.
source_principal люди из источников
FK → sources FK → identity UNIQUE (source_id, source_user_id)
id BigInteger PK
source_id BigInteger FK→sourcesIDX источник · CASCADE
source_user_id Text NOT NULL нативный id у источника
email Text NULL для свода · у части источников отсутствует
display_name Text NULL имя как в источнике
identity_id BigInteger FK→identityNULLIDX NULL = unmatched, ждёт разбора в Admin
created_at DateTime(tz) DEFAULT now()
updated_at DateTime(tz) DEFAULT now() + trigger
Свод по email · несовпавшие — на разбор → Admin · Identity Mapping
Люди источников с одинаковым точным email сводятся в одну identity. Кого свести не удалось (нет email, неоднозначность) — identity_id остаётся NULL: запись уходит на ручной разбор в Admin, где её привязывают к существующей личности или заводят новую. Нечёткое «разные email, тот же человек» — это deep entity resolution: v2 (шаг Curation Pass).
Права источника: группы, членство, грант на сущность

Права берутся в оригинальных терминах источника (проект Jira, пространство Confluence, канал Slack) — без интерпретации в платформенные уровни. Доступ держат две грани: тег — в какой группе лежит сущность (entity_acl), и членство — кто в группу входит (group_membership); без обеих нельзя ответить, кому сущность видна.

3 Группа источника Контейнер в оригинальных терминах.
source_group группы и контейнеры источника
FK → sources UNIQUE (source_id, source_group_id)
id BigInteger PK
source_id BigInteger FK→sourcesIDX источник · CASCADE
source_group_id Text NOT NULL нативный id контейнера
name Text NOT NULL отображаемое имя · «проект Backend»
kind Text NULL тип контейнера · project · space · channel · набор от манифеста
created_at DateTime(tz) DEFAULT now()
updated_at DateTime(tz) DEFAULT now() + trigger
4 Членство Кто входит в группу.
group_membership принадлежность к группам
FK → source_group FK → source_principal PK (group, principal)
source_group_id BigInteger PKFK→group ведущий столбец составного PK (индекс PK покрывает лукап по группе) · CASCADE
source_principal_id BigInteger PKFK→principalIDX второй столбец PK · отдельный IDX под резолв principal→groups · CASCADE
created_at DateTime(tz) DEFAULT когда замечено членство
Снимок текущего членства
Одна строка на пару «группа × человек». Reconciliation сверяет членство с источником и приводит снимок в соответствие: добавили в проект — строка появляется, убрали — удаляется. Это снимок актуального, не лента изменений. Членство плоское — прямые пары; разворот вложенных групп (группа в группе, транзитивное членство) — v2, см. границу итерации.
5 Грант на сущность Тег: кому видна сущность.
entity_acl права на сущности
FK → entities грант: группе · принципалу · public UNIQUE partial (source_group_id, entity_id) UNIQUE partial (source_principal_id, entity_id) UNIQUE partial (entity_id) WHERE public
id BigInteger PK
entity_id BigInteger FK→entitiesIDX сущность KS · CASCADE
scope Text NOT NULLCHECK кому грант · group · principal · public
source_group_id BigInteger FK→groupNULLIDX грант группе · заполнен при scope=group
source_principal_id BigInteger FK→principalNULLIDX грант прямому человеку · заполнен при scope=principal
created_at DateTime(tz) DEFAULT когда замечен грант · без updated_at — снимок без полезной нагрузки
Грант — группе, прямому человеку или всем (public)
scope различает три формы гранта, CHECK держит их согласованными: group — заполнен source_group_id (доступ контейнеру/группе), principalsource_principal_id (адресно человеку), public — оба NULL (контент, открытый всем в источнике: public-канал Slack, anonymous-доступ Confluence). Public — wildcard-грант на уровне строки, без раздувания членства синтетической группой «everyone»; на резолве он добавляется к фильтру как scope = 'public'. Реляционная таблица — source of truth прав; как этот грант единым JOIN покрывает и поиск, и обход графа поверх трёх проекций одной базы, держит модель данных. Узел сущности (entities) проектируется со всем остальным Knowledge Store — здесь зафиксирована только грань прав.
Индексы под направление резолва · они же ключ дедупа
Горячий путь — «набор групп пользователя → доступные сущности», поэтому грант несёт уникальные частичные индексы (source_group_id, entity_id) WHERE source_group_id IS NOT NULL и (source_principal_id, entity_id) WHERE source_principal_id IS NOT NULL (а для public — (entity_id) WHERE scope = 'public'), а не только одиночные FK: лукап идёт index-only, без захода в кучу за entity_id. Та же пара структур несёт двойную службу — порядок (получатель, entity_id) совпадает с направлением резолва, а уникальность даёт conflict target для upsert-реконсиляции (естественный ключ гранта при полиморфном получателе и суррогатном id): повторный захват — ON CONFLICT DO NOTHING, дубли исключены. Частичность (WHERE …) — потому что у каждой формы гранта (scope) заполнен свой набор столбцов. Когда множество доступных групп уже известно (его резолвит на запросе → Query Engine), резолв-джойн от users схлопывается в фильтр по этому набору. Денормализация прав в массив на сущности — отдельный тяжёлый рычаг, отложен (open-questions).
Граница v1 / v2

В v1 — сырой захват прав и личностей, детерминированный свод по точному email с ручным разбором несошедшегося, применение pre-filter и доводка графа фоновым прогоном (Curation Pass). В v2 остаётся платформенная интерпретация прав поверх source-уровня, нечёткое (machine-guess) сведение личностей и жёсткие гарантии на выдаче.

v1 — сырой захват + pre-filter

Пять таблиц выше · свод личностей по точному email · мост identity ↔ users авто-связью по точному email (синхронно, обе стороны) + ручная привязка несошедшегося в Admin (нет email, неоднозначность, адрес входа ≠ адрес источника) · грант группе / принципалу / public · членство и тег в оригинальных терминах источника · pre-filter на выдаче (резолв-джойн). Права eventually consistent: отзыв вступает в силу к ближайшей синхронизации.

v2 — интерпретация и догадка v2

Платформенный ACL поверх source-прав (двойной фильтр, container-override, platform_acl_rules, экран → Admin · Platform ACL) · live-check прав на выдаче (late-binding security trim) · нечёткое cross-source сведение личностей и авто-связь моста по разным email (deep entity resolution — шаг Curation Pass) · разворот вложенных групп (group-in-group): транзитивное членство — в v1 членство плоское, одна строка «группа × человек».