Knowledge Store — дом прав и личностей, которые Harvester захватывает из источников и пишет сюда, а → Query Engine применяет на выдаче. Страница держит модель: как из вошедшего пользователя платформа доходит до разрешённых ему сущностей — через личности и права источника, — и где проходит граница v1/v2. Права сохраняются в оригинальных терминах источника, без интерпретации в платформенные уровни.
Права применяются на запросе как pre-filter: кандидаты сужаются по доступу до ранжирования. Чтобы ответить «что доступно этому пользователю», платформа проходит цепочку — от платформенного аккаунта к его личностям, их членствам в группах источника и к гранту на сущности. Само применение — → Query Engine; здесь — звенья и их хранение.
Прямой грант на сущность конкретному человеку короткозамыкает цепочку
через entity_acl.source_principal_id, минуя группу. Грант
scope = 'public' видят все — он добавляется к фильтру
напрямую, в обход цепочки членства.
Тот же API коннектора, что отдаёт контент, отдаёт и людей источника.
Каждый человек источника — source_principal; одинаковые по
email сводятся в одну identity. Если личность
ещё и входит в платформу — identity мостом ссылается на
Auth · users.
| id | BigInteger | PK | — |
| 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 |
identity.user_id — единственная связь между ними:
заполнен, когда личность ещё и входит в платформу. Каналы
входа — в
Auth · identity_mapping
(Slack-бот source='slack', SSO Okta / Azure);
здесь — Jira / Slack / Confluence как
контент-источники. Тот же Slack носит две роли — вход (Auth)
и контент (здесь), не путать.
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 ещё нет, или адрес входа ≠ адрес
источника) — несвязанное уходит на ручной разбор, не теряется
молча.
alice.smith@corp против alice@corp
входа), авто-связь по точному email не сработает —
identity остаётся без моста
(user_id = NULL). Такую личность администратор
привязывает к нужному пользователю вручную на
Admin · Identity Mapping
— тем же путём, что и несведённые source_principal.
Авто-сведение по разным адресам (deep entity resolution)
— v2, шаг Curation Pass; см.
границу итерации.
| id | BigInteger | PK | — |
| source_id | BigInteger | FK→sourcesIDX | источник · CASCADE |
| source_user_id | Text | NOT NULL | нативный id у источника |
| 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 сводятся в
одну identity. Кого свести не удалось (нет email,
неоднозначность) — identity_id остаётся
NULL: запись уходит на ручной разбор в Admin, где её
привязывают к существующей личности или заводят новую. Нечёткое
«разные email, тот же человек» — это deep entity resolution:
v2 (шаг Curation Pass).
Права берутся в оригинальных терминах источника (проект Jira,
пространство Confluence, канал Slack) — без интерпретации в
платформенные уровни. Доступ держат две грани: тег —
в какой группе лежит сущность (entity_acl), и
членство — кто в группу входит
(group_membership); без обеих нельзя ответить, кому
сущность видна.
| 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 |
| 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 | когда замечено членство |
| 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 — снимок без полезной нагрузки |
scope различает три формы гранта, CHECK держит их
согласованными: group — заполнен
source_group_id (доступ контейнеру/группе),
principal — source_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 — сырой захват прав и личностей, детерминированный свод по точному email с ручным разбором несошедшегося, применение pre-filter и доводка графа фоновым прогоном (Curation Pass). В v2 остаётся платформенная интерпретация прав поверх source-уровня, нечёткое (machine-guess) сведение личностей и жёсткие гарантии на выдаче.
Пять таблиц выше · свод личностей по точному email ·
мост identity ↔ users авто-связью по точному email
(синхронно, обе стороны) + ручная привязка несошедшегося в Admin
(нет email, неоднозначность, адрес входа ≠ адрес источника) ·
грант группе / принципалу / public · членство и тег в оригинальных
терминах источника · pre-filter на выдаче (резолв-джойн). Права
eventually consistent: отзыв вступает в силу к ближайшей синхронизации.
Платформенный 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 членство плоское, одна строка «группа × человек».