catalog

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 13, 2026 License: AGPL-3.0 Imports: 8 Imported by: 0

Documentation

Overview

Package catalog — КАТАЛОЖНЫЙ ФАКТ, прочитанный из СТРОК: какие пары «модуль.ресурс» грантуемы и какие глаголы объявлены каждой.

───────────────────────────────────────────────────────────────────────────── ЧЕМ ЭТОТ ПАКЕТ ОТЛИЧАЕТСЯ ОТ ЛИТЕРАЛА `authzmap`

Литерал `authzmap.objectTypes` / `typeVerbRelations` остаётся и снятию не подлежит: он производная канона `fga_model.fga`, проверяемая гейтом дрейфа, и без него строки каталога остались бы без якоря к канону вовсе. Литерал отвечает на вопрос «что объявлено КАНОНОМ»; этот пакет — на вопрос «что живо В БАЗЕ ПРЯМО СЕЙЧАС».

Пока строки пишет только миграция, ответы совпадают, и совпадают не случайно: страж старта (`seed.AssertCatalogParity`) сверяет литерал с живыми строками на КАЖДОМ старте и при расхождении отказывает в пуске. Различие появляется в один момент — когда строка снята в РАБОТАЮЩЕМ процессе: страж своё уже отработал, а читатель на литерале продолжит считать снятый тип живым до следующего перезапуска. Наблюдаемо это не поломкой, а «прав не выдали» либо, что хуже, выдачей по типу, который платформа сняла.

───────────────────────────────────────────────────────────────────────────── ИМЯ ТИПА МОДЕЛИ ПРИЕЗЖАЕТ СТРОКОЙ — И ЭТО НЕ ВТОРОЙ ПЕРЕХОДНИК

Здесь стояло «имя типа МОДЕЛИ строки каталога не несут — такой колонки в `catalog_resource` нет». Колонка заведена (`object_type`, миграция `20260903112400`), и утверждение снято вместе со своим предметом.

Причина, по которой оно держалось, названа честно и была верна: два места, знающих соответствие, разойдутся тихо. Разошлись бы — если бы соответствие здесь ВЫЧИСЛЯЛОСЬ. Оно не вычисляется: словарь по-прежнему объявляет манифест модуля одной строкой вместе с ресурсом, а колонка есть место, куда он доезжает. Переходник остался один, у него сменилось место хранения.

Согласие колонки с таблицей, порождённой сборкой, держит страж старта (`seed.AssertCatalogParity`) — на КАЖДОМ старте, с отказом в пуске при расхождении.

`cluster` строки по-прежнему не имеет вовсе: вершина иерархии ресурсом не является, и спрашивать её набор у каталога нечем ни при какой колонке.

Index

Constants

View Source
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

func NewFacts(rows Rows) (*Facts, error)

NewFacts собирает факт из живых строк каталога.

ПУСТОЕ МНОЖЕСТВО ОТВЕРГАЕТСЯ, и это не перестраховка. Пустой снимок отверг бы ВСЕ правила арендатора разом, и снаружи это читалось бы как «продукт сломан», а не как «миграции не применены». На старте до этого не доходит — страж отказывает в пуске раньше, — но обновление снимка идёт БЕЗ стража, и пустой ответ там обязан быть отказом обновления, а не новым снимком.

func (*Facts) AllVerbVocabulary

func (f *Facts) AllVerbVocabulary() []string

AllVerbVocabulary — глаголы, которые объявляет ХОТЬ ОДИН живой глагольный тип.

func (*Facts) CommonVerbVocabulary

func (f *Facts) CommonVerbVocabulary() []string

CommonVerbVocabulary — глаголы, общие ДЛЯ ВСЕХ живых глагольных типов.

func (*Facts) FGAObjectType

func (f *Facts) FGAObjectType(dotted string) (string, bool)

func (*Facts) GrantedVerbs

func (f *Facts) GrantedVerbs(fgaType string, authored, typeVerbs []string) []string

func (*Facts) IsKnownModule

func (f *Facts) IsKnownModule(module string) bool

IsKnownModule — членство модуля в ЖИВОМ каталоге. Реализует `domain.ModuleSet`: домен набора не знает и получает его отсюда.

Почему ответ берётся у снимка, а не у запроса к базе

Каталог мал и меняется реже всего в схеме, а спрашивают его на горячем пути создания и правки роли. Запрос на каждом обращении оплачивался бы запросом арендатора. Отставание при этом ОГРАНИЧЕНО и НАЗВАНО — оно равно периоду обновления снимка, задаваемому профилем развёртывания (см. Snapshot), а не сроку жизни процесса: снятие модуля доезжает до пути запроса за один период, без перезапуска.

Подстановочный знак `*` модулем НЕ является: строки с таким именем в каталоге нет и быть не может (`catalog_module_nonempty` плюс грамматика имени), а разрешает его политика правила, а не набор.

func (*Facts) Modules

func (f *Facts) Modules() []string

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

func (f *Facts) VerbsOfType(fgaType string) []string

VerbsOfType — ГЛАГОЛЫ, объявленные ЖИВЫМ типом, отсортированно; nil у типа, чья строка снята либо которого в каталоге нет вовсе (`cluster`).

Возвращается КОПИЯ: значение снимка вызывающему не принадлежит, а испортить его он мог бы одной сортировкой на месте.

type Fixed

type Fixed struct{ F *Facts }

Fixed — источник, отдающий один и тот же факт. Нужен там, где обновления не бывает по построению: разовому пересчёту на старте и пробам.

func (Fixed) Facts

func (f Fixed) Facts() *Facts

Facts реализует Source.

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

type RowSource interface {
	ReadLiveCatalog(ctx context.Context) (Rows, error)
}

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) Facts

func (s *Snapshot) Facts() *Facts

Facts — каталожный факт на момент вызова. Реализует Source.

func (*Snapshot) Refresh

func (s *Snapshot) Refresh(ctx context.Context) error

Refresh перечитывает живое множество и подменяет факт целиком.

Подмена атомарна: вызывающий, взявший факт до неё, доработает на прежнем согласованном множестве, а не увидит половину обновления.

Отказ ОСТАВЛЯЕТ ПРЕЖНИЙ снимок — включая случай пустого ответа без ошибки: пустой снимок отверг бы все правила арендатора разом, и это читалось бы как «продукт сломан», а не как «условие не создано».

func (*Snapshot) Run

func (s *Snapshot) Run(ctx context.Context, period time.Duration)

РЕПЛИКИ: на-реплику — петля обслуживает снимок СВОЕГО процесса: каталожный факт живёт в памяти реплики, и обновить его за соседа нельзя 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`.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL