Documentation
¶
Overview ¶
Package catalog — КАТАЛОЖНЫЙ ФАКТ, прочитанный из СТРОК: какие пары «модуль.ресурс» грантуемы и какие глаголы объявлены каждой.
───────────────────────────────────────────────────────────────────────────── ЧЕМ ЭТОТ ПАКЕТ ОТЛИЧАЕТСЯ ОТ ЛИТЕРАЛА `authzmap`
Литерал `authzmap.objectTypes` / `typeVerbRelations` остаётся и снятию не подлежит: он производная канона `fga_model.fga`, проверяемая гейтом дрейфа, и без него строки каталога остались бы без якоря к канону вовсе. Литерал отвечает на вопрос «что объявлено КАНОНОМ»; этот пакет — на вопрос «что живо В БАЗЕ ПРЯМО СЕЙЧАС».
Пока строки пишет только миграция, ответы совпадают, и совпадают не случайно: страж старта (`seed.AssertCatalogParity`) сверяет литерал с живыми строками на КАЖДОМ старте и при расхождении отказывает в пуске. Различие появляется в один момент — когда строка снята в РАБОТАЮЩЕМ процессе: страж своё уже отработал, а читатель на литерале продолжит считать снятый тип живым до следующего перезапуска. Наблюдаемо это не поломкой, а «прав не выдали» либо, что хуже, выдачей по типу, который платформа сняла.
───────────────────────────────────────────────────────────────────────────── ИМЯ ТИПА МОДЕЛИ ПРИЕЗЖАЕТ СТРОКОЙ — И ЭТО НЕ ВТОРОЙ ПЕРЕХОДНИК
Здесь стояло «имя типа МОДЕЛИ строки каталога не несут — такой колонки в `catalog_resource` нет». Колонка заведена (`object_type`, миграция `20260903112400`), и утверждение снято вместе со своим предметом.
Причина, по которой оно держалось, названа честно и была верна: два места, знающих соответствие, разойдутся тихо. Разошлись бы — если бы соответствие здесь ВЫЧИСЛЯЛОСЬ. Оно не вычисляется: словарь по-прежнему объявляет манифест модуля одной строкой вместе с ресурсом, а колонка есть место, куда он доезжает. Переходник остался один, у него сменилось место хранения.
Согласие колонки с таблицей, порождённой сборкой, держит страж старта (`seed.AssertCatalogParity`) — на КАЖДОМ старте, с отказом в пуске при расхождении.
`cluster` строки по-прежнему не имеет вовсе: вершина иерархии ресурсом не является, и спрашивать её набор у каталога нечем ни при какой колонке.
Index ¶
- Constants
- type Facts
- func (f *Facts) AllVerbVocabulary() []string
- func (f *Facts) CommonVerbVocabulary() []string
- func (f *Facts) FGAObjectType(dotted string) (string, bool)
- func (f *Facts) GrantedVerbs(fgaType string, authored, typeVerbs []string) []string
- func (f *Facts) IsKnownModule(module string) bool
- func (f *Facts) Modules() []string
- func (f *Facts) Resources() []ResourceEntry
- func (f *Facts) RolePreviewLookup() domain.TypeVerbLookup
- func (f *Facts) RoleVerbsFromSelectors(selectors []domain.RuleSelector) []domain.RoleVerb
- func (f *Facts) VerbsOfType(fgaType string) []string
- type Fixed
- type RefreshObserver
- type ResourceEntry
- type ResourceRow
- type RowSource
- type Rows
- type Snapshot
- type Source
- type VerbRow
Constants ¶
const ( // RefreshOutcomeRefreshed — новое множество прочитано и подменило прежнее. RefreshOutcomeRefreshed = "refreshed" // RefreshOutcomeFailed — обновление не удалось; снимок остался прежним. RefreshOutcomeFailed = "failed" )
Исходы обновления снимка. Набор ЗАКРЫТ и приходит отсюда, никогда из запроса, поэтому кардинальность метрики не растёт с трафиком.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Facts ¶
type Facts struct {
// contains filtered or unexported fields
}
Facts — НЕИЗМЕНЯЕМЫЙ каталожный факт на один момент времени.
Значение собирается один раз и после этого не правится: обновление снимка строит НОВОЕ значение и подменяет указатель. Поэтому вызывающий, взявший факт, держит согласованное множество до конца своего вычисления — половины обновления он не увидит ни при какой конкуренции.
func NewFacts ¶
NewFacts собирает факт из живых строк каталога.
ПУСТОЕ МНОЖЕСТВО ОТВЕРГАЕТСЯ, и это не перестраховка. Пустой снимок отверг бы ВСЕ правила арендатора разом, и снаружи это читалось бы как «продукт сломан», а не как «миграции не применены». На старте до этого не доходит — страж отказывает в пуске раньше, — но обновление снимка идёт БЕЗ стража, и пустой ответ там обязан быть отказом обновления, а не новым снимком.
func (*Facts) AllVerbVocabulary ¶
AllVerbVocabulary — глаголы, которые объявляет ХОТЬ ОДИН живой глагольный тип.
func (*Facts) CommonVerbVocabulary ¶
CommonVerbVocabulary — глаголы, общие ДЛЯ ВСЕХ живых глагольных типов.
func (*Facts) GrantedVerbs ¶
func (*Facts) IsKnownModule ¶
IsKnownModule — членство модуля в ЖИВОМ каталоге. Реализует `domain.ModuleSet`: домен набора не знает и получает его отсюда.
Почему ответ берётся у снимка, а не у запроса к базе ¶
Каталог мал и меняется реже всего в схеме, а спрашивают его на горячем пути создания и правки роли. Запрос на каждом обращении оплачивался бы запросом арендатора. Отставание при этом ОГРАНИЧЕНО и НАЗВАНО — оно равно периоду обновления снимка, задаваемому профилем развёртывания (см. Snapshot), а не сроку жизни процесса: снятие модуля доезжает до пути запроса за один период, без перезапуска.
Подстановочный знак `*` модулем НЕ является: строки с таким именем в каталоге нет и быть не может (`catalog_module_nonempty` плюс грамматика имени), а разрешает его политика правила, а не набор.
func (*Facts) Modules ¶
Modules — ЖИВЫЕ модули каталога, отсортированно. Возвращается КОПИЯ: снимок вызывающему не принадлежит.
func (*Facts) Resources ¶
func (f *Facts) Resources() []ResourceEntry
FGAObjectType — имя типа МОДЕЛИ ПРАВ для точечного имени каталога («vpc.network» → «vpc_network»), по ЖИВЫМ строкам; ok=false у ресурса, чья строка снята либо которого в каталоге нет вовсе.
Это тот же вопрос, что задавал `authzmap.FGAObjectType`, и та же закрытость: незнакомая пара обязана дать ok=false, а НЕ произвольный тип модели. Отличие одно и оно несущее — ИСТОЧНИК. Порождённая сборкой таблица закрывала направление СНЯТИЯ и не закрывала ЗАВЕДЕНИЕ: тип, которого сборка не знала, получал «не найдено», и вызывающий пропускал его молча при роли, созданной без отказа (#1816, IAM-CT-2-14).
Вторым переходником это не является: соответствие не вычисляется, а читается из строки, куда его положил манифест модуля. Resources — ЖИВЫЕ пары каталога в порядке точечного ключа.
Отдаётся КОПИЯ по той же причине, что и у `Modules`: перечень принадлежит неизменяемому факту, и сортировка на месте у вызывающего испортила бы снимок для всех остальных.
func (*Facts) RolePreviewLookup ¶
func (f *Facts) RolePreviewLookup() domain.TypeVerbLookup
GrantedVerbs — глаголы, которые правило с авторскими глаголами `authored` даёт НА ТИПЕ `fgaType`.
Предикат ОДИН на обе стороны и живёт у владельца правила (`authzmap.GrantedVerbsWithDeclared`); отсюда приходит единственный факт, который зависит от каталога, — объявляет ли ЖИВОЙ тип набор глаголов вообще. Повторить вычисление здесь значило бы завести второе место об одном предмете: ровно так роль-администратор однажды давала движку всё, а проекции — ничего. RolePreviewLookup — резолв набора глаголов ПАРЫ каталога для превью роли (#1994).
Пара переводится в имя типа МОДЕЛИ по ЖИВОЙ строке, и набор берётся у неё же. Прежде перевод делала таблица, ПОРОЖДЁННАЯ СБОРКОЙ: тип, заведённый применением манифеста в работающем процессе, она не резолвила, и вызывающий брал запасной набор — глаголы ВСЕЙ платформы.
Запасной набор ОСТАЁТСЯ, и он тут же ¶
Правило, не адресующее ни одного типа (форма `*.*` роли-суперпользователя), своего набора не имеет by construction: перечислить ресурсы подстановки домену нечем. Пустое превью читалось бы как «роль ничего не даёт», поэтому такая пара получает ОБЪЕДИНЕНИЕ наборов живых типов.
Объединение, а НЕ пересечение: пересечение сужается, когда какой-нибудь тип снимает у себя глагол, — и роль `*.*` начинала бы обещать меньше, чем даёт, от правки, к ней не относящейся (наблюдалось при #1189).
Оба ответа приходят из ОДНОГО факта: взяв набор типа отсюда, а запасной у другого источника, вызывающий получил бы превью, собранное из двух снимков.
func (*Facts) RoleVerbsFromSelectors ¶
func (f *Facts) RoleVerbsFromSelectors(selectors []domain.RuleSelector) []domain.RoleVerb
RoleVerbsFromSelectors — проекция «тип × глагол» из тех же селекторов, которыми роль материализуется, по ЖИВОМУ каталогу.
Тип в проекции остаётся ТОЧЕЧНЫМ — тем же, каким он назван в селекторах и каким его читает вердикт (`role_verb.object_type`); набор глаголов спрашивается по имени МОДЕЛИ, поэтому перевод делается здесь ровно один раз.
НАПРАВЛЕНИЙ ДВА, и оба обязаны быть верны по живым строкам:
- СНЯТИЕ: тип, чья строка снята, пар не даёт. Пара по снятому типу дошла бы до внешнего ключа `role_verb_type_fk` и была бы им отвергнута, то есть отказ пришёл бы ЧУЖОЙ полосой (IAM-CT-2-06);
- ЗАВЕДЕНИЕ: тип, заведённый применением манифеста в РАБОТАЮЩЕМ процессе, пары даёт. Пока переходник спрашивался у таблицы, порождённой сборкой, этого не было: незнакомый ей тип пропускался молча, и арендатор не получал ничего при роли, созданной без отказа (IAM-CT-2-14).
Порознь каждое направление выполнимо портом, который не производит пар НИКОГДА, — поэтому утверждаются оба.
func (*Facts) VerbsOfType ¶
VerbsOfType — ГЛАГОЛЫ, объявленные ЖИВЫМ типом, отсортированно; nil у типа, чья строка снята либо которого в каталоге нет вовсе (`cluster`).
Возвращается КОПИЯ: значение снимка вызывающему не принадлежит, а испортить его он мог бы одной сортировкой на месте.
type Fixed ¶
type Fixed struct{ F *Facts }
Fixed — источник, отдающий один и тот же факт. Нужен там, где обновления не бывает по построению: разовому пересчёту на старте и пробам.
type RefreshObserver ¶
type RefreshObserver interface {
IncCatalogSnapshotRefresh(outcome string)
}
RefreshObserver — порт наблюдения за обновлением снимка.
Счётчика ДВА, и одного не хватает ни на один из двух вопросов. Ноль отказов даёт и исправный процесс, и процесс, ни разу не пошедший обновляться, — то есть путь, умерший целиком, выглядел бы здоровее всех. Растущий ноль удавшихся при этом не говорит, отказывало ли обновление или его не запускали. Знаменатель считается наравне с числителем.
type ResourceEntry ¶
type ResourceEntry struct {
Module string
Resource string
// ObjectType — имя типа в словаре МОДЕЛИ ПРАВ (`vpc_network`, `account`).
ObjectType string
}
ResourceEntry — одна ЖИВАЯ пара каталога вместе с именем её типа в словаре МОДЕЛИ ПРАВ.
Три величины отдаются ВМЕСТЕ намеренно. Имя типа не выводится из пары (правила `<модуль>_<ресурс>`, верного на всех строках, не существует — см. `ResourceRow.ObjectType`), поэтому читатель, получивший пару без имени, пошёл бы за ним к словарю, ПОРОЖДЁННОМУ СБОРКОЙ, — то есть ровно туда, откуда его уводит этот перечень.
type ResourceRow ¶
type ResourceRow struct {
Module string
Resource string
// ObjectType — имя типа МОДЕЛИ ПРАВ (`vpc_network`, `account`), которым
// адресуется отношение `v_<глагол>`.
//
// Оно ПРИЕЗЖАЕТ строкой, а не выводится из пары, и это несущее решение, а не
// удобство: правила вывода `<модуль>_<ресурс>` в дереве нет — у `storage` и
// `registry` имя ресурса множественное, а тип единственного числа
// (`storage.volumes` → `storage_volume`), у ярусных предков иерархии тип
// идёт БЕЗ приставки модуля (`iam.account` → `account`). Выражения, верного
// на всех строках, не существует.
//
// Без него читатель спрашивал бы имя у словаря, ПОРОЖДЁННОГО СБОРКОЙ, и тип,
// заведённый применением манифеста в работающем процессе, получал бы «не
// найдено» — то есть пропускался бы молча (#1816, IAM-CT-2-14). Это второе
// направление того же предмета, что и снятие: снятие строкам видно, потому
// что строка исчезает; заведение видно только если строка несёт ВСЁ, что о
// ресурсе нужно знать.
ObjectType string
}
ResourceRow — одна ЖИВАЯ строка `kaname.catalog_resource`.
Точечная форма (`dotted`) в структуре не хранится: она производна от пары и собирается там, где нужна. Хранить её третьим полем значило бы завести место, где она может разойтись с парой, — а согласие этих двух в базе держит проверка колонки, а не читатель.
type RowSource ¶
RowSource — ПОРТ чтения живого каталога. Объявлен здесь, у потребителя; реализация — `repo/kaname/pg/catalog_repo.go`.
type Rows ¶
type Rows struct {
Modules []string
Resources []ResourceRow
Verbs []VerbRow
}
Rows — ЖИВОЕ множество каталога, прочитанное одним обращением.
Все три перечня приходят ВМЕСТЕ и от одного чтения. Разнести их по трём вызовам значило бы допустить снимок, собранный из разных моментов времени: ресурс уже снят, а его глаголы ещё живы — состояние, которого в базе не бывает ни при каком порядке применения.
type Snapshot ¶
type Snapshot struct {
// contains filtered or unexported fields
}
Snapshot — держатель каталожного факта в процессе.
Почему СНИМОК, а не запрос на пути запроса ¶
Каталог спрашивают на горячих путях — создание и правка роли, пересчёт проекции, сборка кортежей, — а сам он мал (десятки ресурсов, сотня глаголов) и меняется реже всего в схеме. Запрос к базе на каждом обращении оплачивался бы запросом арендатора; снимок платит один раз за период.
Отставание ОГРАНИЧЕНО и НАЗВАНО ¶
Величина отставания — период обновления, и он задаётся профилем развёртывания, а не вшит: вшитая величина есть решение, принятое за оператора и не предъявленное ему.
Отказ обновления НЕ фатален и НЕ тих ¶
Ронять службу из-за отставшего снимка значило бы менять ограниченное отставание на полный отказ. Но и молчать нельзя: контроль, о чьих отказах никто не узнаёт, неотличим от отсутствующего. Отсюда пара — запись журнала уровня `Error` и счётчик.
func NewSnapshot ¶
func NewSnapshot(initial Rows, src RowSource, log *slog.Logger, obs RefreshObserver) (*Snapshot, error)
NewSnapshot заполняет снимок УЖЕ ПРОЧИТАННЫМИ строками.
Строки приходят параметром, а не читаются здесь, и это несущее решение, а не удобство: на старте их уже прочитал страж паритета, и второе чтение об одном предмете дало бы два места, которые разойдутся молча. Проба `IAM-CT-2-01` утверждает это РАВЕНСТВОМ: операторов к таблицам каталога за время старта ровно столько, сколько шлёт сам страж.
Заполнение НЕ считается обновлением: зачти его — и «ноль обновлений за жизнь процесса» станет неотличимо от одного, то есть счётчик перестанет отвечать на вопрос, ради которого заведён.
func (*Snapshot) Refresh ¶
Refresh перечитывает живое множество и подменяет факт целиком.
Подмена атомарна: вызывающий, взявший факт до неё, доработает на прежнем согласованном множестве, а не увидит половину обновления.
Отказ ОСТАВЛЯЕТ ПРЕЖНИЙ снимок — включая случай пустого ответа без ошибки: пустой снимок отверг бы все правила арендатора разом, и это читалось бы как «продукт сломан», а не как «условие не создано».
func (*Snapshot) Run ¶
РЕПЛИКИ: на-реплику — петля обслуживает снимок СВОЕГО процесса: каталожный факт живёт в памяти реплики, и обновить его за соседа нельзя by construction. Общего состояния она не трогает, в базу только ЧИТАЕТ, поэтому N реплик дают N одинаковых чтений закрытого каталога — цена, ограниченная числом реплик и периодом, а не нагрузкой арендатора.
Run обновляет снимок с назначенным периодом и завершается по отмене контекста.
Отказ отдельного круга цикл НЕ прерывает: он уже сообщён журналом и счётчиком, а прекращение обновлений превратило бы ограниченное отставание в бессрочное.
type Source ¶
type Source interface {
Facts() *Facts
}
Source — читатель каталожного ФАКТА, каким его видит use-case: он знает, что факт откуда-то приходит, и не знает откуда.
Возвращается ЗНАЧЕНИЕ на момент вызова, а не держатель: обновление снимка подменяет указатель целиком, поэтому вызывающий, взявший факт один раз, работает на согласованном множестве до конца своего вычисления и не увидит половину обновления.
type VerbRow ¶
type VerbRow struct {
Module string
Resource string
// Verb — каноническая форма БЕЗ приставки отношения, та же, какой говорит
// `role_verb.verb`.
Verb string
// PerObject — строка производит пообъектное отношение `v_<verb>`. Ложь
// означает глагол, законный АВТОРСКИ и не дающий кортежа ни на одном
// объекте: его действие несёт ярус на родителе (задача #1863).
//
// Читателю этот признак НЕ безразличен, и в этом весь его смысл: набор
// глаголов типа обязан оставаться пообъектным, потому что по нему
// материализуются кортежи. Пропустить сюда ярусную строку значило бы вернуть
// снятое отношение целиком.
PerObject bool
}
VerbRow — одна ЖИВАЯ строка `kaname.catalog_verb`.