domain

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: AGPL-3.0 Imports: 29 Imported by: 0

Documentation

Overview

access_binding_scope.go — typed Scope enum for the RBAC v2 AccessBinding shape. The Scope tier anchors the binding in the cluster ▶ account ▶ project hierarchy; per-resourceName grants emit direct FGA tuples that respect Scope only as a sanity guard.

client_assertion.go — доменные понятия аутентификации клиента подписанным утверждением (задача kacho#898, приёмка F2).

Что здесь и чего здесь нет

Здесь — понятия, о которых обязаны договориться ВСЕ слои: чем клиент себя называет, из какого реестра он разрешается, и чем отличается повтор от первого предъявления. Разбор подписи, чтение базы и форма HTTP-ответа живут у своих слоёв: домен о них не знает (чистый Go, только stdlib).

Package domain — entities + value-types for kaname.

The domain layer depends on stdlib + multierr only — never on pgx, grpc-stubs, or sqlc. `kacho-proto` is permitted strictly for envelope types (Operation, status); for concrete IAM proto-structs we have `internal/dto/toproto`.

constants_extended.go — id-префиксы и константы. Стиль формата отличается от corelib `ids.NewID` (3-char prefix + 17-char crockford-base32) — здесь используется `<prefix>_<17-char crockford>` с `_` separator, чтобы префиксы могли быть длиннее 3 символов (`cag`, `org`, `cond`, `evt`, `soc`).

DB CHECK constraints в миграциях 0011..0014 enforce'ат соответствие формату per-таблица (например `^cag_[0-9a-hjkmnp-tv-z]{17}$`).

federated_assertion.go — доменные понятия перечня доверенных издателей утверждения (задача kacho#1124).

Что этот перечень такое и чем он отличается от реестра клиентов

Реестр клиентов отвечает на вопрос «наша ли это строка и её ли ключ». Перечень доверенных издателей отвечает на другой: «согласны ли МЫ, что вот этот ПОСТОРОННИЙ издатель вправе поручиться вот за этот свой субъект». Ключевой материал здесь принадлежит не нам, и решение о доверии — единственное, что у нас есть.

Отсюда две вещи, которые верны здесь и неверны у реестра клиентов:

  1. **запись доверия имеет собственный срок.** У клиента срок означает «эта строка больше не выдаёт»; здесь — «мы больше не ручаемся за постороннего». Снятие доверия сроком обязано работать, иначе выданное однажды доверие переживёт то, ради чего выдавалось;
  2. **пустой перечень означает «не доверяем никому».** Величина, которая может быть пустой и не проверена на непустоту, означает «принимаем любого» — класс, записанный в корпусе про круг законных отправителей. Тут он даёт не «приняли лишний заголовок», а токен платформы предъявителю, которого мы не заводили вовсе.

ids_extended.go — IAM-style id generator.

Формат: `<prefix>_<17-char crockford-base32>` (lowercase, без I/L/O/U). Отличается от corelib `ids.NewID` тем, что префикс может быть длиннее 3-х символов (DB CHECK constraints в миграциях 0011..0014).

Источник энтропии — crypto/rand. 17 символов crockford × 5 бит = 85 бит.

limit.go — СЛОВАРЬ ВИДОВ, и только он. Авторитета величин здесь больше нет.

Задача продукта `PRO-Robotech/kacho#2117`, приёмка `KAN-QUOTA-1`, стадия S4, сценарий `KAN-Q4-14`, условие готовности `DoD S4` пп. 13-14.

───────────────────────────────────────────────────────────────────────────── ЧТО ОТСЮДА УШЛО И ПОЧЕМУ ЭТО НАЗВАНО, А НЕ СТЁРТО МОЛЧА

Файл держал авторитет величин целиком: идентичность потолка (`LimitID`), его область (`LimitScope` с тремя рукавами и старшинством), носителя (`LimitCarrier`), ЗАКРЫТЫЙ КАТАЛОГ двадцати четырёх видов (`countableKinds`, `CountableKind`) и разрешение действующей величины между областями (`Limit`, `LimitFilter`, `EffectiveLimit`, `ResolveEffective`).

Всё это — предмет ДРУГОГО продукта: служба доступа отвечает на вопрос «кому что разрешено», а не «сколько чего можно завести». Оператор, ставящий её в своём облаке, получал вместе с ней авторитет величин, которого не просил и которым не управлял.

───────────────────────────────────────────────────────────────────────────── ЧТО ОСТАЛОСЬ — И ПОЧЕМУ СНЯТЬ ЕГО ЗАОДНО БЫЛО БЫ РАСШИРЕНИЕМ ПОВЕРХНОСТИ

Остался СЛОВАРЬ: имя вида и разбор его частей. Служба по-прежнему СЧИТАЕТ три собственных вида (`iam.account`, `iam.user.credential`, `iam.serviceAccount.credential`) и по-прежнему отвечает арендатору, каков его потолок. Сменился ИСТОЧНИК величины: её объявляет посадка (`limit_posture_stated.go`, `П25`), а не внешний авторитет, которого в самостоятельной установке нет by construction.

Снять словарь вместе с авторитетом значило бы оставить установку без ограничения на число аккаунтов и путей входа — расширение поверхности, не названное ни в одном артефакте.

───────────────────────────────────────────────────────────────────────────── ЗАКРЫТОГО КАТАЛОГА ЗДЕСЬ БОЛЬШЕ НЕТ, И ЭТО ПРОВЕРЯЕТСЯ, А НЕ ОБЕЩАЕТСЯ

`internal/check` `TestLimitAuthorityIsGoneFromTheTree` судит узлы-имена прод-дерева и объявление `countableKinds` отдельной осью: возврат каталога — находка с координатой, а не тихое отрастание. Куда каталог уезжает, решает развилка платформы `kacho#2190`, и служба доступа её за владельца не решает.

oauth_ceremony.go — СОБСТВЕННАЯ ЦЕРЕМОНИЯ OAuth: код авторизации, семейство выданного по нему и обновляющий токен (kaname#313).

Домен здесь описывает ЗНАЧЕНИЯ и ИСХОДЫ; хранение, свёртки и операторы живут у слоя доступа. Чистый Go, только stdlib.

Исходов ПРЕДЪЯВЛЕНИЯ здесь нет, и это не пропуск

Обмен кода и оборот токена обновления исполняет движок фундамента (`corelib/oauthceremony`) над хранилищами слоя доступа, и исходы предъявления называет контракт его портов: «записи нет», погашен, обёрнут. Прежняя тройка исходов обмена и ротации принадлежала композициям слоя доступа, у которых прод-вызывающих не было, и снята вместе с ними (kaname#434). Здесь остаются исходы, которых у порта нет: заведение семейства в сессию, отсечка субъекта предъявленной строки, запись выпуска, занятость проверяющего секрета.

principal_claims.go — состав утверждений, называющий ПРИНЦИПАЛА, за которого говорит выпущенный нами токен.

Почему имена объявлены, а не выписываются по месту

Читателей у этих имён больше одного: их СТАВИТ чеканка и их ЧИТАЕТ приёмная сторона. Пока имена живут литералами по своим пакетам, их различие не является ничьей находкой — оно не выражено и потому не может покраснеть. Разойтись же им есть чем, и разошлись бы они МОЛЧА: переименование на выпуске оставило бы читателя без принципала не отказом, а тишиной — токен проверился бы целиком, а за кого он говорит, читатель не узнал бы. Отличить такое состояние от «токен выдан системной личности» на стороне читателя нечем.

Что означает отсутствие каждого

Вид и идентификатор ОБЯЗАТЕЛЬНЫ: «назвать некого» и «назван системный» — разные состояния, и смешивать их нельзя, потому что системная личность и есть та, которой принадлежат все служебные записи. Отображаемое имя косметическое: оно ни на одно решение не влияет и при отсутствии замещается идентификатором.

Index

Constants

View Source
const (
	ScopeTypeClusterDotted = "iam.cluster"
	ScopeTypeAccountDotted = "iam.account"
	ScopeTypeProjectDotted = "iam.project"
)

Dotted scope-type API projection (redesign-2026 F7). The AccessBinding scope-anchor is renamed resource_type/resource_id → scopeType/scopeId on the wire, with the word "resource" freed for the reintroduced target. The wire scopeType is dotted (`iam.{cluster,account,project}`) while the within-service storage keeps the bare kind (`cluster`/`account`/`project`) — the two are mapped at the API boundary (dto on output, handler on input). Only the three hierarchy tiers can anchor a binding, so the mapping is total over them.

View Source
const (
	PrefixAccount        = "acc"
	PrefixProject        = "prj"
	PrefixUser           = "usr"
	PrefixServiceAccount = "sva"
	PrefixGroup          = "grp"
	PrefixRole           = "rol"
	PrefixAccessBinding  = "acb"
	// PrefixOperationIAM — separate prefix so api-gateway routes
	// `OperationService.Get(id)` correctly (by the first 3 characters of the
	// id). MUST differ from PrefixAccount — otherwise `acc<…>` (Account) and
	// an Operation on Account would collide.
	PrefixOperationIAM = "iop"
)

ID prefixes for kaname resources. Mirrors the canonical prefix constants in `pkg/ids` so the use-case layer can refer to short names locally (`PrefixAccount`, `PrefixProject`, …) without re-importing corelib for trivial id construction.

View Source
const (
	PrincipalTypeSystem         = "system"
	PrincipalTypeAnonymous      = "anonymous"
	PrincipalTypeUser           = "user"
	PrincipalTypeServiceAccount = "service_account"
	PrincipalDisplayBootstrap   = "kaname-bootstrap"
	PrincipalIDBootstrap        = "bootstrap"
)

PrincipalType — allowed values for kaname.operations.principal_type. 'system' / 'kaname-bootstrap' are used for internal/background flows; 'user' / 'service_account' come from OIDC at the api-gateway edge.

View Source
const (
	PrefixClusterAdminGrant = "cag"
	PrefixCondition         = "cond"
	PrefixSAOAuthClient     = "soc"
	PrefixUserOAuthClient   = "uoc"
	PrefixAuditEvent        = "evt"

	// ClusterSingletonID — единственный валидный id для кластера.
	ClusterSingletonID = "cluster_root"

	// OwnerRoleID — deterministic id of the net-new `owner` system-role
	// (RBAC explicit-model 2026), seeded by migration 0035 as
	// `'rol' || substr(md5('owner'),1,17)`. The Account.Create auto-binding
	// references it. Kept here so the use-case does not re-hash the name at
	// runtime (and stays in lockstep with the migration seed id).
	OwnerRoleID = "rol72122ce96bfec66e2"

	// ClusterAdminRoleID — deterministic id of the system cluster-admin role
	// (`admin`, name 'admin'), seeded by migration 0001 as
	// `'rol' || substr(md5('admin'),1,17)` and re-seeded with its `*.*.*` rules by
	// migration 0031. This is the canonical "GLOBAL super-admin" role.
	//
	// IMPORTANT: the `owner` role (OwnerRoleID) carries the SAME `*.*.*`
	// wildcard SHAPE as cluster-admin, so the GLOBAL+all exception MUST be
	// keyed on this PINNED id (+ is_system), not on the shape alone — otherwise
	// owner would be misclassified as cluster-admin and slip past the reject.
	ClusterAdminRoleID = "rol21232f297a57a5a74"

	// SystemAdminRoleID — the hand-rolled deterministic id of the second `*.*.*`
	// superuser system role (`system.admin`; свод посеял её под именем
	// `kacho-system.admin`, перевод — миграция 20260913143007, kacho#2554), seeded by migration 0001 and
	// re-seeded with `*.*.*` rules by migration 0031. Also a legitimate
	// cluster-admin superuser.
	SystemAdminRoleID = "rol000000000sysadmin"

	// SystemViewerRoleID — рукописный детерминированный id второй системной роли
	// пола каталога (`system.viewer`; свод посеял её под именем `kacho-system.viewer`,
	// перевод — миграция 20260913143007, kacho#2554), посеянной миграцией 0001.
	//
	// ДЛИНА ЕГО — 21, а не 20, и это ФАКТ ПРИМЕНЁННОЙ МИГРАЦИИ, а не описка,
	// которую можно поправить: id неизменяем на всю жизнь ресурса (ban #15,
	// «операции смены id НЕ существует»), а применённую миграцию править нельзя
	// (ban #5). Переименование поздней миграцией сверх того есть перенос выдачи с
	// одной роли на другую — тихое расширение прав, отвергаемое гейтом
	// `TestNoMigrationMovesGrantsBetweenRoles`.
	//
	// Поэтому литерал назван здесь и стоит в закрытом перечне
	// `SeededResourceIDs()`: проверка формы принимает то, что продукт сам посеял,
	// а не объявляет собственный посев негодным. Задача kacho#1808.
	SystemViewerRoleID = "rol000000000sysviewer"
)

id-префиксы. Singleton (`cluster_root`) — литерал, не генерируется. Outbox events (`evt_`) — ULID-based, длина 20..30.

View Source
const (
	ConditionMFAFresh        = "mfa_fresh"
	ConditionNonExpired      = "non_expired"
	ConditionSourceIPInRange = "source_ip_in_range"
	ConditionBusinessHours   = "business_hours"
	ConditionDeviceCompliant = "device_compliant"
)

Condition expressions whitelist (migration 0012 access_binding_conditions_expression_whitelist_ck).

View Source
const (
	MaxGrantDelegationRoles        = 16
	MaxGrantDelegationTargetTypes  = 16
	MaxGrantDelegationSubjectTypes = 4
)

Пределы осей делегирования — границы размера запроса (Р6 приёмки), а не предел числа ресурсов: число делегирований kaname не ограничивает (Р-В7).

View Source
const (
	// RevokeReasonLogout — собственный выход человека: причина снятия его
	// записи сессии и причина отсечки.
	RevokeReasonLogout = "logout"
	// RevokeReasonPasswordChange — смена пароля и завершение восстановления:
	// причина снятия прочих записей сессии и причина отсечки.
	RevokeReasonPasswordChange = "password-change"
	// RevokeReasonSecondFactorRemoved — снятие второго фактора самим человеком
	// гасит ПРОЧИЕ его сессии (Ф12 Р9, kacho#1281): журнал отличает это от
	// выхода и от смены пароля. Причина снятия записи сессии.
	RevokeReasonSecondFactorRemoved = "second-factor-removed"
	// RevokeReasonSecondFactorReset — причина отсечки, которую пишет сброс
	// второго фактора администратором облака (Ф12 Р10): все сессии человека покрыты
	// отсечкой `now` существующим писателем принудительного выхода. В словарь
	// снятия сессии НЕ входит: ни одному снимающему методу она не передаётся.
	RevokeReasonSecondFactorReset = "second-factor-reset"
	// RevokeReasonAccessKeysReset — причина отсечки, которую пишет сброс ключей
	// доступа администратором облака (kaname#638; приёмка
	// `cloud-administrator-resets-login-methods.md`, Р4): все сессии человека
	// покрыты отсечкой `now`. Как и у сброса второго фактора, в словарь снятия
	// сессии НЕ входит: записи сессии она не снимает, а отсекает.
	RevokeReasonAccessKeysReset = "access-keys-reset"
	// RevokeReasonAdminForceLogout — выход, произведённый распорядителем
	// (`InternalIAMService.ForceLogout`; kaname#334, приёмка
	// `forced-exit-has-its-own-session-end-reason.md`, Р1, Р3). Роли у слова
	// ДВЕ, и объявлено оно одно:
	//   - причина снятия НАШИХ записей сессии входа принудительным выходом.
	//     Это слово закрытого словаря, и из запроса оно не берётся (Р2):
	//     свободная причина распорядителя в запись сессии не идёт;
	//   - умолчание СВОБОДНОЙ причины отсечки и события, когда запрос
	//     причины не несёт. Совпадение намеренное: предмет у обеих записей
	//     один — принудительный выход.
	RevokeReasonAdminForceLogout = "admin-force-logout"
	// RevokeReasonEmailVerified — подтверждение адреса снимает ПРОЧИЕ сессии
	// человека (kaname#456, Р10 п. 3): обычное положение получает только та
	// сессия, в которой код предъявлен. Причина снятия записи сессии; отсечки
	// подтверждение не пишет.
	RevokeReasonEmailVerified = "email-verified"
	// RevokeReasonEmailChanged — исход смены адреса снимает ПРОЧИЕ сессии
	// человека (kaname#635, Р8 п. 4): адрес — канал восстановления, и после
	// смены канала прежние устройства за учётную запись не держатся; текущая
	// сессия получает новый носитель. Причина снятия записи сессии; отсечки
	// смена адреса не пишет.
	RevokeReasonEmailChanged = "email-changed"
	// RevokeReasonEndedFromAnotherSession — человек снял свою запись из ДРУГОЙ
	// своей сессии: выход из выбранной либо из всех, кроме текущей (kaname#634,
	// приёмка `own-sessions-are-listed-and-ended-by-their-owner.md`, Р6). Причина
	// снятия записи сессии; отсечки эти глаголы не пишут.
	RevokeReasonEndedFromAnotherSession = "ended-from-another-session"
	// RevokeReasonAccessKeyRevoked — снятие ключа доступа гасит сессии
	// человека (Ф13 Р8, kaname#669): ключ снимают чаще всего потому, что
	// устройства лишились, и живая сессия на нём — сессия держателя устройства.
	// Причина снятия записи сессии и причина отсечки.
	RevokeReasonAccessKeyRevoked = "access-key-revoked"
	// RevokeReasonIdentityRemoved — снятие личности (`UserService.Delete`)
	// обрывает всё, выданное ей до момента снятия (kaname#664, Р5). Только
	// причина отсечки: записи сессии уходят каскадом той же транзакции и
	// причиной снятия не помечаются.
	RevokeReasonIdentityRemoved = "identity-removed"
	// RevokeReasonIdentityBlocked — блокировка личности (`UserService.Block`)
	// обрывает всё, выданное ей до момента блокировки, и снятие блокировки
	// выданного до неё не возвращает (kaname#717). Только причина отсечки:
	// записи сессии блокировка не снимает, а отсекает.
	RevokeReasonIdentityBlocked = "identity-blocked"
	// RevokeReasonAddressClaimed — запись с неподтверждённым адресом перешла к
	// владельцу ящика, предъявившему код восстановления (kaname#458, Р3 п. 3,
	// Р7): все сессии записи сняты, выданное до перехода отсечено. Журнал
	// отличает это от смены пароля самим человеком — у оператора это разные
	// вопросы. Причина снятия записи сессии и причина отсечки.
	RevokeReasonAddressClaimed = "address-claimed"
)

Слова причин ДВУХ записей — одним объявлением (Ф3 Р4, Р6, Р9).

Константы ниже служат двум словарям. Закрытый — причина СНЯТИЯ записи сессии: ограничение `human_sessions_ended_reason_check`, его перечень в домене — HumanSessionEndReasons. Открытый — причина ОТСЕЧКИ (`user_token_revocations.reason`, ограничена только длиной). Какой записи служит каждая константа, сказано у неё. Слово, которое пишут обе записи, объявлено один раз: второе написание разошлось бы с первым молча.

`password-change` — ТО ЖЕ значение, что пишет существующий хук завершения восстановления (`internal/apps/kaname/api/user`). `logout` — своё: журнал обязан отличать выход от смены пароля (Ф1-63).

View Source
const (
	// ClaimPrincipalType — вид принципала: `user` либо `service_account`.
	ClaimPrincipalType = "kaname_principal_type"
	// ClaimPrincipalID — идентификатор принципала.
	ClaimPrincipalID = "kaname_principal_id"
	// ClaimPrincipalDisplay — отображаемое имя принципала. Косметическое.
	ClaimPrincipalDisplay = "kaname_principal_display_name"
	// ClaimSAKeyID — идентификатор строки удостоверения служебной учётки, по
	// которому выдан токен (kaname#947). Это КЛЮЧ ОТСЕЧКИ: снятие строки пишет
	// отсечку её идентификатором (триггер схемы
	// `sa_oauth_client_removal_cuts_minted_tokens`), и правило отзыва
	// (`tokenrevocation`) читает его из состава. Токен, выданный по
	// удостоверению и не несущий этого имени, отзыв удостоверения не снимает —
	// он действует до своего срока.
	ClaimSAKeyID = "kaname_sa_key_id"
)
View Source
const (

	// MaxRules — rules[] cardinality per role.
	MaxRules = 64
	// MaxCompiledPermissions — compiled-permissions cap.
	// Lockstep with DB CHECK + proto (size).
	MaxCompiledPermissions = 1024
)
View Source
const (
	// SystemAccountName — имя системного аккаунта, в котором живут служебные
	// записи модулей.
	SystemAccountName = "system"
	// SystemAdminRoleName — имя встроенной роли администратора установки.
	SystemAdminRoleName = "system.admin"
	// SystemViewerRoleName — имя встроенной роли пола каталога.
	SystemViewerRoleName = "system.viewer"
	// BootstrapAdminSAName — имя служебной записи, которой чеканят первый
	// неинтерактивный токен.
	BootstrapAdminSAName = "bootstrap-admin"
)

Объявленные написания посевной идентичности (§2.3 приёмки). Имя называет УСТАНОВКУ, а не продукт: у стороннего оператора ею не владеет ни платформа, ни служба, поэтому различитель в имени лгал бы в обе стороны.

Названы здесь, рядом с окном, а не у каждого читателя: иначе объявлений стало бы столько же, сколько читателей, и разошлись бы они молча.

View Source
const (
	SpeakerAnonymous     = string(SubjectTypeSystemClass) + ":" + string(ClassAnonymous)
	SpeakerAuthenticated = string(SubjectTypeSystemClass) + ":" + string(ClassAuthenticated)
)

Субъекты классов в записи модели прав (`тип:идентификатор`).

View Source
const AccessKeyChallengeBytes = 32

AccessKeyChallengeBytes — длина случайного испытания: 32 байта = 256 бит, не меньше 16 требуемых нормой (§13.4.3).

View Source
const AccessKeyDescriptionMax = 256

AccessKeyDescriptionMax — предел описания в ЗНАКАХ, единица схемы (`CHECK (length(description) <= 256)`), как у удостоверений-соседей.

View Source
const AccessKeyLoginChallengeContextMax = 128

AccessKeyLoginChallengeContextMax — предел длины контекста формы: контекст — 32 случайных байта в base64url без дополнения (43 знака); предел с запасом держит ограничение схемы, а не доверие к вызывающему.

View Source
const CarrierIdentity = "identity"

CarrierIdentity — носитель «личность» в том виде, в каком он стоит в схеме.

Значение принадлежит СХЕМЕ (`project_resource_quotas.carrier_type` и предикат списывающего триггера), поэтому у адаптера базы свой литерал, а не ссылка сюда: адаптер обязан называть то, что лежит в столбце, даже если домен однажды назовёт это иначе. Совпадение трёх написаний закреплено пробой (`internal/repo/kaname/pg`), а не подразумевается.

View Source
const CatalogEndpoint = "GET /iam/v1/permissionCatalog"

CatalogEndpoint — where a client reads the grantable taxonomy AND the retired resources with their successors.

ОБЪЯВЛЕНИЕ ОДНО НА ДЕРЕВО. Адрес называют ОБЕ полосы отказа на несуществующем ресурсе — снятия (здесь) и грантуемости (`api/role/rules_catalog.go`), — и до kacho#1814 первая молчала: различие никем не решалось, оно побочный эффект того, что гейты писались порознь.

Адрес назван в отказе потому, что каноническое написание намеренно НЕ единообразно по модулям (`compute.instance` / `iam.serviceAccount` единственного числа; `storage.volumes` / `registry.registries` / `loadbalancer.networkLoadBalancers` множественного), поэтому «догадайся» исполнимой инструкцией не является.

View Source
const CeremonyHandleBytes = 64

CeremonyHandleBytes — длина рукоятки: верхняя граница нормы для `user.id`.

View Source
const CeremonyScopeOpenID = "openid"

CeremonyScopeOpenID — область интерактивного входа: её запрашивает первопартийный клиент (консоль, CLI), и она проецируется в утверждение `scope` выданного токена доступа (RFC 9068 §2.2.3).

Прав область НЕ несёт: решение о доступе принимает модель (приёмка LINE-A-1 Р9), и выданное по коду судится так же, как всякий наш токен. Токена личности церемония не выдаёт (`corelib/oauthceremony`, doc.go) — область называет вид гранта, а не обещание ID-токена.

View Source
const ClientDescriptionMaxRunes = 512

ClientDescriptionMaxRunes — предел хранимого описания в рунах (Р3).

View Source
const CustomRoleNameForm = `^[a-z][a-z0-9_]{0,40}$`

Regex / limits — centralised so domain.Validate and the DB CHECKs agree.

Формы имени ресурса здесь БОЛЬШЕ НЕТ: она объявлена один раз на всё дерево (`pkg/validate/nameform`), и iam читает то же объявление (kacho#1279). Прежде здесь стояла своя — `^[a-z][-a-z0-9]{2,62}$`, — и расходилась с каноном в обе стороны: была УЖЕ него по началу имени (буква вместо буквы-или-цифры) и по длине (от 3 вместо 1), и ШИРЕ него по хвосту (допускала имя, кончающееся дефисом). Гейт единственности формы её не видел by construction: он ловит байт-идентичную копию канона, а независимо написанную регулярку — нет, и свою слепую зону называет сам.

Идентификатор роли остаётся своим и формой имени НЕ судится: `roles/vpc.admin` — то, на что ссылаются привязки, а не косметическая метка (записанное решение владельца, kacho#715). CustomRoleNameForm — форма имени ПОЛЬЗОВАТЕЛЬСКОЙ роли. Зеркало ограничения таблицы `roles_custom_name_check` (`0056_role_definition_tier.sql`, применённая миграция, ban #5).

View Source
const DescriptionMax = 256

DescriptionMax — предел описания в ЗНАКАХ, а не в байтах.

Единица — та же, что у ограничений базы (`*_description_check`: `length(description) <= 256`, Postgres считает знаки) и у загрузчика манифеста (`minRoleDescriptionRunes`). Счёт в байтах отвергал кириллическое описание в 129 знаков, которое база принимает: описание роли модуля, доставленное манифестом, проходило загрузку и роняло применение ролей на старте (kaname#856, роль `registry.public.read`).

View Source
const EditorDeleteNote = "co-materialized on in-scope leaf objects, NOT on the account/project anchor itself"

EditorDeleteNote is the verbatim explanation of the editor delete-qualifier.

View Source
const EditorDeleteVerb = "delete*"

EditorDeleteVerb is the qualifier appended to an editor-tier role's effective verbs (delete of in-scope leaf objects, not the anchor).

View Source
const ErrCredentialKindField = "credential_kind"

ErrCredentialKindField — имя поля, которое обязан называть отказ. Объявлено одним местом, чтобы тон отказа не разошёлся между двумя глаголами выдачи. #nosec G101 -- это ИМЯ ПОЛЯ запроса, которое обязан назвать текст отказа (конвенция отказов Kachō), а не значение удостоверения.

View Source
const HumanSessionIDPrefix = "hss"

HumanSessionIDPrefix — приставка идентификатора записи в дефисном каноне.

View Source
const LegacyPublicWildcardWord = "user:*"

LegacyPublicWildcardWord — прежнее слово публичного чтения в составе субъектов манифеста. Рендер блока переводит его в класс (`modelrender.subjectWords`), и поэтому оно — одно из написаний класса (IsClassSubjectWord).

View Source
const MaxAssertionIDLength = 256

MaxAssertionIDLength — потолок длины идентификатора однократности.

Значение выбирает ПРЕДЪЯВИТЕЛЬ, поэтому его длина — это его выбор нашего расхода. Потолок повторяет ограничение схемы: два места об одном предмете разошлись бы молча, поэтому проба сверяет их между собой.

View Source
const MaxSubjectsPerBinding = 32

MaxSubjectsPerBinding — hard upper bound on subjects[]. Anti-DoS + tractable per-subject audit/expand. 0 < n ≤ 32 enforced by NormalizeSubjects (sync, INVALID_ARGUMENT) and mirrored by the DB.

View Source
const MaxTargetResourcesPerBinding = 256

MaxTargetResourcesPerBinding — жёсткий потолок target.resources[]. Защита от отказа в обслуживании и посильная пообъектная материализация: реконсайлер пересекает каждый подошедший по правилам объект с целью через ЛИНЕЙНЫЙ AccessTarget.Contains внутри ОДНОЙ синхронной транзакции записи под рекомендательной блокировкой выдачи и повторяет это на каждый объект, созданный в кластере (forwardObjectForBinding). Неограниченный перечень стоил бы |подошедших|×|цели| сравнений на горячем пути и неограниченной строки JSONB. 0 < n ≤ 256 судится синхронно (INVALID_ARGUMENT), как MaxSubjectsPerBinding.

View Source
const OwnLaneSubjectHead = "own:"

OwnLaneSubjectHead — голова идентичности, отчеканенной нашей полосой.

View Source
const PKCEMethodS256 = "S256"

PKCEMethodS256 — ЕДИНСТВЕННЫЙ принимаемый метод испытания. `plain` не заводится: он сводит PKCE к передаче секрета в открытом виде, и колонка, допускающая его, была бы местом, куда он однажды ляжет.

View Source
const PersonalAccountNamePrefix = "personal-cloud-"

PersonalAccountNamePrefix — знак ПРОСТРАНСТВА ИМЁН личных аккаунтов (приёмка Ф4 Р9). Личный аккаунт заводит система как следствие регистрации (Р8): его имя `personal-cloud-<хвост>` выбирается без участия арендатора, и «personal-cloud-…» человек прочтёт, а идентификатор — нет.

Написание объявлено ЗДЕСЬ единожды и читается ДВУМЯ сторонами: генератором имени зеркала (`user.RegisterMirrorTx`) и резервом префикса в `AccountService.Create/Update`. Две копии разошлись бы молча — резерв перестал бы накрывать реально заводимые имена, а имя зеркала перестало бы попадать в зарезервированное пространство (code-authoring §5.1: одно написание значения).

View Source
const PrefixAccessKey = "ak"

PrefixAccessKey — префикс дефисной формы `ak-<17>`; сама константа канона — в фундаменте (`ids.PrefixAccessKeyHyphen`), здесь только имя для отказов и таблицы форм.

View Source
const PrefixGrantDelegation = "gdl"

PrefixGrantDelegation — префикс идентификатора делегирования (`gdl` + 17 знаков crockford-base32, `ids.NewID`).

View Source
const PublicReaderRelation = "public_reader"

PublicReaderRelation — имя отношения публичного чтения в блоке контейнера.

Объявлено ОДИН раз: его порождает генератор блоков модели, пишет разворачивание выдачи и принимает страж прямого факта (`relation_fact_subject_class_reads_only`), и второе написание разошлось бы с первым молча.

View Source
const PublicationRelation = "v_get"

PublicationRelation — отношение хранимой публикации.

View Source
const PublicationSubject = SpeakerAnonymous

PublicationSubject — ХРАНИМЫЙ субъект публикации объекта для чтения без входа (Р8, SC-12).

Входных слов у соседа два, перечень закрыт правилом приёма фундамента (`corelib/authz/proxytuple.PublicReadSubjects`, kaname#893): переходное `user:*` и `system_class:anonymous`, оба + `v_get`. Здесь — ЕДИНСТВЕННЫЙ субъект, которого хранит служба, какое бы слово ни пришло. Его зовут запись и снятие публикации, разбор остаточных фактов и миграция переезда: снятие, назвавшее другое написание, удалило бы несуществующий факт, и чтение анонимом устояло бы (SC-13).

View Source
const RecoveryCodeLength = 10

RecoveryCodeLength — длина кода в знаках алфавита без разделителя: 10 знаков по 5 бит — 50 бит случайности.

View Source
const SessionBearerBytes = 32

SessionBearerBytes — длина случайной части носителя в байтах: 32 байта = 256 бит, не меньше 128 требуемых (Ф3 §8 инв. 9). В URL-безопасном base64 без дополнения это 43 знака — не короче 22 требуемых (Ф3-08).

View Source
const ShortIDLen = 20

ShortIDLen — full id length (prefix + body); matches pkg/ids.

View Source
const SystemRoleNameForm = `^[a-z][-a-z0-9]*(\.[a-z][a-z0-9_]*){0,2}$`

SystemRoleNameForm — форма имени СИСТЕМНОЙ роли. Зеркало ограничения таблицы `roles_system_name_check` того же файла: нижний регистр, дефис в первом сегменте, подчёркивание в сегментах после первого, не более трёх сегментов.

Здесь стояло `^roles/[a-z]+\.[a-z]+$`, и этой форме не удовлетворяла НИ ОДНА живая строка: предикат `grep -c "'roles/"` по каталогу миграций даёт ноль во всех файлах, а имена продукта записаны как `vpc.network.admin`. То есть `Role.Validate()` отвергал каждую системную роль платформы — и покраснеть это не могло, потому что системную роль в Go до задачи kacho#1824 никто не строил.

Правило пережило свой предмет молча; сегодня оно зеркалит ограничение, которое действительно судит записываемую строку.

View Source
const TextHumanSessionIDRule = "must match ^hss-[crockford-base32]{17}$"

TextHumanSessionIDRule — правило формы идентификатора записи, как его называет отказ формы (Р5): `Illegal argument sessionId: <правило>`.

Variables

View Source
var (
	// ErrCeremonySessionUnknown — строки сессии, в которую заводится семейство,
	// НЕТ ВОВСЕ.
	//
	// Отдельно от «не жива», и это не педантизм: прежняя форма отвергала
	// отсутствующую сессию внешним ключом, называя его имя, — то есть
	// различение уже существовало, и слить его в отказ о живости значило бы
	// сделать опечатку в идентификаторе неотличимой от гонки с выходом.
	ErrCeremonySessionUnknown = errors.New("ceremony session: unknown")

	// ErrCeremonySessionNotLive — строка сессии ЕСТЬ, но сессия уже не жива:
	// снята выходом (своим либо принудительным) либо истекла.
	//
	// Состояний РОВНО ДВА, и оба сливаются намеренно: предъявителю они говорят
	// одно — «входа, в котором идёт церемония, больше нет». Третьего состояния
	// («сессии нет») здесь НЕТ — у него свой признак выше.
	//
	// Исход ЗАВЕДЕНИЯ, а не предъявления: разбирается не предъявленная строка,
	// а право завести новую.
	ErrCeremonySessionNotLive = errors.New("ceremony session: not live")

	// ErrCeremonySubjectCutOff — предъявленная строка (код либо токен
	// обновления) есть, активна и в сроке, но сессия, в которой она выдана,
	// аутентифицирована НЕ ПОЗЖЕ отсечки своего субъекта
	// (`user_token_revocations.revoke_before`).
	//
	// Исход ПРЕДЪЯВЛЕНИЯ, и у порта фундамента своего слова у него нет: это не
	// повтор — семейство по нему не отзывается, и журнал не называет атакой отзыв
	// доступа, — и не истечение. Порту он уходит «записи нет»
	// (`oauthceremony.ErrGrantNotFound`), причиной в цепочке: гранта, по которому
	// предъявитель пришёл, больше нет.
	ErrCeremonySubjectCutOff = errors.New("ceremony session: authenticated no later than its subject's cutoff")

	// ErrAccessTokenFamilyNotLive — выпуск токена доступа не записан: семейства,
	// в которое он заводится, нет либо оно отозвано (kaname#319).
	//
	// Исход ЗАВЕДЕНИЯ записи выпуска, а не предъявления. Два состояния сливаются
	// намеренно: выпуску говорят одно — «семейства, в котором идёт выдача, больше
	// нет», — и токен клиенту уезжать не должен. Различает их, если понадобится,
	// строка семейства, а не этот отказ.
	ErrAccessTokenFamilyNotLive = errors.New("access token: family is unknown or revoked")

	// ErrGrantOwnerUnverified — правило выдачи удостоверениям человека ответило
	// «владелец-человек не подтвердил адрес» (kaname#456, Р5б): пары нет,
	// предъявленный токен обновления не обёрнут (оборот и выдача — одна единица
	// работы), семейство не отозвано. Наружу — тот же тон `invalid_grant`, что
	// у всякого отказа после именования кода.
	ErrGrantOwnerUnverified = errors.New("ceremony grant: the owner has not verified the email address")
	// ErrGrantRuleUndecidable — правило выдачи спросить не смогли: отказ
	// операции (`temporarily_unavailable`), а не `invalid_grant` — клиент,
	// получивший `invalid_grant`, выбросил бы живой токен обновления.
	ErrGrantRuleUndecidable = errors.New("ceremony grant: the issuance rule could not be asked")

	// ErrVerifierAtCapacity — проверяющий секрета занят: все места ёмкости
	// (`passwordverify`) заняты другими проверками. Отказ ПОВТОРЯЕМЫЙ и наш, а не
	// «секрет неверен»: несостоявшаяся сверка вердиктом не становится.
	ErrVerifierAtCapacity = errors.New("secret verifier: at capacity")
)
View Source
var ErrAccountNameOfTheIDForm = errors.New(
	"Illegal argument name: the account id form is reserved for the account's own id")

ErrAccountNameOfTheIDForm — имя формы идентификатора аккаунта, не равное собственному идентификатору (приёмка «идентификатор аккаунта можно указать при создании», Р6).

Имя аккаунта уникально на весь кластер, а имя по умолчанию — сам идентификатор. Пока идентификатор может прислать вызывающий, имя формы идентификатора, взятое другим аккаунтом, выдавало бы занятость идентификатора любому, кто создаёт аккаунт, и заранее отнимало бы у будущего аккаунта его имя по умолчанию. С правилом владелец такого имени — только аккаунт с этим самым идентификатором. Тот же предикат стоит в схеме проверкой `accounts_name_is_not_a_foreign_id`; текст отказа у обоих один.

View Source
var ErrAssertionClientUnknown = errors.New("client assertion: client does not resolve in a registry able to assert")

ErrAssertionClientUnknown — идентификатор не резолвится ни в одну строку реестра, СПОСОБНОГО к утверждению.

Один признак на два состояния — «строки нет вовсе» и «строка есть у вида клиента, не располагающего ключевым материалом», — и это не упрощение, а требование: различимые исходы дали бы предъявителю ОРАКУЛ существования. Он сообщал бы, заведён ли такой клиент, а по нему устанавливают и то, каким именно видом он заведён.

View Source
var ErrAssertionReplayed = errors.New("client assertion: single-use identifier already redeemed")

ErrAssertionReplayed — утверждение с этим идентификатором однократности уже предъявлялось ЭТИМ клиентом.

Отдельный признак, а не общий отказ: наружу все отказы аутентификации неразличимы (приёмка F2 §7), но ВНУТРЬ различимость обязана существовать — иначе счётчик исхода не с чем связать, и мёртвый контроль становится невидимым.

View Source
var ErrBasicCredentialRefused = errors.New("credential refused")

ErrBasicCredentialRefused — ЕДИНСТВЕННЫЙ отказ полосы базового секрета.

Неизвестный идентификатор, неверный секрет, истёкший срок, отозванное удостоверение, неактивный владелец, вид, не принимаемый этой поверхностью, — ОДНА И ТА ЖЕ ошибка. Различимый исход есть ОРАКУЛ: по нему отличают «нет такого» от «есть, но не ваш», то есть ровно то, что скрытие и должно закрыть.

Различимость живёт ВНУТРЬ — в счётчиках и журнале по причинам, а не в тексте ошибки и не в ответе на проводе. Причина едет внутрь отказом-обёрткой (RefuseBasicCredential), чей текст дословно равен этому и для которого `errors.Is` с этим значением истинно. Отдельным исходом остаётся только НЕДОСТУПНОСТЬ АВТОРИТЕТА: это не отказ в удостоверении, а неспособность установить его состояние, и предлагать вызывающему переаутентифицироваться на неисправность, которую не исправит ни одно его удостоверение, значило бы вводить его в заблуждение.

View Source
var ErrEmpty = errors.New("required field is empty")

ErrEmpty — sentinel for empty required fields (used in NULL-validation before repo).

View Source
var ErrLoginVerifierNotSerializable = errors.New("login verifier is not serializable")

ErrLoginVerifierNotSerializable — материал не сериализуется. Отказ ГРОМКИЙ: молча выданное пустое читалось бы как «материала нет», то есть было бы ложью о строке.

View Source
var ErrRecoveryCodeNotSerializable = errors.New("recovery code is not serializable")

ErrRecoveryCodeNotSerializable — код не сериализуется ни в JSON, ни в текст: в письмо он уходит одним путём — `Letter` из писателя очереди.

View Source
var ErrScopeMismatch = errors.New("scope does not match resource_type / resource_id")

ErrScopeMismatch — Scope does not match (resource_type, resource_id). Service-layer maps to gRPC InvalidArgument.

View Source
var ErrSessionBearerNotSerializable = fmt.Errorf("session bearer is not serializable")

ErrSessionBearerNotSerializable — носитель не сериализуется ни в JSON, ни в текст: значение уходит клиенту одним путём — заголовком `Set-Cookie`, который пишет обработчик полосы, — и попасть в журнал либо в ответ иным путём не может.

View Source
var ErrTrustedIssuerUnknown = errors.New(
	"federated assertion: (issuer, subject) does not resolve in the trusted-issuer list")

ErrTrustedIssuerUnknown — пара (издатель, субъект) не резолвится ни в одну запись перечня.

ОДИН признак на все состояния «мы за это не ручаемся»: записи нет вовсе · доверие выдано другому субъекту того же издателя · доверие выдано, но строка, которую оно уполномочивало, снята. Различимые исходы дали бы предъявителю оракул: по ним устанавливают, заведено ли доверие и кому именно.

View Source
var LoginMethodLookupSecret = LoginMethodKind(assurance.MethodLookupSecret.String())

LoginMethodLookupSecret — набор запасных кодов: строка несёт проверочный материал набора, из которого значение кода не восстановимо (Ф12 Р6).

View Source
var LoginMethodPassword = LoginMethodKind(assurance.MethodPassword.String())

LoginMethodPassword — пароль: строка несёт его проверочный материал.

View Source
var LoginMethodTOTP = LoginMethodKind(assurance.MethodTOTP.String())

LoginMethodTOTP — одноразовый код по времени (RFC 6238): строка несёт секрет ОБЁРНУТЫМ (Ф12 Р2) и последний принятый шаг.

Functions

func AccountScopeApplies

func AccountScopeApplies(k LimitKind) bool

AccountScopeApplies отвечает, применима ли область аккаунта к виду.

func AllMaterializableTypes

func AllMaterializableTypes() []string

AllMaterializableTypes returns the closed, sorted, deduped set of `<module>.<resource>` types the reconciler can materialize per-object. It is the wildcard-expansion set for a BOUNDED-scope `*.*` rule (issue kacho-iam#224 / D-8a): an owner role bound at ACCOUNT/PROJECT becomes an explicit per-object admin on EVERY object kind inside the scope, instead of relying on the FGA derivation cascade.

This is a STRICT SUPERSET of labelSelectableTypes, differing by exactly registry.repositories (materializable, not label-selectable). Every other materializable type (mirror-fed vpc/compute/loadbalancer + EVERY iam-native type) is also ARM_LABELS-selectable. The iam content types (user/serviceAccount/group/ role/accessBinding) remain materializable by ARM_ANCHOR/ARM_NAMES (replacing the flat model's missing `from account` cascade with per-object materialization) AND are additionally label-selectable.

Sorted so the resulting selector + role_rule_selectors index are deterministic (stable fast-path JOIN + migration-seed lockstep, rule_wildcard_scope_test.go).

func BindableScopes

func BindableScopes() []string

BindableScopes — копия закрытого набора областей выдачи, годных в предки. Копия, а не сам срез: набор закрыт, и вызывающий не вправе его пополнить.

func CeremonyFamilyBound added in v0.5.0

func CeremonyFamilyBound(born, sessionExpiresAt time.Time, familyTTL time.Duration) time.Time

CeremonyFamilyBound — граница семейства собственной церемонии: меньшее из срока сессии, в которой семейство выдано, и рождения семейства плюс срок семейства, названный установкой (`authn.ceremony.refresh-ttl`, kaname#318). Семейство кончается вместе со своим входом и не живёт дольше своего срока: оборот выпускает преемника, но границы не сдвигает.

Правило названо ЗДЕСЬ один раз: его спрашивают и выдача кода (рождение — момент выдачи), и сборка гранта из записи семейства (рождение — отметка строки). Сессия без срока (нулевой момент) границы не сужает. Потолок срока семейства судит страж старта; здесь срок — уже принятая величина.

func CeremonyScopes added in v0.5.0

func CeremonyScopes() []string

CeremonyScopes — ЗАКРЫТЫЙ перечень областей, которые интерактивный клиент вправе запросить у точки авторизации. Копией: вызывающий не расширит его на месте. Область вне перечня отвергается движком церемонии (`invalid_scope`) — принять её и ничего по ней не сделать было бы «принято и проигнорировано».

func ClassNestedRuleRefusal added in v0.5.0

func ClassNestedRuleRefusal(role RoleID, rules Rules, containers map[string]PublicReadContainer) (string, bool)

ClassNestedRuleRefusal — ворота выдачи СИСТЕМНОМУ КЛАССУ роли с вложенными правилами (Р5, ND-05). Судится КАЖДОЕ вложенное правило роли:

  1. контейнер объявлен (`publicReadToChildren`), иначе — текст SC-65 соседа;
  2. тип вложенного правила — ПРЯМОЙ ребёнок контейнера, иначе — до внука `public_reader` не доходит;
  3. правило даёт ровно `get` и `list` и не сужено.

Первое невыполненное условие — отказ с его текстом. Роль без вложенных воротам безразлична. `containers` — что живой каталог говорит о типах- владельцах; тип, о котором не сказано, читается необъявленным.

func CustomDefinitionTierToScope

func CustomDefinitionTierToScope(tierType, tierID string) (account, project string, ok bool)

CustomDefinitionTierToScope maps a wire definition_tier (dotted tierType + anchor id) to the account/project scope of a CUSTOM role (redesign-2026 F4). Pre-Phase-0 tierType is REQUIRED (prefix-derivation is B3-gated, so an empty tierType is rejected). iam.cluster is rejected — system roles are seeded by migration, never created via the public API. ok=false for an empty / iam.cluster / unknown tierType (the caller turns it into INVALID_ARGUMENT "Illegal argument definitionTier").

func CutoffBelowFirstAuthentication added in v0.5.0

func CutoffBelowFirstAuthentication(first time.Time) time.Time

CutoffBelowFirstAuthentication — момент отсечки, которой глагол снимает носители, выданные ДО первой аутентификации личности нашей посадкой, и не задевает ни одной сессии нашей посадки: на единицу разрешения хранилища (микросекунда) раньше первой аутентификации (Ф3 Р4, Р5, Ф1 §4.2). Край сравнивает отсечку с моментом сессии включающе, поэтому равенство с первой аутентификацией сняло бы самую раннюю сессию. Правило одно для выхода, смены пароля и снятия ключа доступа (Ф13 Р8): второе написание разошлось бы молча.

func DerivedIDSuffix

func DerivedIDSuffix(seed string) string

DerivedIDSuffix возвращает первые семнадцать шестнадцатеричных символов `md5(seed)` — дословно то же, что постгресовое `substr(md5(seed), 1, 17)`.

Все шестнадцатеричные символы годны как крокфордова база-32 (`0-9a-f` ⊂ `[0-9a-hjkmnp-tv-z]`), поэтому производные идентификаторы проходят ограничения формы `soc_`/`cag_`.

Ничего секретного здесь нет: вход — посевные строки, выход — публичные идентификаторы. Дайджест зафиксирован уже применённой миграцией, поэтому иной дайджест был бы не «крепче», а просто перестал бы адресовать существующие строки.

func EncodeRules

func EncodeRules(rs Rules) ([]byte, error)

EncodeRules encodes domain Rules to the roles.rules JSONB payload shape.

func FGASubjectRef

func FGASubjectRef(subjectType, subjectID string) string

FGASubjectRef formats the FGA "user" side of a tuple for a subject:

user            → user:<id>
service_account → service_account:<id>
group           → group:<id>#member  (computed relation expands group members)
system_class    → system_class:<anonymous|authenticated>

Empty / unknown subject_type defaults to user (defensive). SINGLE source of truth shared by the AccessBinding grant-tuple builder (tuples.go / scope_grant_tuples.go) and the ARM_LABELS reconciler (reconcile) — the two MUST stay byte-symmetric, else a member's grant and revoke would target different FGA users and leak standing access.

func HasSystemClass added in v0.5.0

func HasSystemClass(subjects []Subject) bool

HasSystemClass — среди субъектов выдачи есть системный класс.

func HumanSessionEndReasons added in v0.5.0

func HumanSessionEndReasons() []string

HumanSessionEndReasons — перечень ЗАКРЫТОГО словаря причин снятия записи сессии: ровно те значения, что принимает `human_sessions_ended_reason_check`. Причины отсечки RevokeReasonSecondFactorReset и RevokeReasonAccessKeysReset в него не входят — они не снимают ни одной записи.

Функция, а не переменная: каждый вызов отдаёт СВЕЖИЙ срез, и вызывающий, дописавший в него, словаря не меняет. Элементы — константы по имени, а не литералы: слово объявлено один раз. Совпадение перечня с ограничением базы в обе стороны держит проба KN-SER-07 (`internal/migrations/human_session_forced_exit_reason_integration_test.go`).

func IsAssertionClientUnknown

func IsAssertionClientUnknown(err error) bool

IsAssertionClientUnknown отвечает, не резолвится ли клиент.

func IsAssertionReplayed

func IsAssertionReplayed(err error) bool

IsAssertionReplayed отвечает, отвергнуто ли предъявление как ПОВТОР.

Предикат, а не сравнение на месте: вызывающие обёртывают ошибку контекстом, и сравнение `err == ErrAssertionReplayed` перестало бы работать на первом же обёртывании — молча, потому что «не повтор» и «не смогли распознать» выглядят одинаково.

func IsClassReadVerb added in v0.5.0

func IsClassReadVerb(verb string) bool

IsClassReadVerb — глагол чтения, допустимый у выдачи классу (Р4, SC-23).

func IsClassSubjectWord added in v0.6.0

func IsClassSubjectWord(word string) bool

IsClassSubjectWord — слово состава субъектов называет системный класс: `system_class`, `system_class:<класс>` либо прежнее слово LegacyPublicWildcardWord, которое рендер переводит в класс.

Единственное написание правила (kaname#866, CE-03): его читают загрузчик манифеста (Р2 — класс в действии чтения ресурса с ключом) и допуск собранной модели (Д3 — запись класса в блоке нового типа). Узнаётся по ВИДУ слова, а не по имени класса: имена — закрытый словарь, и запись с незнакомым именем обязана дойти до отказа, а не проскочить мимо него.

func IsIdentityCarriedKind added in v0.3.0

func IsIdentityCarriedKind(k LimitKind) bool

IsIdentityCarriedKind — вид считается в личности.

func IsLabelSelectableType

func IsLabelSelectableType(objectType string) bool

IsLabelSelectableType reports whether a `<module>.<resource>` type may carry a match_labels (ARM_LABELS) selector (feed-gate). Unified model: every iam-native type is label-selectable (project/account + the content types user/serviceAccount/group/role/accessBinding). It consults ONLY labelSelectableTypes — registry.repositories is materializable but NOT label-selectable, so it stays out of this predicate.

func IsPersonalAccountName added in v0.5.0

func IsPersonalAccountName(name AccountName) bool

IsPersonalAccountName — имя принадлежит зарезервированному пространству личных аккаунтов. Дефис на границе несущий: `personal-cloud-abc` зарезервировано, а `personal-cloudabc` (без дефиса) — обычное имя ВНЕ резерва.

func IsPostureStatedKind

func IsPostureStatedKind(k LimitKind) bool

IsPostureStatedKind — величину этого вида объявляет посадка, а не авторитет.

func IsPublicReadVerb added in v0.6.0

func IsPublicReadVerb(verb string) bool

IsPublicReadVerb — действие с этим именем касается публичное чтение ([publicReadVerbsOfChild]). Имя сравнивается в нормальной форме (NormalizeVerb).

func IsRetiredType

func IsRetiredType(objectType string) bool

IsRetiredType reports whether a dotted `<module>.<resource>` names a resource the platform has retired. A retired type is not grantable on any rule arm.

func IsRoleAssignable

func IsRoleAssignable(r Role, resourceType, resourceID string) bool

IsRoleAssignable reports whether role r may be bound on resource (resourceType, resourceID) per the STRICT assignability matrix.

func IsRoleAssignableInAccount

func IsRoleAssignableInAccount(r Role, resourceType, resourceID, scopeOwningAccountID string) bool

IsRoleAssignableInAccount extends IsRoleAssignable with the hierarchy-down rule that needs the scope's RESOLVED owning-account — knowledge the stateless predicate does not have (acceptance IAM-1-25). scopeOwningAccountID is the account that OWNS the scope anchor: for an account scope it is the account id itself; for a project scope it is the project's account_id (resolved by the caller via a project→account lookup); for cluster / cross-service scopes it is "".

It admits the strict matrix (IsRoleAssignable) PLUS the single hierarchy-down case: an iam.account-tier custom role is assignable on a PROJECT nested in the role's own account (role.account_id == the project's owning account). The account boundary is never crossed — an account-role of a DIFFERENT account stays not-assignable — and no other tier gains breadth (system stays everywhere, project-role stays own-project). scopeOwningAccountID=="" (unresolved / non-account scope) collapses to the strict predicate, so a missing resolve never over-grants (fail-closed).

func IsScopeAnchorKind

func IsScopeAnchorKind(bare string) bool

IsScopeAnchorKind reports whether a bare within-service kind may anchor an AccessBinding. Only the three hierarchy tiers can; a per-object type names an object UNDER the anchor and belongs to the `target` axis (F8), whose vocabulary is the live platform catalog (`ValidTargetType`, kaname#904).

func IsSeededResourceID

func IsSeededResourceID(id string) bool

IsSeededResourceID отвечает, назван ли литерал закрытым перечнем посеянного.

Проверку ПРЕФИКСА он на себя НЕ берёт: её делает вызывающий (`shared.ValidateResourceID`), поэтому посеянный id роли не проходит там, где ждут аккаунт.

func IsStoredPublication added in v0.5.0

func IsStoredPublication(subject, relation string) bool

IsStoredPublication — хранимый факт есть публикация для чтения без входа. Узнаётся по идентичности хранимой пары, а не по входному слову: после переезда переходное слово (`user:*`) в фактах не встречается, а слово-класс совпадает с хранимым лишь написанием, не происхождением.

func IsSystemClassName added in v0.5.0

func IsSystemClassName(id SubjectID) bool

IsSystemClassName — имя из закрытого словаря классов.

func IsTrustedIssuerUnknown

func IsTrustedIssuerUnknown(err error) bool

IsTrustedIssuerUnknown отвечает, не резолвится ли пара.

Предикат, а не сравнение на месте: вызывающие обёртывают ошибку контекстом, и сравнение на равенство перестало бы работать на первом же обёртывании — молча, потому что «не доверенный» и «не смогли распознать» выглядят одинаково, а второе означало бы недоступность реестра, у которой исход СВОЙ.

func IsVerbOfType

func IsVerbOfType(verb string, typeVerbs []string) bool

IsVerbOfType сообщает, объявляет ли ТИП этот глагол, то есть материализуется ли он как отношение `v_<глагол>` НА ЭТОМ типе.

Заменила снятый глобальный словарь глаголов на путях эмиссии. Разница не косметическая: глобальный словарь отвечал одинаково для всех типов, поэтому «у этого типа такого глагола нет» было невыразимо — и правило, называющее соседний глагол, порождало кортеж с отношением, которого у типа не существует. Пустой набор не принадлежит никому (fail-closed): тип, не объявивший ничего, не получает ни одного `v_*`.

func IsWellFormedModuleName

func IsWellFormedModuleName(module string) bool

IsWellFormedModuleName — годно ли имя по ФОРМЕ, безотносительно членства.

Читателей у грамматики ДВА, и они судят разные поля: сегмент `module` правила роли (validateModule) и оболочку манифеста (`module:`). Вторая копия образца разошлась бы с первой молча — на том имени, о котором знает только одна.

Подстановочный знак `*` формой имени НЕ является: он маркер политики, и разбирается Rule.Validate отдельно.

func IsWellFormedObjectTypeName

func IsWellFormedObjectTypeName(objectType string) bool

IsWellFormedObjectTypeName — годно ли имя типа объекта по ФОРМЕ, безотносительно того, объявлял ли его кто-нибудь.

func ModuleNameGrammar

func ModuleNameGrammar() string

ModuleNameGrammar — образец имени модуля СТРОКОЙ, для текстов отказа.

Отказ обязан назвать не только негодный токен, но и правило: без правила автор манифеста узнаёт, что ошибся, и не узнаёт, чем именно. Величина берётся у того же объявления, что судит, — выписанная рядом, она разошлась бы с ним молча.

func ModuleOfCatalogType added in v0.7.0

func ModuleOfCatalogType(dotted string) string

ModuleOfCatalogType — модуль точечного типа каталога по его имени (`<модуль>.<ресурс>`, например `registry.registries` → `registry`). Пусто, если имя без точки.

func NestedRuleBeyondDirectChildText added in v0.5.0

func NestedRuleBeyondDirectChildText(containerType string, role RoleID, nestedType string) string

NestedRuleBeyondDirectChildText — вложенное правило называет не ПРЯМОГО ребёнка контейнера: `public_reader from <указатель>` до внука не доходит.

func NestedRuleNotWholeText added in v0.5.0

func NestedRuleNotWholeText(role RoleID, childType string) string

Тексты отказа ворот класса (Р5, ND-05) — контракт тона, дословно приёмочные.

NestedRuleNotWholeText — вложенное правило не читает тип-ребёнок целиком: даёт не ровно `get` и `list` либо сужено именами или метками.

func NewKac127ID

func NewKac127ID(prefix string) string

NewKac127ID возвращает идентификатор формата `<prefix>_<17-char crockford>`. Panic если prefix пустой (programmer error: префикс приходит из package-level константы).

func NormalizeVerb

func NormalizeVerb(v string) string

NormalizeVerb — ЕДИНСТВЕННАЯ точка приведения имени глагола к канонической форме.

Разрыв, который она закрывает, был двусторонним. Проверка принадлежности приводила ВХОД, а индекс словаря строился ДОСЛОВНО — словарная запись с заглавной буквой не нашлась бы никогда. И наоборот: имя отношения собиралось из АВТОРСКОГО написания, поэтому написание, отличающееся регистром, проходило проверку и адресовало отношение, которого в модели нет; владелец модели отвергает такую запись окончательно, а отказ считается постоянным — строка навсегда блокирует свою партицию очереди.

Приведение ТОЖДЕСТВЕННО на всех существующих глаголах, поэтому ни одно отношение, ни один кортеж и ни одна запись каталога не меняются.

func ObjectTypeNameGrammar

func ObjectTypeNameGrammar() string

ObjectTypeNameGrammar — образец имени типа СТРОКОЙ, для текстов отказа и для сверки с ограничением базы.

Отказ обязан назвать не только негодный токен, но и правило: без правила автор манифеста узнаёт, что ошибся, и не узнаёт, чем именно. Величина берётся у того же объявления, что судит, — выписанная рядом, она разошлась бы с ним молча.

func OrderVerbsForDisplay

func OrderVerbsForDisplay(verbs []string) []string

OrderVerbsForDisplay возвращает глаголы в КАНОНИЧЕСКОМ порядке показа: сперва старшинство (get/list/create/update/delete), затем всё остальное — стабильно, по алфавиту.

Точка порядка ОДНА на всё превью и на публичное поле каталога. Порядок этих поверхностей — часть контракта: его читают существующие клиенты. Пока он жил внутри превью, поле каталога брало порядок из своего источника — и смена источника на пересечение наборов молча переставила бы значения по алфавиту.

func PasswordHashFormatMarkers added in v0.5.0

func PasswordHashFormatMarkers() []string

PasswordHashFormatMarkers — признаки перечня, отсортированные: перепись и тексты отказов печатают их в устойчивом порядке.

func RefuseBasicCredential added in v0.5.0

func RefuseBasicCredential(reason BasicCredentialRefusalReason) error

RefuseBasicCredential — единый отказ полосы с названной причиной.

func ResolveVerbsAndTier

func ResolveVerbsAndTier(authored, typeVerbs []string) (verbs []string, tier string)

ResolveVerbsAndTier expands a rule's authored verbs (verb `*` → набор ТИПА) and derives the per-RULE back-compat tier (strongest verb-class among the rule's verbs), mapped the SAME way the consumer authz-gate resolves an action: get/list → viewer ; create/update (+ domain mutations) → editor ; delete → admin. Per-RULE, never whole-role (B-11). The tier tuple keeps tier-based Check call-sites working; the v_* tuples carry the precise per-verb enforcement. typeVerbs — набор глаголов, объявленный ТИПОМ, на который правило адресовано. Он приходит ПАРАМЕТРОМ от вызывающего, который тип уже знает: владельцем таблицы остаётся authzmap, а домен — без внешних зависимостей (см. объявление файла). Вызывающий, у которого типа нет (правило не резолвится ни в один известный), передаёт словарь, общий для всех ресурсов, — это его решение, не домена.

func RetiredTypes

func RetiredTypes() []string

RetiredTypes returns the closed, sorted set of retired dotted types. Exported so the gates that keep the retirement complete can be driven from ONE list rather than from a copy of it.

func RoleBoundToSystemClassText added in v0.5.0

func RoleBoundToSystemClassText(id RoleID) string

RoleBoundToSystemClassText — текст отказа правке роли, выданной классу (SC-22). Один производитель на синхронный отказ и на отказ записи.

func RoleDeclaringModule added in v0.7.0

func RoleDeclaringModule(r Role) string

RoleDeclaringModule — модуль, манифест которого объявил роль; пусто — роль не модульная (платформенная, своя роль аккаунта либо проекта). Это и есть «роль модуля» Д2 приёмки.

Владельца несёт колонка `OwnerModule`, её проставляет применитель манифеста. У ролей, посеянных до её заведения, колонка пуста, и владение читается ТЕМ ЖЕ признаком, что у снятия ролей модуля (`RetireRole`): первый сегмент имени кластерной роли. Признак принимается лишь тогда, когда каждое правило роли называет этот же модуль: платформенные `admin`, `edit`, `view`, `owner` точки в имени не несут, а у `system.*` модуль правил с сегментом имени не совпадает.

func RoleNotReadOnlyForClassText added in v0.5.0

func RoleNotReadOnlyForClassText(id RoleID) string

RoleNotReadOnlyForClassText — текст отказа выдаче классу роли с пишущим глаголом (SC-21). Один производитель на синхронный отказ и на отказ записи.

func RuleRefsByRule

func RuleRefsByRule(rules Rules) [][]RoleRuleRef

RuleRefsByRule — объявленные сегменты, разложенные ПО ПРАВИЛАМ.

Длина результата равна длине входа ВСЕГДА: правило без адресуемых сегментов даёт пустой срез, а не пропускается. Иначе индекс перестал бы указывать на своё правило — то есть ключ RuleState.RuleIndex стал бы ложным.

Чем это отличается от [RuleRefsOf], и различие несущее

RuleRefsOf дедуплицирует сегменты ПО ВСЕЙ РОЛИ: два правила, объявившие один сегмент, дают одну строку — и это верно для вопроса «сколько адресуемых сегментов у роли». Здесь наоборот: дедупликация только ВНУТРИ правила, потому что сегмент, потерянный обоими правилами, потерян у ОБОИХ, и схлопнув их, мы объявили бы одно из них действующим.

func ScopeTierByKind

func ScopeTierByKind() map[string]Scope

ScopeTierByKind — копия словаря «вид → ярус».

Экспорт заведён ради ОДНОГО вызывающего — пробы согласия с триггером схемы (см. §«Третье место» выше). Второе объявление отношения живёт в другом пакете, поэтому сверить его иначе нечем: без этого читателя согласие двух объявлений было бы обещанием, а не проверкой.

Отдаётся копия: словарь неизменяем by construction, и вызывающий не должен иметь возможности это нарушить.

func ScopeTypeFromDotted

func ScopeTypeFromDotted(dotted string) (bare string, ok bool)

ScopeTypeFromDotted maps the dotted wire scopeType to the bare within-service anchor kind. ok=false for any value outside the closed three-tier set (empty, non-dotted bare, or unknown dotted) — the caller rejects it with InvalidArgument.

func ScopeTypeToDotted

func ScopeTypeToDotted(bare string) string

ScopeTypeToDotted maps the bare within-service anchor kind to the dotted wire scopeType. An unrecognized kind is returned unchanged (defensive — a binding anchor is always one of the three tiers).

func SeedIdentityDeclared added in v0.3.0

func SeedIdentityDeclared(name string) string

SeedIdentityDeclared приводит написание к ОБЪЯВЛЕННОМУ: имя из окна отдаётся своей парой, имя вне окна — собой.

Нужен там, где имя есть КЛЮЧ ДИСПЕТЧЕРИЗАЦИИ, а не значение запроса: разбор ветвится по одному написанию, и приводить к нему обязан один помощник, а не каждая ветвь по-своему.

func SeedIdentitySpellings added in v0.3.0

func SeedIdentitySpellings(name string) []string

SeedIdentitySpellings — ВСЕ написания, которыми сегодня адресуется тот же объект: само присланное имя плюс его пара по окну, если она есть.

Отношение симметрично: присланное прежнее написание резолвится в объявленное и наоборот. Асимметричное окно приняло бы манифест чужого продукта и отвергло свой собственный, переведённый, — а перевод идёт своим порядком (П3).

Имя вне окна возвращается ОДНИМ элементом, а не пустым срезом: окно расширяет приём ровно на объявленные пары и ничего не решает про остальные имена.

func SeededResourceIDs

func SeededResourceIDs() []string

SeededResourceIDs возвращает перечень для гейта предмета. Копия — чтобы вызывающий не мог расширить приём, правя чужую карту.

func SpeakerClasses added in v0.5.0

func SpeakerClasses(speaker string) []string

SpeakerClasses — классы, которые получает вопрос от имени `speaker`, СВЕРХ самого говорящего (Р2).

Правило перечисляет виды, которым классы положены, а ветви «все прочие» у него нет:

  • человек и сервисная учётка (`user:<id>`, `service_account:<id>`) — вошедший: оба класса. Вход не отнимает прав, поэтому выданное анониму читает и вошедший;
  • `system_class:anonymous` — вызывающий без личности: классов сверх себя у него нет. Обратного вывода нет никогда: у анонима нет `authenticated` (SC-10);
  • группа (`group:<id>`, `group:<id>#member`), подстановка любого вида (`user:*`, SC-15) и всё прочее — ничего: группа говорящим-личностью не является (SC-11), подстановка говорящим не бывает вовсе. Вопрос о вызывающем без личности задаётся классом `system_class:anonymous`, а не подстановкой: плоскость данных реестра kacho переведена на класс (SC-39), и переходного отображения подстановки в аноним нет.

Безымянность — значение входа (`system_class:anonymous`), а не вывод из формы строки: пустой и нераспознанный говорящий отвергаются раньше, разбором SpeakerRefusal.

func SpeakerRefusal added in v0.5.0

func SpeakerRefusal(speaker string) error

SpeakerRefusal — отказ говорящему, который говорящим не бывает (Р3).

Один разборщик на все методы, принимающие говорящего (`Check`, `BatchCheck`, `CheckRelation`): отказ в одном из них оставил бы соседний спрашивающим от имени «вошедшего без личности».

  • `system_class:authenticated` — «вошедший» без названной личности есть противоречие (SC-14); любое имя класса, кроме `anonymous`, — тем же отказом;
  • подстановка любого вида, `user:*` в том числе, — не говорящий (SC-15): она называет множество, а не того, кто спрашивает.

nil — говорящий допустим.

func UndeclaredContainerText added in v0.5.0

func UndeclaredContainerText(targetType string, role RoleID, childType string) string

UndeclaredContainerText — текст отказа выдачи классу на объект типа, не объявленного контейнером, ролью, правило которой покрывает его тип-ребёнок (SC-65). Контракт тона — дословно приёмочный.

func ValidKeyIDForm

func ValidKeyIDForm(raw string) bool

ValidKeyIDForm — форма идентификатора ключа как предикат для приёмной стороны. Отдельная функция, а не экспортированная регулярка: регулярку вызывающий скопировал бы, и две копии разошлись бы молча.

func ValidTargetType

func ValidTargetType(cat TargetTypeCatalog, dotted string) bool

ValidTargetType сообщает, может ли точечный `<module>.<resource>` быть назван типом пообъектной цели выдачи: тогда и только тогда, когда у типа есть ЖИВАЯ строка каталога платформы (Р1 приёмки `access-target-names-live-catalog-types.md`, kaname#904).

Почему источник — каталог. Цель чтится в одном месте: реконсайлер сравнивает её с типом объекта из зеркала точным совпадением строк (`AccessTarget.Contains`), а зеркало принимает объект лишь живого типа (`resource_mirror/emitter.go`, отказ «is not a live entry of the platform resource catalog»). Множество типов, которые лента МОЖЕТ произвести, и есть живой каталог — включая типы, заведённые манифестами модулей. Закрытый перечень в коде (`materializableTypes`) отвечал на этот вопрос второй, отдельно сопровождаемой копией и ошибался в обе стороны: тип модуля, живой в каталоге, отвергался — и выдача на объект модуля выражалась только всей областью, то есть ШИРЕ запрошенного; а тип, которого лента не производит, принимался бы и не давал ничего.

Подстановочный `*`, не-точечное имя, другое написание и снятая строка дают false: ни у одного из них живой строки нет. Пустой каталог (nil) — false на всё: отсутствие источника не превращается во всеразрешение.

func ValidateAssertionID

func ValidateAssertionID(raw string) error

ValidateAssertionID проверяет форму идентификатора однократности ДО того, как он попадёт в хранилище.

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

func ValidateCeremonyDigest added in v0.5.0

func ValidateCeremonyDigest(field, digest string) error

ValidateCeremonyDigest — форма свёртки кода либо токена.

func ValidateCeremonyLevel added in v0.5.0

func ValidateCeremonyLevel(level string) error

ValidateCeremonyLevel — уровень аутентификации гранта из той же закрытой оси, что у сессии (`assuranceLevelValues`, Ф11): семейство несёт СНИМОК уровня сессии на выдаче кода (миграция `20260925121413`), и второго словаря у этого предмета нет.

func ValidatePKCEChallenge added in v0.5.0

func ValidatePKCEChallenge(challenge, method string) error

ValidatePKCEChallenge — форма испытания и метода.

func ValidateRedirectURIs

func ValidateRedirectURIs(field string, uris []string, required bool) error

ValidateRedirectURIs — the redirect rule, exported because both the Create and the Update path must apply exactly the same one (api-conventions: a mutable field is validated on Update by the same rules as on Create).

WHY https IS NOT COSMETIC. A redirect target is where the authorization code — a credential — is delivered. A plaintext target hands it to anyone on the path; a relative or opaque one lets the provider's own matching rules decide where it lands. Both are rejected here rather than left to the provider, because the provider's answer arrives too late to be a contract: the caller would see a peer error instead of a named field.

Types

type AccessBinding

type AccessBinding struct {
	ID              AccessBindingID
	SubjectType     SubjectType
	SubjectID       SubjectID
	RoleID          RoleID
	ResourceType    ResourceType
	ResourceID      string              // opaque id (any prefix, cross-service OK)
	Scope           Scope               // RBAC v2 — anchor tier (CLUSTER/ACCOUNT/PROJECT)
	Status          AccessBindingStatus // PENDING|ACTIVE|REVOKED
	ExpiresAt       *time.Time          // nullable — TTL
	GrantedByUserID UserID              // audit
	RevokedAt       *time.Time          // nullable
	RevokedByUserID *UserID             // nullable
	CreatedAt       time.Time

	// MaterializedAt — OUTPUT-ONLY. When this binding's per-object access last
	// became live: MAX(updated_at) over its ACTIVE rows in
	// access_binding_target_members (the reconciler's ledger). Zero when nothing
	// has materialized yet.
	//
	// It exists because Kachō is eventually-consistent by contract: Operation.done
	// means the binding row is DURABLE, never that the FGA tuples are visible
	// (gating done on downstream visibility is forbidden — ban #9). In that window
	// the grantee's 403 is byte-identical to a real denial, leaving the granting
	// admin with nothing to distinguish "propagating" from "wrong grant" and
	// nothing to poll. This field is the observable — NOT a barrier: it is filled
	// by the read path from an already-written ledger and never blocks a write.
	//
	// Legitimately zero forever for a binding that materializes no per-object
	// membership (CLUSTER scope — served by the flat short-circuit; a legacy
	// permissions-only role with no authored rules[]). Zero therefore means "no
	// ACTIVE per-object member", NOT "the grant is broken".
	//
	// Not persisted on the access_bindings row; filled by the read-side batch
	// projection, like Subjects.
	MaterializedAt time.Time

	// DeletionProtection guards the binding from Delete (RBAC explicit-model 2026
	// P6 — D-10, by the image of vpc.address.deletion_protection). The owner
	// auto-binding created on Account.Create sets it true; Delete on a protected
	// binding → FAILED_PRECONDITION (sync pre-check + atomic CAS backstop). Cleared
	// via Update(update_mask=["deletion_protection"]) (C-03). Default false.
	DeletionProtection bool

	// GrantedRelation — ВТОРАЯ ФОРМА выдачи: имя отношения модели, выдаваемое
	// напрямую на области выдачи, взаимоисключающе с RoleID (ограничение БД
	// `access_bindings_grant_form_ck`). Роль раздаёт ГЛАГОЛЫ через свои правила, а
	// встроенные права платформы выражены именованными отношениями
	// (`system_viewer`, `fga_writer`, `viewer` на кластере) — подобрать роль под
	// каждое означало бы завести роли с пустыми правилами.
	//
	// Непусто ТОЛЬКО у системной выдачи: отношение — внутреннее имя, от которого
	// зависит решение о доступе, и выдавать его вправе только платформа. На вход
	// создания это поле не принимается вовсе — его нет в запросе, поэтому «принято
	// и проигнорировано» здесь невозможно by construction.
	GrantedRelation string

	// System — выдача заведена платформой (встроенный доступ: права служебных
	// учёток, публичное чтение справочников). OUTPUT-ONLY на публичном контракте.
	//
	// Отвечает на вопрос «кто выдал» точнее, чем подставленная учётка: выдал не
	// человек, а платформа, поэтому GrantedByUserID у такой выдачи пуст, и это не
	// пробел аудита, а его содержание.
	System bool

	// Subjects — the full multi-subject set (RBAC rules-model 2026).
	// Persisted in the access_binding_subjects child table; SubjectType/
	// SubjectID above remain the legacy single = Subjects[0] (projection +
	// the active-grant UNIQUE anchor). Empty on a row read without the child
	// load — read-side Get/List fills it. On Create the use-case
	// NormalizeSubjects-resolves it from the request before persisting.
	Subjects []Subject

	// Labels — tenant-facing метки САМОГО ресурса AccessBinding. Делают
	// AccessBinding label-selectable наравне с account/project (ARM_LABELS-грант
	// на iam.accessBinding → v_list по `labels @> matchLabels`; List фильтрует
	// viewer ∪ v_list).
	Labels Labels

	// Target — WHICH objects under the scope-anchor the grant applies to
	// (redesign-2026 F8). AllInScope = whole anchor (incl. future); Resources =
	// closed per-object set. The zero value is treated as AllInScope (legacy /
	// internal rows); the public Create RPC rejects a missing target (least-priv).
	Target AccessTarget

	// RevocationReason — почему выдача отозвана (kaname#963, Д2). Пусто у
	// действующей выдачи и у отозванной до заведения поля (Д7).
	RevocationReason RevocationReason

	// DelegationID — делегирование права выдавать, по которому выдачу сделал
	// делегат (kaname#986, Р2). Пусто у выдачи держателя права выдавать. Ставит
	// только делегированный путь права выдавать; на вход запроса поле не
	// принимается.
	DelegationID GrantDelegationID

	// HeldTargets — НЕ ХРАНИТСЯ. Объекты цели, которые на приёме выдачи были
	// зарегистрированы (Р8 приёмки `unregistration-revokes-its-grants.md`). Запись
	// выдачи берёт на каждом замок строки регистрации и отказывает, если строки
	// уже нет: выдача, принятая до снятия и записанная после, живой не остаётся
	// (URG-24). Пусто — судить нечего (ключи свободны либо цель — вся область).
	HeldTargets []ResourceRef
}

AccessBinding — link (subject_type, subject_id) ↔ role_id ↔ (resource_type, resource_id) with lifecycle fields (status, expires_at, granted_by, revoked_at/revoked_by).

State machine: PENDING → ACTIVE → REVOKED (terminal). Transitions are atomic CAS-style UPDATEs (no TOCTOU). REVOKED is irreversible via `WHERE status IN ('PENDING','ACTIVE')`.

DB partial UNIQUE access_bindings_active_grant_uniq (migration 0003; WHERE revoked_at IS NULL) → strict INSERT: a duplicate active grant raises ErrAlreadyExists with verbatim text «these permissions are already granted to <subject_id> on <res_type>:<res_id>». Re-grant after revoke is allowed (revoked rows are out of the partial UNIQUE scope).

func (AccessBinding) StructuralParent

func (b AccessBinding) StructuralParent() (StructuralTuple, bool)

StructuralParent returns the binding's scope parent-pointer:

project:<resourceID>  →  project  →  iam_access_binding:<id>
account:<resourceID>  →  account  →  iam_access_binding:<id>
cluster:<resourceID>  →  cluster  →  iam_access_binding:<id>

This triple is what `iam_access_binding.super_admin: super_admin from project or admin from account or any_admin from cluster` resolves over — the sole enabler of the cascade on a binding. ok=false when the scope is not a hierarchy parent, the binding has no id, or the scope has no id.

The empty-scope-id case is unreachable through the API — validateScopeID pins the cluster tier to the singleton id and requires a non-empty account/project id — and the guard is here for what it prevents rather than for what it expects: without it such a row projects `cluster:` with an empty object id, which the store rejects, and a rejected write in the outbox is retried rather than dropped, so one malformed row would hold up its partition indefinitely.

It is deliberately INDEPENDENT of status. A revoked binding keeps its row, and it is still a record that has to be readable and deletable by the administrator of its account; a status filter here would take that away precisely when the queue is behind. It cannot bring the grant back: what the grantee held were tuples on the SCOPE object, and this is a pointer at the binding, not a grant on the scope. Locked by service/cascade_queue_independence_integration_test.go (TestRevokedBindingStaysManageableAndGrantsNothing).

func (AccessBinding) Validate

func (b AccessBinding) Validate() error

type AccessBindingID

type AccessBindingID string

type AccessBindingStatus

type AccessBindingStatus string

AccessBindingStatus — enum lifecycle (migration 0011 CHECK).

const (
	AccessBindingStatusPending AccessBindingStatus = "PENDING"
	AccessBindingStatusActive  AccessBindingStatus = "ACTIVE"
	AccessBindingStatusRevoked AccessBindingStatus = "REVOKED"
)

func (AccessBindingStatus) Validate

func (s AccessBindingStatus) Validate() error

type AccessKey added in v0.5.0

type AccessKey struct {
	ID     AccessKeyID
	UserID UserID
	// CredentialID — идентификатор удостоверения WebAuthn: байты нормы,
	// глобально уникальны; наружу не выходят (Р10).
	CredentialID []byte
	// PublicKey — открытый ключ в форме COSE_Key, байт в байт как принят.
	PublicKey []byte
	// Algorithm — идентификатор алгоритма COSE.
	Algorithm int64
	// SignCount — сохранённое значение счётчика подписи (Р6).
	SignCount uint32
	// UserHandle — рукоятка `user.id`, которую церемония регистрации положила
	// в ЭТОТ ключ: байты `CeremonyHandle` человека (64 случайных байта, Ф13
	// Р3). С ней сверяется каждое утверждение ключа (Ф13-06 «з»).
	UserHandle  []byte
	Name        AccessKeyName
	Description AccessKeyDescription
	CreatedAt   time.Time
	// LastUsedAt — момент последнего успешного предъявления; нулевой указатель
	// — предъявлений не было.
	LastUsedAt *time.Time
	// Discoverability — признак обнаружимости удостоверения (Р12): сообщается
	// аутентификатором ОДИН раз, в церемонии регистрации, и после неё его
	// негде прочитать заново. Читатель — страж последнего способа входа.
	Discoverability Discoverability
}

AccessKey — строка ключа.

func (AccessKey) Presentable added in v0.5.0

func (k AccessKey) Presentable() bool

Presentable — годен ли ключ к предъявлению на полосе входа без пароля: тогда и только тогда, когда его обнаружимость ПОДТВЕРЖДЕНА. «Не сообщено» и «подтверждённо необнаружим» не годны — правило консервативно (Р12).

func (AccessKey) Validate added in v0.5.0

func (k AccessKey) Validate() error

Validate — самопроверка записываемой строки (self-validating domain).

type AccessKeyChallenge added in v0.5.0

type AccessKeyChallenge struct {
	// Challenge — случайные байты; ключ строки.
	Challenge []byte
	// UserID — кому выдано: из чужой сессии испытание не находится (Ф7-55).
	UserID  UserID
	Purpose AccessKeyChallengePurpose
	// IssuedAt / ExpiresAt — момент выдачи и предел срока.
	IssuedAt  time.Time
	ExpiresAt time.Time
	// ConsumedAt — момент предъявления; нулевой указатель — не предъявлено.
	ConsumedAt *time.Time
}

AccessKeyChallenge — выданное испытание: привязано к ВЫЗЫВАЮЩЕМУ (Р11), однократно (Ф7-03, Ф7-53) и срочно (Ф7-34, Ф7-54). Срок у обеих процедур — одна величина контракта (§7 инв. 5).

func (AccessKeyChallenge) StateAt added in v0.5.0

StateAt — состояние испытания на момент `now`.

func (AccessKeyChallenge) Validate added in v0.5.0

func (c AccessKeyChallenge) Validate() error

Validate — самопроверка записываемого испытания.

type AccessKeyChallengePurpose added in v0.5.0

type AccessKeyChallengePurpose string

AccessKeyChallengePurpose — процедура, для которой выдано испытание. Испытание регистрации утверждением не предъявить и наоборот: словарь закрыт ограничением схемы.

const (
	// ChallengeForRegistration — испытание церемонии регистрации (§7.1).
	ChallengeForRegistration AccessKeyChallengePurpose = "registration"
	// ChallengeForAssertion — испытание предъявления (§7.2).
	ChallengeForAssertion AccessKeyChallengePurpose = "assertion"
)

func AccessKeyChallengePurposes added in v0.5.0

func AccessKeyChallengePurposes() []AccessKeyChallengePurpose

AccessKeyChallengePurposes — закрытый перечень.

type AccessKeyChallengeState added in v0.5.0

type AccessKeyChallengeState string

AccessKeyChallengeState — состояние испытания регистрации на момент предъявления: три различимых состояния (Ф7-34), плюс «выдано».

const (
	// ChallengeIssued — выдано, не предъявлено, срок не вышел.
	ChallengeIssued AccessKeyChallengeState = "issued"
	// ChallengeUnknown — служба такого не выдавала (Ф7-02).
	ChallengeUnknown AccessKeyChallengeState = "unknown"
	// ChallengeConsumed — уже предъявлено (Ф7-03).
	ChallengeConsumed AccessKeyChallengeState = "consumed"
	// ChallengeExpired — срок вышел (Ф7-34).
	ChallengeExpired AccessKeyChallengeState = "expired"
)

type AccessKeyDescription added in v0.5.0

type AccessKeyDescription string

AccessKeyDescription — описание ключа: свободный текст с пределом в ЗНАКАХ.

Тип свой, а не `Description` соседей, потому что у них проверка входа считает БАЙТЫ (`len > 256`), а схема — знаки: на кириллической метке две формы расходятся — 200 знаков есть 400 байт. Здесь единица одна — знак, как у схемы и у фундаментного валидатора `corevalidate.Description` (Р10). Расхождение у соседей — их предмет, здесь оно только названо.

func (AccessKeyDescription) Validate added in v0.5.0

func (d AccessKeyDescription) Validate() error

Validate — предел в рунах.

type AccessKeyID added in v0.5.0

type AccessKeyID string

AccessKeyID — платформенный идентификатор ключа (Р10). Единственное, чем ключ адресуется извне.

func (AccessKeyID) Validate added in v0.5.0

func (id AccessKeyID) Validate() error

Validate — форма собственного идентификатора: то, что БУДЕТ ЗАПИСАНО.

type AccessKeyLoginChallenge added in v0.5.0

type AccessKeyLoginChallenge struct {
	// Challenge — случайные байты; ключ строки. Длина — та же, что у
	// испытаний церемоний (`AccessKeyChallengeBytes`).
	Challenge []byte
	// FormContext — контекст формы, под которым испытание выдано. Одно ЖИВОЕ
	// испытание на контекст держит частичная уникальность схемы, а не
	// проверка перед вставкой (ban #10).
	FormContext string
}

AccessKeyLoginChallenge — выдаваемое испытание полосы входа: привязано к контексту формы (Р2), однократно (Ф13-08) и срочно.

func (AccessKeyLoginChallenge) Validate added in v0.5.0

func (c AccessKeyLoginChallenge) Validate() error

Validate — самопроверка выдаваемого испытания.

type AccessKeyName added in v0.5.0

type AccessKeyName string

AccessKeyName — имя ключа одной формы дерева (DNS label RFC 1123). Форма — у фундамента; здесь тип для колонки, чтобы «что записывается» судилось тем же валидатором, что у всех именуемых ресурсов службы.

func (AccessKeyName) Validate added in v0.5.0

func (n AccessKeyName) Validate() error

Validate судит записываемое имя: пустое до записи не доживает (`corevalidate.NameOrDefault`), поэтому здесь оно негодно.

type AccessTarget

type AccessTarget struct {
	AllInScope bool          // whole-anchor grant (incl. future objects)
	Resources  []ResourceRef // per-object grant (mutually exclusive with AllInScope)
}

AccessTarget — выбор объектов выдачи (F8). Значима ровно одна ветвь; нулевое значение — выдача на весь якорь (смысл AllInScope).

func (AccessTarget) Contains

func (t AccessTarget) Contains(dottedType, id string) bool

Contains сообщает, перечисляет ли закрытый пообъектный перечень цели объект (dottedType, id). Это проверка членства наименьших привилегий: совпадение по role.rules материализует ТОЛЬКО объект, который цель тоже перечисляет. Цель AllInScope или пустая явных объектов здесь не перечисляет (её ширина — весь якорь, его материализует ветвь «вся область» реконсайлера, а не эта проверка), поэтому Contains для неё ложен; вызывающий судит IsEmpty()/AllInScope до неё.

func (AccessTarget) Digest

func (t AccessTarget) Digest() string

Digest returns a deterministic, set-based canonicalization of the target for the active-grant partial UNIQUE. AllInScope / empty → "all"; a resource set → a hash of its SORTED "type:id" members (order-independent — the same set in any order collides, IAM-1-29).

func (AccessTarget) DistinctTypes added in v0.6.0

func (t AccessTarget) DistinctTypes() []string

DistinctTypes — различные типы пообъектной цели в порядке первого появления. Им транзакция записи выдачи спрашивает каталог одним оператором.

func (AccessTarget) FirstNonLiveType added in v0.6.0

func (t AccessTarget) FirstNonLiveType(cat TargetTypeCatalog) (string, bool)

FirstNonLiveType — первый в порядке перечня тип пообъектной цели, у которого нет живой строки каталога; ok=false, когда таких нет (в том числе у цели «вся область», у которой типов нет вовсе). Отказ называет ПЕРВЫЙ неживой тип (Р3): перечень отвергается целиком, и вызывающему нужен один исправимый адрес.

func (AccessTarget) IsEmpty

func (t AccessTarget) IsEmpty() bool

IsEmpty сообщает, что не задана ни одна ветвь (явного выбора нет). Пустая цель хранится и сверяется как AllInScope, но публичный Create её отвергает (цель ОБЯЗАТЕЛЬНА).

func (AccessTarget) ResourceIDsForTypes

func (t AccessTarget) ResourceIDsForTypes(types []string) []string

ResourceIDsForTypes возвращает идентификаторы пообъектных элементов цели, чей точечный тип входит в `types` (порядок сохранён, повторы сняты). По ним реконсайлер находит кандидатов ARM_ANCHOR пообъектной цели по идентификатору (MatchByIDs), а не обходом всей области (MatchAllInScope) — путь материализации наименьших привилегий (IAM-1-21). Пусто, если ни один элемент не подходит по типу.

func (AccessTarget) Validate

func (t AccessTarget) Validate() error

Validate судит форму цели: ветви взаимоисключающие, у каждого пообъектного ResourceRef непустой тип, а идентификатор непуст и подчиняется грамматике объекта (`validObjectID`). AllInScope и пустая цель всегда годны.

Отказы копятся по всем элементам; у элемента с пустым идентификатором отказ один — «(empty)», проверка формы его не дублирует.

Членство типа в каталоге здесь НЕ судится: домен чист и каталога не видит. Годность типа — вопрос живого каталога на момент записи (`ValidTargetType`, `FirstNonLiveType`); его задаёт use-case создания выдачи синхронно по снимку и повторно в транзакции записи (kaname#904).

type Account

type Account struct {
	ID          AccountID
	Name        AccountName
	Description Description
	Labels      Labels
	OwnerUserID UserID
	CreatedAt   time.Time
}

Account — top-level tenant («организация» как товар продукта IAM). Замещает прежнюю связку tenant+folder из retired kacho-resource-manager. Уникальное имя глобально (DB UNIQUE accounts_name_unique).

FK: owner_user_id → users(id) ON DELETE RESTRICT. Удаление RESTRICT при наличии Project / ServiceAccount / Group / custom Role.

func (Account) StructuralFacts

func (a Account) StructuralFacts() []StructuralTuple

StructuralFacts returns the two structural facts the model reads on `account`:

cluster:<singleton>  →  cluster  →  account:<id>     (levels 1-2 reach it here)
user:<owner>         →  owner    →  account:<id>

The owner is a structural fact and not merely a materialized grant: an account is created by self-service, so tearing it down has to be as reliable as creating it, which is why `account`'s five verbs read `or owner` directly.

func (Account) Validate

func (a Account) Validate() error

Validate — multierr.Combine всех полей. owner_user_id-existence — это cross-row проверка, делается через FK на repo-уровне (`accounts_owner_fk`); здесь только проверка, что значение непустое.

type AccountID

type AccountID string

AccountID / ProjectID / ... — newtypes over string, never bare ids.

type AccountName

type AccountName string

Every newtype below has its own regex.

func (AccountName) Validate

func (n AccountName) Validate() error

type Arm

type Arm int

Arm classifies a rule's selector arm.

const (
	ArmAnchor Arm = iota
	ArmNames
	ArmLabels
)

type AssertionClient

type AssertionClient struct {
	// ID — НАШ идентификатор строки реестра. Им и только им клиент себя
	// называет: он же издатель и субъект утверждения.
	ID string
	// Kind — из какой таблицы реестра строка прочитана.
	Kind AssertionClientKind
	// OwnerID — принципал, от чьего имени клиент получает токен.
	OwnerID string
	// PublicKeyPEM — зарегистрированный открытый ключ.
	PublicKeyPEM string
	// Algorithm — ЗАРЕГИСТРИРОВАННЫЙ алгоритм. Пустое значение — законный вход
	// схемы, и означает оно «ключа нет», а НЕ «любой алгоритм».
	Algorithm string
	// ExpiresAt — момент истечения клиента в Unix-секундах; 0 означает
	// «бессрочно», и это законное состояние схемы (колонка допускает NULL).
	ExpiresAt int64
	// Отзыв ключа НЕ является здесь полем, и это не упущение: колонки отзыва в
	// схеме обеих таблиц НЕТ — отзыв выражен СНЯТИЕМ СТРОКИ. Завести поле
	// значило бы описать состояние, которого схема не допускает, и построить
	// на нём пробу, которую нечем поставить (F1 §14 п. 2). Отозванный клиент
	// не резолвится вовсе, и отказ наступает на том же шаге, что для
	// несуществующего, — то есть требование «отозванный токена не получает»
	// выполняется by construction, а не проверкой поля.
	// OwnerActive — владелец в состоянии ACTIVE. Не-ACTIVE — это ДВА значения
	// словаря (PENDING и BLOCKED), и оба обязаны давать один исход.
	OwnerActive bool
	// DeclaredAudiences — сужение адресатов, ОБЪЯВЛЕННОЕ заказчиком ключа при
	// его выдаче (`IssueSAKeyRequest.audience`, задача kacho#1136).
	//
	// ПУСТОЙ ПЕРЕЧЕНЬ ОЗНАЧАЕТ «СУЖЕНИЯ НЕ ОБЪЯВЛЕНО», А НЕ «ЛЮБОЙ АДРЕСАТ», и
	// это НЕ тот класс, что пустой список доверенных отправителей. Там пустое
	// значение снимало единственный контроль; здесь оно оставляет ВНЕШНЮЮ
	// границу — объявленный посадкой перечень адресатов, который страж старта
	// требует непустым и без которого выдача не строится вовсе. Сужение ключа
	// действует ВНУТРИ этой границы и никогда её не расширяет: адресат, которого
	// посадка не объявила, не выдаётся ни одному ключу, как бы он ни объявился.
	//
	// У клиента пользовательского токена перечня нет BY CONSTRUCTION: адресата
	// его токена проставляет сам сервис из своей настройки, и заказчик не вправе
	// назвать его вовсе (решение Р2, `interactive_client.proto`). Поэтому сужать
	// там нечем, и пустое значение на том пути — не умолчание, а отсутствие
	// предмета.
	DeclaredAudiences []string
}

AssertionClient — строка реестра, способная предъявить утверждение.

Зеркального значения (идентификатора клиента во внешнем сервере) здесь НЕТ, и это не упущение: оно не участвует в разрешении клиента ни как второй ключ поиска, ни как запасной (приёмка F2 §2.1). Субъект с двумя именами дал бы две записи журнала об одном действии и два ключа однократности на одно утверждение — то есть отменил бы однократность, сохранив её форму.

func (AssertionClient) CanPresentAssertion

func (c AssertionClient) CanPresentAssertion() bool

CanPresentAssertion отвечает, способна ли строка вообще участвовать в аутентификации утверждением.

Пустой зарегистрированный алгоритм и пустой ключ — «ключа нет». Это тот же класс, что пустой список доверенных отправителей: значение, которое может быть пустым и не проверено на непустоту, означает «не сужаем».

type AssertionClientKind

type AssertionClientKind string

AssertionClientKind — вид клиента, способного доказать владение ключом.

Словарь ЗАКРЫТ и содержит ровно те виды, у которых в схеме есть ключевой материал. Интерактивного клиента здесь нет НАМЕРЕННО: у него нет способа доказать владение ключом by construction, поэтому он не резолвится ни во что, и отказ наступает на том же шаге, что и для несуществующего клиента (приёмка F2 §2.9). Формулировка «нашли строку, ключ пуст» потребовала бы, чтобы кто-то поддерживал в третьей таблице инвариант «ключа быть не должно»; инвариант, который надо поддерживать, ломается, а выраженный отсутствием таблицы на пути — нет.

const (
	// AssertionClientUser — клиент, через который выдаётся токен пользователя.
	AssertionClientUser AssertionClientKind = "user"
	// AssertionClientServiceAccount — клиент ключа служебной учётки.
	AssertionClientServiceAccount AssertionClientKind = "service_account"
)

func AssertionClientKinds

func AssertionClientKinds() []AssertionClientKind

AssertionClientKinds возвращает закрытый словарь целиком.

type AssignableRole

type AssignableRole struct {
	RoleID      RoleID
	Name        RoleName
	Description Description
	IsSystem    bool
	ScopeGroup  RoleScopeGroup
	CreatedAt   time.Time
}

AssignableRole — the lean projection of a Role for the grant-form picker Carries only publicly-safe fields the UI needs to render + group the picker (id / resolved name / description / is_system / server-computed scope_group / created_at) — NO permissions array and no infra-sensitive fields. ScopeGroup is computed via ScopeGroupOf.

type AuditEventID

type AuditEventID string

AuditEventID — идентификатор записи журнала: `evt_` плюс 20…30 символов crockford-base32 (ограничение `audit_outbox_id_check`, миграция `0001_initial.sql`).

Тело СЛУЧАЙНО, а не производно от времени: производитель (`pg.newAuditEventID`) берёт 14 байт у источника случайности и печатает из них 22 символа. Порядок по идентификатору поэтому НЕ является порядком по времени; сортировать журнал надо по `created_at`. Прежняя редакция называла его ULID — то есть обещала ровно ту сортируемость, которой нет.

func (AuditEventID) Validate

func (id AuditEventID) Validate() error

type AuditOutboxEntry

type AuditOutboxEntry struct {
	ID              AuditEventID
	EventType       EventTypeName
	TenantAccountID *AccountID
	EventPayload    json.RawMessage
	Status          AuditOutboxStatus
	Attempts        int
	CreatedAt       time.Time
	NextAttemptAt   time.Time
}

AuditOutboxEntry — строка журнала аудита `kaname.audit_outbox` (заведён миграцией `0001_initial.sql`), дописываемая только вперёд.

Что происходит на самом деле

Строка ложится в ТУ ЖЕ транзакцию, что и мутация домена, и оттуда вывозится в приёмник журнала — поток структурных записей службы (`cmd/kaname/audit_shipper_wiring.go`, механизм — `pkg/audit`). Состояние строки после этого помечено доставленным, а состояние очереди целиком снимает периодический сканер (`cmd/kaname/outbox_metrics_wiring.go`).

Здесь было описано ОТСУТСТВИЕ доставки — у него больше нет предмета

Прежняя редакция объясняла, почему дренаж неконструируем: приёмника аудита не существовало ни одного, и из четырёх объявленных состояний достижимо было ровно одно. Оба утверждения были верны на день записи и перестали быть верными вместе с приёмником (kacho#812): состояний теперь ДВА и оба достижимы, потому что словарь сужен до того, что продукт производит.

Здесь стояло описание, неверное ЧЕТЫРЕЖДЫ

Прежняя редакция объявляла: дренаж отправляет строки в топик брокера; журнал заведён миграцией 0013; идентификатор — ULID; ULID сортируется по времени. Верно из этого ноль. Брокера в продукте нет и он запрещён non-negotiable #7; миграция 0013 — про снятие перечня условий обхода; идентификатор собирается из СЛУЧАЙНЫХ байт (`newAuditEventID`, 22 символа crockford-base32), то есть по времени не сортируется ни в каком порядке. Опасен был не сам текст, а его направление: он описывал систему СЛОЖНЕЕ и исправнее, чем она есть, поэтому читатель уходил искать дренаж и топик вместо того, чтобы увидеть, что доставки нет вовсе.

func (AuditOutboxEntry) Validate

func (e AuditOutboxEntry) Validate() error

type AuditOutboxStatus

type AuditOutboxStatus string

AuditOutboxStatus — состояние ДОСТАВКИ строки журнала.

Значений ровно два, и столько же допускает ограничение таблицы (миграция `20260823001500_audit_journal_gets_its_receiver.sql`). Прежде их объявлялось четыре: «в полёте» и «отказ» не писал никто и никогда — полёта не существует, потому что строка держится блокировкой своей транзакции от клейма до пометки, а терминального отказа не существует, потому что у приёмника нет класса «не приму никогда». Значение, которого продукт произвести не умеет, обещает подсистему, которой нет.

const (
	AuditOutboxStatusPending AuditOutboxStatus = "pending"
	AuditOutboxStatusSent    AuditOutboxStatus = "sent"
)

func (AuditOutboxStatus) Validate

func (s AuditOutboxStatus) Validate() error

type BasicCredential

type BasicCredential struct {
	// PrincipalType — "user" | "service_account".
	PrincipalType string
	PrincipalID   string
	DisplayName   string
	// CredentialID — идентификатор СТРОКИ; им адресуется отзыв.
	CredentialID string
	ExpiresAt    time.Time
}

BasicCredential — вердикт авторитета о годном предъявленном удостоверении.

type BasicCredentialRefusalReason added in v0.5.0

type BasicCredentialRefusalReason string

BasicCredentialRefusalReason — ПРИЧИНА единого отказа полосы базового секрета (задача kaname#379). Закрытый словарь.

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

Наружу полоса отвечает ОДНИМ отказом — ErrBasicCredentialRefused: различимый исход был бы оракулом. Но отказ, неотличимый и внутри, делает каждый контроль полосы ненаблюдаемым: отсечка отзыва-всех, сработавшая тысячу раз, выглядит так же, как перебор секрета, и так же, как контроль, не сработавший ни разу.

Поэтому причина едет ВНУТРЬ — значением ошибки (RefuseBasicCredential) — и читается только BasicCredentialRefusalReasonOf. Текст ошибки остаётся дословно текстом единого отказа: вызывающий, напечатавший её, различия не увидит, а на провод полоса отдаёт свой фиксированный текст и вовсе не значение ошибки.

const (
	// BasicRefusalMalformed — предъявленное не называет удостоверения, которое
	// у нас может быть: пусто, форма или контрольная сумма не сходятся, вид по
	// приставке идентификатора не наш. База не спрашивалась.
	BasicRefusalMalformed BasicCredentialRefusalReason = "malformed"
	// BasicRefusalNotFound — живой строки нет: отозвано, истекло, владелец
	// неактивен либо её не было никогда. Внутри эти случаи тоже неразличимы, и
	// это не упущение: их отсекает один предикат одного оператора, а не
	// развилка, и развести их значило бы завести второй запрос.
	BasicRefusalNotFound BasicCredentialRefusalReason = "not-found"
	// BasicRefusalSecretMismatch — строка есть, секрет не тот.
	BasicRefusalSecretMismatch BasicCredentialRefusalReason = "secret-mismatch"
	// BasicRefusalOwnerRevoked — удостоверение выдано не позже отсечки
	// отзыва-всех его владельца-человека. Имя то же, что у исхода полосы ключа
	// токен-эндпоинта: один контроль — одно имя на всех полосах, которые его
	// исполняют.
	BasicRefusalOwnerRevoked BasicCredentialRefusalReason = "owner-revoked"
	// BasicRefusalOwnerUnverified — владелец-человек удостоверения не
	// подтвердил адрес (kaname#456, Р5). Имя то же, что у исхода полосы ключа
	// токен-эндпоинта: один вердикт правила выдачи — одно имя.
	BasicRefusalOwnerUnverified BasicCredentialRefusalReason = "owner-unverified"
)

func BasicCredentialRefusalReasonOf added in v0.5.0

func BasicCredentialRefusalReasonOf(err error) (BasicCredentialRefusalReason, bool)

BasicCredentialRefusalReasonOf — причина отказа полосы.

false — причина не названа: ошибка не отказ полосы либо отказ отдан голым сторожевым. Это отдельный исход, а не причина по умолчанию: подставленная причина выглядела бы фактом и легла бы в чужую клетку.

func BasicCredentialRefusalReasons added in v0.5.0

func BasicCredentialRefusalReasons() []BasicCredentialRefusalReason

BasicCredentialRefusalReasons — закрытый словарь целиком, в объявленном порядке. Читатели словаря (перепись исходов, витрина) выводят свои клетки отсюда, а не выписывают их второй копией.

type BearerDigest added in v0.5.0

type BearerDigest string

BearerDigest — свёртка носителя в хранилище (шестнадцатеричная запись SHA-256). Сама по себе носителем не является: обратно значение не восстанавливается.

type CeremonyContext added in v0.5.0

type CeremonyContext struct {
	FamilyID  string
	ClientID  string
	UserID    string
	SessionID string
	Scope     []string
}

CeremonyContext — контекст церемонии: он один на семейство, код и всякий обновляющий токен семейства. Расхождение его копий отвергает СОСТАВНОЙ внешний ключ схемы, а не проверка писателя.

func (CeremonyContext) Validate added in v0.5.0

func (c CeremonyContext) Validate() error

Validate — контекст заполнен целиком. Пустая область исходом не является: это не «все права», а отсутствие решения.

type CeremonyHandle added in v0.5.0

type CeremonyHandle struct {
	// contains filtered or unexported fields
}

CeremonyHandle — рукоятка человека: случайные байты, ни с чем не связанные. Нулевое значение — «рукоятки нет», а не рукоятка.

func NewCeremonyHandle added in v0.5.0

func NewCeremonyHandle() (CeremonyHandle, error)

NewCeremonyHandle — производитель новой рукоятки. Входа у него нет by construction: значение, выводимое из чего бы то ни было о человеке, этой подписью не выражается.

func RestoreCeremonyHandle added in v0.5.0

func RestoreCeremonyHandle(stored []byte) (CeremonyHandle, error)

RestoreCeremonyHandle — рукоятка из значения, сохранённого ранее; негодная форма — ошибка, а не рукоятка. Только для адаптера хранилища.

func (CeremonyHandle) Bytes added in v0.5.0

func (h CeremonyHandle) Bytes() []byte

Bytes — копия байтов рукоятки: единственный выход значения наружу.

func (CeremonyHandle) CarriesNoNameOf added in v0.5.0

func (h CeremonyHandle) CarriesNoNameOf(u User) error

CarriesNoNameOf — ЗАМОК: рукоятка не несёт ни одного ИДЕНТИФИКАТОРА человека — ни платформенного `id`, ни адреса почты.

Проверяется ВХОЖДЕНИЕ, а не равенство: подставленный адрес, дополненный до длины рукоятки, равенства не дал бы, а уехал бы так же безвозвратно. Регистр не различается: адрес хранится приведённым, а в церемонию попадает как есть.

Отображаемое имя НЕ судится, и это решение, а не пропуск. Вхождение имеет смысл, только пока случайные 64 байта его практически не несут: `id` — 20 знаков, адрес — не короче пяти (`x@y.z`), и случайное вхождение того и другого — порядка 1e-9 и меньше на чеканку. Имя бывает в один символ, и его вхождение случайно — около 40% чеканок (замер: 7922 из 20000 для «A»); рукоятка после заведения не меняется, поэтому такой отказ был бы вечным отказом человеку в регистрации. Имя к тому же не идентификатор: им ничего не ключуют, и в церемонии оно стоит законно в соседнем поле.

func (CeremonyHandle) IsZero added in v0.5.0

func (h CeremonyHandle) IsZero() bool

IsZero — рукоятки нет.

func (CeremonyHandle) Validate added in v0.5.0

func (h CeremonyHandle) Validate() error

Validate — длина нормы и не нули. Все нули означали бы, что значение не чеканили, а забыли, — это отличимо от негодной длины, потому что чинится другим.

type ClientDescription added in v0.5.0

type ClientDescription struct {
	// contains filtered or unexported fields
}

ClientDescription — приведённое описание клиента; нулевое значение — описания нет.

func NewClientDescription added in v0.5.0

func NewClientDescription(raw string) ClientDescription

NewClientDescription — приведение сырого значения заголовка (шаги шапки). Пустой вход — отсутствие. Приведение идемпотентно: значение, прочитанное из записи, приводится в себя.

func (ClientDescription) IsZero added in v0.5.0

func (c ClientDescription) IsZero() bool

IsZero — описания нет.

func (ClientDescription) Value added in v0.5.0

func (c ClientDescription) Value() string

Value — приведённое значение; у отсутствия — пустая строка.

type Cluster

type Cluster struct {
	ID          ClusterID
	Name        ClusterName
	Description Description
	CreatedAt   time.Time
}

Cluster — singleton (id = `cluster_root`). Корень иерархии cluster → account → project → resource. Используется как объект модели прав для `cluster:cluster_root#system_admin@user:usr_xxx`.

func (Cluster) Validate

func (c Cluster) Validate() error

type ClusterAdminEntry

type ClusterAdminEntry struct {
	ClusterAdminGrantID string
	SubjectType         string
	SubjectID           string
	SubjectEmail        string
	SubjectDisplayName  string
	GrantedByUserID     string
	GrantedByEmail      string // "" when granted_by == "bootstrap"
	GrantedAt           time.Time
}

ClusterAdminEntry — read-projection of one active cluster-admin grant with denormalised user fields (subject email/display_name, granter email) resolved by the ListActive read-adapter's users JOIN. Pure domain view used by the InternalClusterService.ListAdmins use-case + handler; the pg adapter maps its rows INTO this type so the use-case/handler never import the pgx adapter package (Clean-Architecture dependency rule — the port speaks in domain types).

Denormalised fields (SubjectEmail / SubjectDisplayName / GrantedByEmail) are output-only mirrors; the authoritative subject/granter ids are SubjectID / GrantedByUserID.

type ClusterAdminGrant

type ClusterAdminGrant struct {
	ID           ClusterAdminGrantID
	ClusterID    ClusterID
	SubjectType  GrantSubjectType
	SubjectID    SubjectID
	GrantedBy    string // 'bootstrap' либо user_id (verbatim text)
	GrantedAt    time.Time
	GrantedUntil *time.Time // NULL = permanent
}

ClusterAdminGrant — permanent root grant. Один источник истины для FGA-tuple `cluster_admin`.

Partial UNIQUE (subject_type, subject_id) WHERE granted_until IS NULL гарантирует на DB-уровне, что **permanent** grant у одного subject — один. Temporary grants не входят в модель — есть только permanent.

func (ClusterAdminGrant) IsActive

func (g ClusterAdminGrant) IsActive() bool

IsActive — true если grant — permanent active (`granted_until IS NULL`). False — для revoked / expired / time-bombed grants (granted_until set). Используется handler'ом и use-case'ами для diagnostic-веток.

func (ClusterAdminGrant) Validate

func (g ClusterAdminGrant) Validate() error

type ClusterAdminGrantID

type ClusterAdminGrantID string

ClusterAdminGrantID — self-validating newtype, format `cag_<17-crockford>`.

func (ClusterAdminGrantID) Validate

func (id ClusterAdminGrantID) Validate() error

type ClusterID

type ClusterID string

ClusterID — fixed literal `cluster_root` (singleton constraint в DB).

func (ClusterID) Validate

func (id ClusterID) Validate() error

type ClusterName

type ClusterName string

ClusterName — отображаемое имя singleton-кластера: длина 1..64 и БОЛЬШЕ ничего. Здесь стояло «kebab-case», и это было обещанием алфавита, которого проверка не делает ни в одной ветке.

Форму имени ресурса (`pkg/validate/nameform`) поле НЕ несёт намеренно: оно не задаётся клиентом — значение пишет посевная миграция, а у службы кластера нет глагола, который бы его менял.

func (ClusterName) Validate

func (n ClusterName) Validate() error

type CodeDigest added in v0.5.0

type CodeDigest string

CodeDigest — свёртка кода в хранилище (шестнадцатеричная запись SHA-256).

type ContainerSelector added in v0.5.0

type ContainerSelector struct {
	// Type — точечный тип контейнера (`registry.registries`), словарь каталога.
	Type string
	// OwnerFP — отпечаток правила-владельца: его селектор и выбирает контейнер.
	// По нему разворачивание выдачи классу узнаёт, что контейнер в составе выдачи
	// ИМЕННО этим правилом (а не соседним), прежде чем писать `public_reader`.
	// В хранимую строку селектора не едет: решению о доступе он не нужен.
	OwnerFP       string
	Arm           Arm
	ResourceNames []string          // ArmNames only
	MatchLabels   map[string]string // ArmLabels only
}

ContainerSelector — тип и отбор правила-владельца вложенного правила (kaname#874). Ветвь — та же, что у правила верхнего уровня: якорь выбирает все контейнеры типа, имена — названные, метки — с совпавшими метками.

type CredentialKind

type CredentialKind string

CredentialKind — ЧЕМ УДОСТОВЕРЕНИЕ СЕБЯ ПРЕДЪЯВЛЯЕТ (задача kacho#1142, приёмка BAT-1 §2.5, §4.1).

Значение — то же, что лежит в колонке `credential_kind` обеих таблиц удостоверений: второго написания не заводится. Вид ЗАПИСЫВАЕТСЯ при вставке и читателем НЕ вычисляется: правила вывода вида по содержимому строки в службе нет.

Словарь закрыт, и видов в нём три. Четвёртый, LEGACY, описывал строки прежнего потока без собственного ключевого материала и снят вместе со столбцом имени клиента у прежнего издателя (kaname#362): вид был определён через этот столбец, и строк этого вида миграция снятия не допускает.

const (
	// CredentialKindUnspecified — вид не назван вызывающим. Встречается ТОЛЬКО
	// во входе выдачи и разрешается сохранённым поведением; в строке
	// удостоверения не бывает.
	CredentialKindUnspecified CredentialKind = ""

	// CredentialKindKeypair — ключевая пара ES256: вызывающий сам собирает и
	// подписывает `client_assertion` и обменивает его.
	CredentialKindKeypair CredentialKind = "KEYPAIR"

	// CredentialKindSecret — однострочный непрозрачный секрет, предъявляемый
	// как есть.
	CredentialKindSecret CredentialKind = "SECRET"

	// CredentialKindFederated — удостоверение предъявляет ВНЕШНИЙ издатель по
	// перечню доверенных субъектов; ни материала, ни секрета у нас нет.
	CredentialKindFederated CredentialKind = "FEDERATED"
)

func ResolveIssuedKind

func ResolveIssuedKind(asked CredentialKind, hasTrustedSubjects, federationSupported bool) (CredentialKind, error)

ResolveIssuedKind — КЛАССИФИКАТОР НАД ЗАПРОСОМ ВЫДАЧИ. Ветвей ТРИ, и дыры у него нет by construction: «ни материала, ни перечня» здесь означает KEYPAIR, потому что материал чеканим МЫ — строка получает его при выпуске. Значение вне словаря отвергается с именем поля, а не получает ближайший вид.

asked — вид, названный вызывающим (пустой = не назван). hasTrustedSubjects — непуст ли перечень доверенных субъектов запроса. federationSupported — есть ли у этого глагола поле, которым задаётся федеративный вид (у личности его нет, и вид недостижим by construction).

func (CredentialKind) IsIssuable

func (k CredentialKind) IsIssuable() bool

IsIssuable отвечает, производит ли этот вид хоть один глагол выдачи. Ответ — свойство самого вида, а не проверки в конкретном глаголе: иначе следующий глагол завёл бы выдачу под значение вне словаря.

func (CredentialKind) String

func (k CredentialKind) String() string

String — значение как оно лежит в колонке.

type Description

type Description string

func (Description) Validate

func (d Description) Validate() error

type Discoverability added in v0.5.0

type Discoverability string

Discoverability — область значений признака обнаружимости: ТРИ состояния (Р12). «Не сообщено» — своё значение, а не пустое и не «нет»: пустая строка состоянием не является и до записи не доживает.

const (
	// DiscoverabilityConfirmed — расширение результата церемонии сообщило
	// «обнаруживаемое» (`credProps.rk = true`).
	DiscoverabilityConfirmed Discoverability = "discoverable"
	// DiscoverabilityNotDiscoverable — расширение сообщило «не обнаруживаемое».
	// Писателя у состояния сегодня нет: приём церемонии такой результат
	// отвергает (Ф7-41), — но область значений факта его содержит (Ф7-57).
	DiscoverabilityNotDiscoverable Discoverability = "not-discoverable"
	// DiscoverabilityUnreported — расширения в результате нет.
	DiscoverabilityUnreported Discoverability = "unreported"
)

func AccessKeyDiscoverabilities added in v0.5.0

func AccessKeyDiscoverabilities() []Discoverability

AccessKeyDiscoverabilities — закрытый перечень, тот же, что у ограничения схемы `user_access_keys_discoverability_check`.

func DiscoverabilityFromExtension added in v0.5.0

func DiscoverabilityFromExtension(rk *bool) Discoverability

DiscoverabilityFromExtension — состояние по расширению `credProps.rk`: nil — «не сообщено», а не «нет».

func (Discoverability) Validate added in v0.5.0

func (d Discoverability) Validate() error

Validate — значение из закрытого перечня.

type DisplayName

type DisplayName string

func (DisplayName) Validate

func (d DisplayName) Validate() error

type Email

type Email string

func (Email) Validate

func (e Email) Validate() error

type EmailChangeCode added in v0.5.0

type EmailChangeCode struct {
	ID     EmailChangeCodeID
	UserID UserID
	// NewEmail — новый адрес в приведённом виде.
	NewEmail Email
	// Digest — свёртка кода; пусто — новый адрес занят на запросе, кода нет.
	Digest CodeDigest
	// IssuedAt — момент запроса (часы полосы); по нему считается темп (Р6).
	IssuedAt time.Time
	// ExpiresAt — абсолютный срок; величина — настройка.
	ExpiresAt time.Time
}

EmailChangeCode — запись принятого запроса смены адреса. Состав закрыт.

func (EmailChangeCode) Validate added in v0.5.0

func (c EmailChangeCode) Validate() error

Validate — запись, годная к записи: судится до базы то, что база держит ограничениями.

type EmailChangeCodeID added in v0.5.0

type EmailChangeCodeID string

EmailChangeCodeID — идентификатор строки отложенной смены. Наружу не адресуется.

type EventTypeName

type EventTypeName string

EventTypeName — `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$` (CHECK).

func (EventTypeName) Validate

func (n EventTypeName) Validate() error

type ExternalSubject

type ExternalSubject string // OIDC sub

func NewOwnLaneSubject added in v0.5.0

func NewOwnLaneSubject() ExternalSubject

NewOwnLaneSubject — свежая идентичность субъекта нашей полосы.

func (ExternalSubject) IsOwnLane added in v0.5.0

func (s ExternalSubject) IsOwnLane() bool

IsOwnLane — отчеканена ли идентичность нашей полосой (по голове).

func (ExternalSubject) Validate

func (s ExternalSubject) Validate() error

type FamilyRevocationReason added in v0.5.0

type FamilyRevocationReason string

FamilyRevocationReason — причина отзыва семейства. Словарь ЗАКРЫТ и совпадает с ограничением `token_families_revoked_reason_ck`: корзины «прочее» у него нет, и значение вне перечня — наша ошибка, а не чужая. Совпадение в обе стороны держит проба живой схемы `TestIntegration_RevocationVocabularyAgreesWithTheDomain`, а то, что у каждого слова есть писатель, — гейт `TestFamilyRevocationVocabulary_KN_FRV_17_EveryWordHasAWriter`: слово, которого никто не пишет, перечень превращает в обещание (kaname#339).

const (
	// FamilyRevokedByCodeReplay — повторное предъявление кода авторизации.
	FamilyRevokedByCodeReplay FamilyRevocationReason = "code-replay"
	// FamilyRevokedByRefreshReplay — повторное предъявление обновляющего токена.
	FamilyRevokedByRefreshReplay FamilyRevocationReason = "refresh-replay"
	// FamilyRevokedBySessionEnd — сессия, в которой шла церемония, снята.
	FamilyRevokedBySessionEnd FamilyRevocationReason = "session-ended"
	// FamilyRevokedByClientRevocation — клиент, которому выдан грант, сам
	// попросил отзыва (RFC 7009). Написание — дословно причина фундамента
	// `oauthceremony.RevocationClientRevoke`: адаптер порта отзыва сопрягает
	// словари ПО ЗНАЧЕНИЮ (`ceremonyport.FamilyReasonOf`), и другое написание
	// оставило бы причину без слова (kaname#406).
	FamilyRevokedByClientRevocation FamilyRevocationReason = "client-revoke"
)

func FamilyRevocationReasons added in v0.5.0

func FamilyRevocationReasons() []FamilyRevocationReason

FamilyRevocationReasons — перечень целиком, КОПИЕЙ: вызывающий не может расширить его на месте.

func (FamilyRevocationReason) Validate added in v0.5.0

func (r FamilyRevocationReason) Validate() error

Validate — причина из словаря. Пустая причина исходом не является: отзыв без причины неотличим от неотозванного семейства в переписи.

type FeedSource

type FeedSource int8

FeedSource — where the reconciler reads candidate objects for a selectable type (D7).

const (
	// FeedMirror — candidates read from kaname.resource_mirror (labels @>
	// matchLabels); containment from mirror.parent_* (D7). Mirror-fed types can be
	// PENDING_VERIFICATION (object not yet mirrored). Consumer-owned resources:
	// compute / vpc / loadbalancer.
	FeedMirror FeedSource = iota
	// FeedIAMDirect — candidates read SAME-DB from IAM's own resource table;
	// containment via iam-hierarchy (project ⊑ account ⊑ cluster). Never PENDING
	// (the object is always in its own table the instant it exists). Покрывает ВСЕ
	// iam-native типы (любой `iam.*`): account / project / user / serviceAccount /
	// group / role / accessBinding — все label-selectable под единой моделью.
	FeedIAMDirect
)

func FeedSourceForType

func FeedSourceForType(objectType string) FeedSource

FeedSourceForType classifies a selectable object type by its feed-source (D6/D7). An `iam.*` type is IAM-direct (own-table same-DB match, iam-hierarchy containment); every other selectable family (compute/vpc/loadbalancer) is mirror-fed. The classification is by module-prefix only — the precise selectable whitelist is enforced separately in the use-case (a non-selectable type never reaches the reconciler).

type FormKind added in v0.5.0

type FormKind string

FormKind — вид формы, к которому привязан признак защиты от подделки запроса (Р12). Перечень ЗАКРЫТ и объявлен один раз; Ф4 и Ф5 дописывают свои виды сюда, а не заводят второй перечень.

const (
	FormLogin    FormKind = "login"
	FormLogout   FormKind = "logout"
	FormPassword FormKind = "password"
	// FormRegister — форма регистрации (Ф4 Р1; форма защищена признаком, как и
	// прочие, меняющие состояние, — Ф1-39…43).
	FormRegister FormKind = "register"
	// Восстановление доступа (Ф5, `kacho#1271`) — две формы, два вида: запрос
	// кода и его предъявление с новым паролем. Признак одной не годится другой
	// (Ф1 Р6): у них разные предметы и разные последствия.
	FormRecovery         FormKind = "recovery"
	FormRecoveryComplete FormKind = "recovery-complete"
	// Второй фактор (Ф12, `kacho#1281`, Р4): один вид на четыре глагола
	// семейства `second-factor/*` — у них один предмет (заведённый фактор
	// самого человека) — и свой вид у церемонии повышения: её зовут и без
	// фактора, ветвью пароля (Ф11-09), и признак семейства ей не годится.
	FormSecondFactor FormKind = "second-factor"
	FormStepUp       FormKind = "step-up"
	// FormStepUpAccessKeyBegin — выдача испытания повышения ключом доступа под
	// живой сессией (kaname#827, приёмка SUA Р2): свой вид, а не вид
	// церемонии — выдача меняет состояние (заводит испытание сессии), и признак
	// церемонии на форме выдачи не предъявим, и наоборот.
	FormStepUpAccessKeyBegin FormKind = "step-up-access-key-begin"
	// Подтверждение адреса (kaname#456, Р6; имена ратифицированы — Ф6 Р15):
	// запрос письма и предъявление кода — две формы, два вида, и признак одного
	// другому не годится.
	FormVerifyEmail        FormKind = "verify-email"
	FormVerifyEmailConfirm FormKind = "verify-email-confirm"
	// Вход ключом доступа (Ф13 Р1, решение владельца 2026-09-22): ДВА вида на
	// два глагола `access-key/begin` и `access-key/login`. Оба меняют состояние
	// — `begin` заводит испытание и считается попыткой, `login` выдаёт сессию,
	// — и проверка «вид признака — вид ЭТОЙ формы» (Ф3 Р12) обязана их
	// различать: признак формы запроса на форме подтверждения не предъявим, и
	// наоборот. Контекст при этом ОДИН (печенье `kaname_form`).
	FormAccessKeyBegin FormKind = "access-key-begin"
	FormAccessKeyLogin FormKind = "access-key-login"
	// FormPasswordEnroll — заведение первого пароля из живой сессии (kaname#213,
	// приёмка A7 Р1): свой вид, не вид смены — заведение и смена суть разные
	// действия с разными основаниями.
	FormPasswordEnroll FormKind = "password-enroll"
	// Смена адреса почты (kaname#635, приёмка email-change Р1): запрос смены и
	// предъявление кода с нового адреса — две формы, два вида; признак одного
	// другому не годится.
	FormEmailChange        FormKind = "email-change"
	FormEmailChangeConfirm FormKind = "email-change-confirm"
	// FormSessionEnd — снятие своих записей сессии (kaname#634, Р1): ОДИН вид на
	// оба снимающих глагола — выход из выбранной и из всех, кроме текущей. Предмет
	// у них один (снятие своих записей), и признак различает формы, а не исходы —
	// как у семейства второго фактора.
	FormSessionEnd FormKind = "session-end"
)

func FormKinds added in v0.5.0

func FormKinds() []FormKind

FormKinds — закрытый перечень видов формы, копией.

func ParseFormKind added in v0.5.0

func ParseFormKind(s string) (FormKind, error)

ParseFormKind — вид формы из строки запроса; вне перечня — отказ, называющий поле и перечень.

type GrantDelegation added in v0.7.0

type GrantDelegation struct {
	ID        GrantDelegationID
	AccountID AccountID
	// DelegateID — служебная учётка-делегат. Тип делегата один
	// (`SubjectTypeServiceAccount`), поэтому хранится только идентификатор.
	DelegateID   ServiceAccountID
	RoleIDs      []RoleID
	TargetTypes  []string
	SubjectTypes []SubjectType
	// Module — модуль ролей и типов делегирования; выводится, на вход не
	// принимается.
	Module      string
	Status      GrantDelegationStatus
	DelegatedBy GrantDelegationActor
	CreatedAt   time.Time
	RevokedAt   *time.Time
	RevokedBy   *GrantDelegationActor
}

GrantDelegation — делегирование права выдавать.

func SortedGrantDelegationAxes added in v0.7.0

func SortedGrantDelegationAxes(d GrantDelegation) GrantDelegation

SortedGrantDelegationAxes — оси делегирования по возрастанию строки: оси — наборы, порядок входа не хранится (Р1, `api-repeated-order`).

func (GrantDelegation) Active added in v0.7.0

func (d GrantDelegation) Active() bool

Active — делегирование действует.

func (GrantDelegation) AllowsRole added in v0.7.0

func (d GrantDelegation) AllowsRole(id RoleID) bool

AllowsRole — роль входит в набор ролей делегирования.

func (GrantDelegation) AllowsSubjectType added in v0.7.0

func (d GrantDelegation) AllowsSubjectType(t SubjectType) bool

AllowsSubjectType — тип субъекта выдачи входит в набор типов делегирования.

func (GrantDelegation) AllowsTargetType added in v0.7.0

func (d GrantDelegation) AllowsTargetType(dotted string) bool

AllowsTargetType — тип объекта цели входит в набор типов делегирования.

func (GrantDelegation) IsDelegate added in v0.7.0

func (d GrantDelegation) IsDelegate(principalType, principalID string) bool

IsDelegate — принципал (вид, идентификатор) — делегат этого делегирования.

Сравнивается ПАРА, а не голый идентификатор: вид принципала нормализуется одной функцией (`SubjectTypeOfPrincipal`), поэтому человек с совпавшим идентификатором делегатом не считается by construction.

func (GrantDelegation) IsDelegateSubject added in v0.7.0

func (d GrantDelegation) IsDelegateSubject(s Subject) bool

IsDelegateSubject — субъект выдачи — сам делегат (Д4: себе делегат не выдаёт).

type GrantDelegationActor added in v0.7.0

type GrantDelegationActor struct {
	Type string
	ID   string
}

GrantDelegationActor — принципал шага жизненного цикла: вид ("user" | "service_account") и идентификатор.

type GrantDelegationID added in v0.7.0

type GrantDelegationID string

GrantDelegationID — идентификатор делегирования.

type GrantDelegationStatus added in v0.7.0

type GrantDelegationStatus string

GrantDelegationStatus — состояние делегирования; слово хранения.

const (
	// GrantDelegationActive — делегирование действует.
	GrantDelegationActive GrantDelegationStatus = "ACTIVE"
	// GrantDelegationRevoked — отозвано; конечное состояние.
	GrantDelegationRevoked GrantDelegationStatus = "REVOKED"
)

type GrantSubjectType

type GrantSubjectType string

GrantSubjectType — enum: user|service_account (NOT group: cluster-admin grant — strictly individual identity for audit). Backed by migration 0011 CHECK.

const (
	GrantSubjectTypeUser           GrantSubjectType = "user"
	GrantSubjectTypeServiceAccount GrantSubjectType = "service_account"
)

func (GrantSubjectType) Validate

func (t GrantSubjectType) Validate() error

type Group

type Group struct {
	ID          GroupID
	AccountID   AccountID
	Name        GroupName
	Description Description
	Labels      Labels
	CreatedAt   time.Time
}

Group — Account-scoped (account_id FK ON DELETE RESTRICT), имеет members (User или ServiceAccount — полиморфно через `group_members.member_type`). Используется в AccessBinding для упрощения раздачи прав.

func (Group) Validate

func (g Group) Validate() error

type GroupID

type GroupID string

type GroupMember

type GroupMember struct {
	GroupID    GroupID
	MemberType SubjectType // user | service_account (group из enum'а исключен)
	MemberID   SubjectID
	AddedAt    time.Time
}

GroupMember — связка group_id ↔ (member_type, member_id). Целостность member_id обеспечивается DB-триггером `group_members_member_exists_trg` (нет полиморфного FK в Postgres).

func (GroupMember) Validate

func (m GroupMember) Validate() error

type GroupName

type GroupName string

func (GroupName) Validate

func (n GroupName) Validate() error

type HumanSession added in v0.5.0

type HumanSession struct {
	ID     HumanSessionID
	UserID UserID
	// AuthenticatedAt — момент аутентификации. Неподвижен всю жизнь записи
	// (Ф11 Р6): край сравнивает его с отсечкой.
	AuthenticatedAt time.Time
	// LastPresentedAt — момент последнего ПРЕДЪЯВЛЕНИЯ СПОСОБА (сверка
	// пароля, кода, ключа), не носителя запросу. Читается внутри службы.
	LastPresentedAt time.Time
	// ExpiresAt — абсолютный срок, ОДИН (F4d-27): окна бездействия нет.
	ExpiresAt time.Time
	// AssuranceLevel — по правилу Ф11 от множества предъявленного.
	AssuranceLevel string
	// PresentedMethods — множество предъявленного (Ф11 Р2), слова словаря
	// `assurance.Methods`. Читается внутри службы правилом уровня.
	PresentedMethods []string
	// Client — описание клиента, каким его назвал запрос выдачи (kaname#634,
	// Р3). Пишет его ТОЛЬКО операция выдачи; перевыпуск носителя его не трогает.
	// Читатель — ровно перечень своих сессий: в ответ краю и в событие аудита оно
	// не идёт.
	Client ClientDescription
	// CreatedAt назначает запись.
	CreatedAt time.Time
}

HumanSession — запись сессии (Р1). Состав закрыт: поле без читателя есть значение, которое пишут и не читают.

func (HumanSession) Expired added in v0.5.0

func (s HumanSession) Expired(now time.Time) bool

Expired — истёк ли срок на данный момент. Граница — включающая: в сам момент срока сессии уже нет.

func (HumanSession) Validate added in v0.5.0

func (s HumanSession) Validate() error

Validate — запись, годная к выдаче. Судится ДО базы то, что база держит ограничениями: уровень из оси, непустое множество, срок позже момента, последнее предъявление не раньше аутентификации.

type HumanSessionID added in v0.5.0

type HumanSessionID string

HumanSessionID — идентификатор записи сессии. Адресуется ровно одним внешним путём — её ВЛАДЕЛЬЦЕМ на полосе формы (kaname#634, приёмка `own-sessions-are-listed-and-ended-by-their-owner.md`, Р4, Р5): перечень своих сессий его называет, выход из выбранной его принимает. Края он достигает ровно одним путём — ответом `Resolve` о носителе, и край возвращает его службе метаданным доверенного отправителя, чтобы снятие ключа доступа оставило текущую запись живой (Ф13 Р8, kaname#677). Носителем он не является и о субъекте ничего не говорит; субъект глагола — всегда субъект носителя запроса (у снятия ключа — переданная личность).

func ParseHumanSessionID added in v0.5.0

func ParseHumanSessionID(s string) (id HumanSessionID, ok bool)

ParseHumanSessionID — идентификатор записи, присланный клиентом; форма судится ДО хранилища. Пустое значение — отказ «required» — вызывающий судит раньше этого разбора; здесь ok=false значит «не та форма».

type InteractiveClient

type InteractiveClient struct {
	ID          InteractiveClientID
	CreatedAt   time.Time
	Name        InteractiveClientName
	Description Description
	Labels      Labels

	// RedirectURIs — where the provider may deliver an authorization code.
	// At least one, every one an absolute https:// URL.
	RedirectURIs []string
	// PostLogoutRedirectURIs — optional; same https:// rule when present.
	PostLogoutRedirectURIs []string

	// ClientID — provider-side client id (output-only).
	ClientID string
	// Audiences — stamped by iam from its own configuration (output-only).
	Audiences []string
	// GrantTypes — always exactly authorization_code + refresh_token (output-only).
	GrantTypes []string
	// TokenEndpointAuthMethod — provider-side auth method (output-only).
	TokenEndpointAuthMethod string

	Status InteractiveClientStatus
}

InteractiveClient — the OAuth2 client through which a HUMAN completes an interactive sign-in ceremony.

Provider-side fields (ClientID, Audiences, GrantTypes, TokenEndpointAuthMethod) are OUTPUT-ONLY: they are decided by iam and the identity provider, never by the caller. Audiences in particular is a decision, not an omission — see Р2 in the acceptance: a field that can be set but cannot be set CORRECTLY is a field without a reader, and one that can be set incorrectly breaks the edge silently.

func (InteractiveClient) Validate

func (c InteractiveClient) Validate() error

Validate — self-validating domain entity.

type InteractiveClientID

type InteractiveClientID string

InteractiveClientID — id of an InteractiveClient. Hyphen canon `ic-<17>` (ids.PrefixInteractiveClientHyphen), immutable for the life of the resource and the only externally addressable identity (core rule #15).

type InteractiveClientName

type InteractiveClientName string

InteractiveClientName — cluster-unique cosmetic label. Judged by the single resource-name form of the tree, so the DB CHECK and this validator agree.

func (InteractiveClientName) Validate

func (n InteractiveClientName) Validate() error

Validate — единственная форма имени (`pkg/validate/nameform`).

type InteractiveClientStatus

type InteractiveClientStatus string

InteractiveClientStatus — lifecycle status.

const (
	InteractiveClientActive   InteractiveClientStatus = "ACTIVE"
	InteractiveClientDeleting InteractiveClientStatus = "DELETING"
)

InteractiveClientStatus values. Mirrored by a DB CHECK.

func (InteractiveClientStatus) Validate

func (s InteractiveClientStatus) Validate() error

Validate — the status is one of the two named values.

type InviteStatus

type InviteStatus string

InviteStatus — invite-flow state for a User row.

PENDING — created via `UserService.Invite`, external_id="" until first login; the invitee has not yet registered and verified the address. ACTIVE — either self-registration on our sign-in lane without a pending invite, or a PENDING row activated by confirming the invited address. BLOCKED — административный запрет на членство в Account'е. Ставится и снимается ДЕЙСТВИЯМИ `UserService.Block` / `Unblock` (право `identity_suspender@iam_user` = админ аккаунта плюс каскад облака; `v_update` с этого типа снят, kacho#1128); писатель — `userWriter.SetInviteStatus`.

Состояние принадлежит СТРОКЕ ЧЛЕНСТВА, а не человеку. Одна личность держит по строке на каждый Account, поэтому запрет принадлежит тому аккаунту, который его наложил, и не отключает личность там, где она законно активна: выдача токена перебирает набор членств и обслуживает первое аутентифицирующееся, отказывая лишь когда ни одно не может (token_enrichment_service.go).

Снимать запрет самостоятельным действием нельзя: восстановление пароля доказывает владение почтовым ящиком — ровно то, чего администратор, ставя запрет, под сомнение не ставил (см. humansession/recovery_complete.go). Поэтому у пути блокировки ОБЯЗАН быть административный путь снятия, иначе заблокированный окажется заперт навсегда. Гейт blocked_state_reachability_test.go требует, чтобы каждый писатель этого состояния был объявлен вместе со ссылкой на снятие, — и делает появление одностороннего пути упавшей сборкой, а не открытием.

const (
	InviteStatusPending InviteStatus = "PENDING"
	InviteStatusActive  InviteStatus = "ACTIVE"
	InviteStatusBlocked InviteStatus = "BLOCKED"
)

func (InviteStatus) MayAuthenticate

func (s InviteStatus) MayAuthenticate() bool

MayAuthenticate reports whether a user in this state may be issued a token.

This is the VERDICT both token hooks ask for, and the reason it exists as a predicate rather than as a WHERE clause: the hooks used to resolve the identity through an ACTIVE-only query, which turns "blocked" into "absent" — and then read absence in opposite directions. One refused; the other took it for "the mirror has not committed yet" and issued the reduced claim set to a blocked user. A filter cannot be asked "why"; a verdict can.

Only ACTIVE authenticates. PENDING is an invitee who has not confirmed an identity yet (the DB CHECK users_invite_status_consistency keeps such a row from carrying an external id at all, so it is a floor rather than a live path), and an unset state is not an authorisation.

func (InviteStatus) MaySignInToOwnLane added in v0.5.0

func (s InviteStatus) MaySignInToOwnLane() bool

MaySignInToOwnLane reports whether a person in this state may sign in on OUR sign-in lane and hold a session there (kaname#456, Р11 п. 1). Wider than InviteStatus.MayAuthenticate by exactly one state: an invitee who registered by the invite's address (PENDING) signs in to the verification position — the only screens a session reaches before the address is confirmed — while a token is issued to nobody in that position. BLOCKED never signs in.

func (InviteStatus) Validate

func (s InviteStatus) Validate() error

type KeyID

type KeyID string

KeyID — идентификатор ключа (`kid`).

func (KeyID) Validate

func (k KeyID) Validate() error

Validate проверяет форму идентификатора ключа.

type LabelKey

type LabelKey string

func (LabelKey) Validate

func (k LabelKey) Validate() error

type LabelVal

type LabelVal string

func (LabelVal) Validate

func (v LabelVal) Validate() error

type Labels

type Labels map[LabelKey]LabelVal

Labels — key→val map.

func (Labels) Validate

func (l Labels) Validate() error

Validate — Labels: cardinality ≤64; each pair via LabelKey/LabelVal.

type LimitKind

type LimitKind string

LimitKind — a dotted token naming what is being counted. Two forms, and only two:

`<domain>.<resource>`            — a resource counted in its carrier
`<domain>.<parent>.<child>`      — how many <child> fit in ONE <parent>

Both forms name REAL types of the authorization model, and the three-part form names two of them. That is a gate rather than a convention: a ceiling stated on a name the platform does not know is a ceiling nobody can check and nobody can show the tenant (§7 п.9 of the acceptance).

func AccountScopedKinds

func AccountScopedKinds() []LimitKind

AccountScopedKinds отдаёт КОПИЮ перечня в устойчивом порядке.

func IdentityCarriedKinds added in v0.3.0

func IdentityCarriedKinds() []LimitKind

IdentityCarriedKinds — копия закрытого перечня, в порядке объявления.

func PostureStatedKinds

func PostureStatedKinds() []LimitKind

PostureStatedKinds — копия закрытого множества, в порядке объявления.

func (LimitKind) ChildKind

func (k LimitKind) ChildKind() LimitKind

ChildKind — the two-part token of the child a nested kind counts; empty for a flat kind. `vpc.network.subnet` → `vpc.subnet`.

The child's domain is the kind's domain: a nested kind never crosses a service boundary, because the parent and the child are rows of one database and the count is an invariant of one schema (data-integrity §within-service).

func (LimitKind) Nested

func (k LimitKind) Nested() bool

Nested reports whether this kind bounds children within ONE parent (`vpc.network.subnet`) rather than within the carrier as a whole.

func (LimitKind) ParentKind

func (k LimitKind) ParentKind() LimitKind

ParentKind — the two-part token of the parent a nested kind counts within; empty for a flat kind. `vpc.network.subnet` → `vpc.network`.

This is the token that must resolve against the closed table, and it is returned rather than re-derived at each call site so the two halves of the three-part gate cannot disagree about where the split is.

func (LimitKind) Parts

func (k LimitKind) Parts() []string

Parts splits the kind into its dotted segments.

func (LimitKind) Service

func (k LimitKind) Service() string

Service — the owner service this kind belongs to (`vpc.network` → `vpc`, `vpc.network.subnet` → `vpc`).

Derived from the token rather than stored beside it: two fields naming one thing drift, and the dot is the same separator the platform's reference types already use.

type LoginMethod added in v0.5.0

type LoginMethod struct {
	UserID   UserID
	Kind     LoginMethodKind
	Verifier LoginVerifier
	// State — состояние строки (Ф12 Р1). Обязательно у всякого вида: значение,
	// подставленное молча, было бы вторым местом об одном предмете рядом с
	// умолчанием колонки.
	State LoginMethodState
	// AcceptedStep — последний принятый шаг кода по времени (Ф12 Р5);
	// StepAccepted отделяет «шага ещё не было» от значения: нулевой шаг —
	// законная величина оси, а не отсутствие.
	AcceptedStep int64
	StepAccepted bool
	// CreatedAt назначает запись. У строки `pending` — момент заведения, от
	// которого считается срок (Ф12 Р8); у `active` — момент подтверждения.
	CreatedAt time.Time
	// LastUsedAt — момент последнего ПРИНЯТОГО предъявления способа второго
	// фактора (kaname#827, приёмка SUA Р6): у `totp` — принятый код, у
	// `lookup_secret` — снятый запасной код. LastUsed отделяет «не
	// использовали» от значения: нулевой момент значением оси не является, и
	// отсутствие наружу — отсутствие ключа ответа, а не нулевое время. Пишут
	// его только операторы предъявления; у пароля момента нет — его не
	// допускает проверка схемы.
	LastUsedAt time.Time
	LastUsed   bool
}

LoginMethod — строка способа входа.

Ключ — пара (`UserID`, `Kind`): второй способ того же вида человеку отвергается базой. `UserID` — идентификатор ЧЕЛОВЕКА (`users.id`), а не внешний субъект и не почта: у приглашённого внешнего субъекта нет, почта изменяема.

func (LoginMethod) Enrolled added in v0.5.0

func (m LoginMethod) Enrolled() bool

Enrolled — строка есть способ входа: заведён и подтверждён (Ф12 Р1).

func (LoginMethod) Validate added in v0.5.0

func (m LoginMethod) Validate() error

Validate — строка, годная к записи. Отказ не несёт материала.

type LoginMethodKind added in v0.5.0

type LoginMethodKind string

LoginMethodKind — вид способа входа. Словарь ЗАКРЫТ и совпадает с ограничением таблицы `user_login_methods_kind_check`.

ИМЕНА БЕРУТСЯ У СЛОВАРЯ СПОСОБОВ ПРЕДЪЯВЛЕНИЯ (`internal/assurance`, Ф11 Р8), а не пишутся здесь литералами: словарь объявлен ОДИН раз, и второе объявление перечня имён в прод-коде службы — находка гейта `TestAssuranceMethodVocabularyIsDeclaredOnce`. Видов три (Ф12 Р1): пароль · код по времени · набор запасных кодов; ключ доступа (`webauthn`) строкой этой таблицы не является — его хранилище заводит Ф7 своей приёмкой.

func LoginMethodKinds added in v0.5.0

func LoginMethodKinds() []LoginMethodKind

LoginMethodKinds — словарь видов; копия, чтобы вызывающий не мог его расширить.

func ParseLoginMethodKind added in v0.5.0

func ParseLoginMethodKind(s string) (LoginMethodKind, error)

ParseLoginMethodKind — вид из строки. Написание одно: регистр и пробелы не нормализуются, иначе вид становится несколькими значениями.

func (LoginMethodKind) Validate added in v0.5.0

func (k LoginMethodKind) Validate() error

Validate — вид из словаря.

type LoginMethodState added in v0.5.0

type LoginMethodState string

LoginMethodState — состояние строки способа (Ф12 Р1). Значений ровно два, закреплены ограничением схемы `user_login_methods_state_check`.

const (
	// LoginMethodStatePending — секрет чеканен, первый код не предъявлен.
	// Способом входа такая строка не является и живёт не дольше окна свежести
	// правки своих данных (Ф12 Р8); снимает её уборка либо новое заведение.
	LoginMethodStatePending LoginMethodState = "pending"
	// LoginMethodStateActive — подтверждён; единственное состояние `password`
	// и `lookup_secret`.
	LoginMethodStateActive LoginMethodState = "active"
)

func (LoginMethodState) Validate added in v0.5.0

func (s LoginMethodState) Validate() error

Validate — состояние из словаря.

type LoginVerifier added in v0.5.0

type LoginVerifier struct {
	// contains filtered or unexported fields
}

LoginVerifier — проверочный материал способа входа: хеш пароля в той форме, в какой его создала функция хеширования (прежнего поставщика либо наша). Это СЕКРЕТ: хеш не пароль, но перебор офлайн по нему возможен.

Устройство выбрано так, чтобы материал не уезжал из процесса ни одним общим путём вывода:

  • форматирование, журнал и `String` отдают заглушку;
  • JSON и текстовая форма ОТКАЗЫВАЮТ;
  • материал лежит ЗА УКАЗАТЕЛЕМ. Форматирование обходит неэкспортированное поле, не вызывая методов типа, и печатает его содержимое сырым; указатель на вложенной глубине печатается адресом, а не содержимым. Так тип защищён и там, где его методы не спрашиваются.

Выход у материала ОДИН — `Reveal`. Его вызывающих держит гейт дерева `internal/check` `TestLoginVerifierStaysInside`.

Нулевое значение означает «материала нет» и в строку способа не проходит: отсутствие материала выражается ОТСУТСТВИЕМ строки.

Сравнение через `==` сверяет указатели, а не материал. Сверка предъявленного с хранимым — дело проверяющего (часть П2), и только постоянным временем.

func NewLoginVerifier added in v0.5.0

func NewLoginVerifier(material string) (LoginVerifier, error)

NewLoginVerifier — материал как есть, ДОСЛОВНО: без обрезки и нормализации. Перенесённый хеш держит вход прежним паролем ровно пока он побайтово тот же (Р1 Ф1). Пустой материал отвергается.

func (LoginVerifier) Format added in v0.5.0

func (v LoginVerifier) Format(f fmt.State, verb rune)

Format — заглушка на ЛЮБОМ глаголе форматирования, включая `%x` и `%q`.

func (LoginVerifier) GoString added in v0.5.0

func (v LoginVerifier) GoString() string

GoString — заглушка и для `%#v`.

func (LoginVerifier) IsZero added in v0.5.0

func (v LoginVerifier) IsZero() bool

IsZero — материала нет.

func (LoginVerifier) LogValue added in v0.5.0

func (v LoginVerifier) LogValue() slog.Value

LogValue — заглушка для журнала.

func (LoginVerifier) MarshalJSON added in v0.5.0

func (v LoginVerifier) MarshalJSON() ([]byte, error)

MarshalJSON — отказ.

func (LoginVerifier) MarshalText added in v0.5.0

func (v LoginVerifier) MarshalText() ([]byte, error)

MarshalText — отказ.

func (LoginVerifier) Reveal added in v0.5.0

func (v LoginVerifier) Reveal() string

Reveal — ЕДИНСТВЕННЫЙ выход материала. Звать его вправе только места, перечисленные гейтом `TestLoginVerifierStaysInside`.

func (LoginVerifier) SameMaterial added in v0.5.0

func (v LoginVerifier) SameMaterial(other LoginVerifier) bool

SameMaterial — тот же ли материал у двух значений, сверкой постоянного времени и без выхода материала наружу (kaname#458, Р5): выдача входа сверяет материал, проверенный до транзакции, с хранимым после захвата строки личности. Нулевое значение не совпадает ни с чем, и с нулевым тоже: «материала нет» — не материал.

func (LoginVerifier) String added in v0.5.0

func (v LoginVerifier) String() string

String — заглушка.

type Membership

type Membership struct {
	ID        MembershipID
	AccountID AccountID
	// AccountName — ЗЕРКАЛО имени аккаунта, источник истины сам аккаунт.
	// Заполняется на чтении соединением в той же БД; пустое значение означает
	// «имя не задано», а НЕ «аккаунта нет».
	AccountName AccountName
	UserID      UserID
	State       MembershipState
	// InvitedBy — кто пригласил. Это и есть след приглашения, ПЕРЕЖИВАЮЩИЙ вход:
	// активация трогает состояние и отметку правки и не трогает это поле.
	// Пусто у членства, заведённого не приглашением.
	InvitedBy UserID
	// CreatedAt — когда членство заведено; для приглашения — когда оно выписано.
	CreatedAt time.Time
	// UpdatedAt — когда состояние менялось в последний раз.
	UpdatedAt time.Time
}

Membership — принадлежность человека аккаунту как ОТДЕЛЬНАЯ связь.

Ресурс ВЫХОДНОЙ: глаголов создания, правки и снятия у него на читающей поверхности нет. Заводит членство приглашение, снимает — исключение из аккаунта, и оба принадлежат потоку человека.

type MembershipID

type MembershipID string

MembershipID — идентификатор членства, дефис-форма `mbr-<17>`.

Он ВЫЧИСЛЯЕТСЯ из пары «человек × аккаунт» неизменяемой SQL-функцией без соли, а не чеканится: писателей строки членства больше одного, и все обязаны прийти к ОДНОЙ строке на одну пару. Отсюда два следствия, которые здесь названы, потому что они меняют решения читающего кода:

  • идентификатор ПЕРЕИСПОЛЬЗУЕТСЯ. Уникальность пары полная, снятие есть удаление строки, функция неизменяема — значит повторное приглашение того же человека в тот же аккаунт вернёт ТОТ ЖЕ идентификатор. Признак «строка заведена заново» — `CreatedAt`, а не `ID`;
  • идентификатор ВЫЧИСЛИМ ПОСТОРОННИМ. Зная человека и аккаунт, его получает кто угодно, ни разу не обратившись к платформе. Поэтому одиночное чтение обязано быть суженным по аккаунту в САМОМ ЗАПРОСЕ: без этого адрес чужого членства строится арифметикой, а не перебором.

type MembershipState

type MembershipState string

MembershipState — состояние членства. Значений ровно два, и словарь закреплён CHECK'ом на таблице: третьего в колонке не появится.

Согласованность состояния членства со статусом человека конструкцией НЕ закреплена — её держат писатели (зеркалящий триггер строки человека и два стейтмента репозитория). Читающий код поэтому судит то, что в строке лежит, и не выводит одно из другого.

const (
	// MembershipStatePending — человек приглашён и ещё не входил.
	//
	// Наблюдаемо АККАУНТ-СКОУПНЫМ чтением и не наблюдаемо своим списком: у
	// приглашённого нет внешнего субъекта, значит нечем аутентифицироваться,
	// значит своего чтения он вызвать не может. Это не расхождение проекций, а
	// арифметика — разные наблюдатели видят разное.
	MembershipStatePending MembershipState = "PENDING"
	// MembershipStateActive — человек состоит в аккаунте.
	MembershipStateActive MembershipState = "ACTIVE"
)

type MembershipTuple

type MembershipTuple struct {
	User     string // e.g. "user:usr-…", "group:grp-…#member"
	Relation string // tier relation (e.g. "editor")
	Object   string // e.g. "compute_instance:inst-1"
}

MembershipTuple — a per-object FGA relation tuple the reconciler emits/revokes for a materialized membership (subject → tier → object). A flat value so the reconcile use-case stays transport/storage-agnostic; the pg adapter maps it to the fga_outbox payload.

type MirrorObject

type MirrorObject struct {
	ObjectType      string
	ObjectID        string
	ParentProjectID string
	// ParentAccountIDs — АККАУНТЫ, под которыми лежит объект. Множественное
	// число здесь не запас на будущее: у ЛИЧНОСТИ принадлежность аккаунту
	// перестала быть свойством её строки и стала отдельной связью, которых у
	// человека бывает несколько (kacho#470/#471), поэтому скалярное поле называло бы
	// ОДИН аккаунт из многих — и выдача второго аккаунта не находила бы своего
	// человека by construction (kacho#1172).
	//
	// У всех прочих типов набор вырожден: ноль элементов (объект не лежит ни в
	// одном аккаунте) либо один. Пустые строки в набор НЕ КЛАДУТСЯ — «аккаунта
	// нет» выражается отсутствием элемента, а не элементом-пустышкой, иначе
	// область с пустым идентификатором совпала бы с ним.
	ParentAccountIDs []string
	Labels           map[string]string
}

MirrorObject — the same-DB parent-scope projection of an owner object read from resource_mirror (β fed it; γ reads it). Pure value; the reader adapter fills it.

func (MirrorObject) IsContainedIn

func (m MirrorObject) IsContainedIn(scope ScopeAnchor) bool

IsContainedIn reports whether a mirror object lies UNDER the given scope-anchor (the single containment predicate for byName AND byLabel — parity).

project:P ⊑ project:P                         (same project)
project:P ⊑ account:A  if A ∈ mirror.ParentAccountIDs
any       ⊑ cluster:*                          (cluster contains everything)

A cluster-scoped binding contains every registered object. The cluster id is not compared (there is a single cluster root in the FGA model).

This predicate is PURE (no DB): it trusts ParentAccountIDs to already carry the object's FULL account set. For a mirror-fed object registered with only its owning PROJECT, the reader adapter resolves the account through the project→account hierarchy same-DB (resource_mirror reader COALESCE) BEFORE filling ParentAccountIDs, so an account-scoped binding transitively contains an object nested in a project of its account even when the stored parent_account_id column was empty. The resolution is account-bounded (one project → one account), so this predicate never leaks across the account boundary.

ЧЛЕНСТВО ЗДЕСЬ — «хотя бы одно», и это не послабление. У личности аккаунтов столько, сколько у человека членств (kacho#1172): выдача аккаунта B накрывает его потому, что он состоит в B, и НИ ОДИН чужой аккаунт от этого его не накрывает — набор перечисляет ровно те аккаунты, связь с которыми существует строкой `kaname.memberships`.

Пустая область (scope.ID == "") не совпадает ни с чем: набор пустых строк не содержит by construction (см. комментарий поля), но проверка стоит здесь же — сравнение «пусто с пусто» иначе давало бы истину на объекте без аккаунта.

func (MirrorObject) MatchesLabels

func (m MirrorObject) MatchesLabels(matchLabels map[string]string) bool

MatchesLabels reports whether the mirror object's labels satisfy the AND-equality match set: for EVERY (k,v) in matchLabels the object must have labels[k]==v (superset allowed — the object may carry extra labels). This is the in-Go equivalent of the JSONB `labels @> matchLabels` probe used by the reader's SQL filter; kept here so the containment verdict is testable in pure domain and the reconciler can re-assert it on a candidate set.

type ModuleSet

type ModuleSet interface {
	// IsKnownModule — состоит ли модуль в наборе.
	IsKnownModule(module string) bool
}

ModuleSet — членство в наборе модулей платформы, каким его знает ВЫЗЫВАЮЩИЙ.

Подстановочный знак `*` членом набора не является ни в одной реализации: он не имя модуля, а маркер политики, и разбирается Rule.Validate отдельно.

func ModuleSetOf

func ModuleSetOf(names ...string) ModuleSet

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

Пустой перечень даёт набор, не признающий НИЧЕГО, и это не вырожденный случай, а тот же fail-closed, что у отсутствующего набора ниже: «перечень пуст» не есть «принимаем любой». Разница лишь в том, что здесь отказ приходит с именем модуля, а там — с указанием на непровязанный источник.

type OAuthClientName

type OAuthClientName string

OAuthClientName — человекочитаемое имя токена (SA-key / user-token). Судится единственной формой имени дерева, как и остальные имена iam.

Пустая строка остаётся законным ВХОДОМ выпуска и означает «назови сам»: до записи её заменяет имя, производное от идентификатора (`validate.NameOrDefault` в use-case). Здесь пустое уже не проходит — эта проверка судит то, что БУДЕТ ЗАПИСАНО, а записи с пустым именем не бывает.

func (OAuthClientName) Validate

func (n OAuthClientName) Validate() error

Validate — OAuthClientName судится той же формой, что и остальные имена.

Прежде здесь стояла ветка «пустое допустимо», и она была единственным местом iam, где имя доживало до записи пустым. Пустое имя — не «имя, которого нет», а ресурс, который не ищется, не отличается в списке и показывается прочерком. Ветка снята: пустое заменяется умолчанием ДО записи, в use-case выпуска.

type OperationID

type OperationID string

type PasswordCostClass added in v0.5.0

type PasswordCostClass struct {
	Format PasswordHashFormat
	Params map[PasswordHashCostParam]uint32
}

PasswordCostClass — формат и полный набор параметров стоимости этого формата.

func (PasswordCostClass) Key added in v0.5.0

func (c PasswordCostClass) Key() string

Key — ключ класса: формат и параметры в порядке, объявленном перечнем (`2a cost=12`, `argon2id memory=65536 iterations=3 parallelism=4`). Детерминирован: одна пара «формат, параметры» — один ключ, каким бы порядком ни обходилась карта.

func (PasswordCostClass) ParamsLabel added in v0.5.0

func (c PasswordCostClass) ParamsLabel() string

ParamsLabel — параметры одной строкой через запятую (`cost=12`, `memory=65536,iterations=3,parallelism=4`): значение метки наблюдаемости.

func (PasswordCostClass) Validate added in v0.5.0

func (c PasswordCostClass) Validate() error

Validate — формат из перечня; названы ВСЕ параметры стоимости формата и ни одного чужого. Неназванный параметр читался бы нулём, а класс с нулём в стоимости — не класс.

type PasswordHashCostParam added in v0.5.0

type PasswordHashCostParam string

PasswordHashCostParam — параметр СТОИМОСТИ формата: тот, что назначает цену одной проверки. Соль и длина ключа параметрами стоимости не являются и потолка не несут — они не меняют ни времени, ни памяти проверки.

const (
	// CostParamBcryptCost — показатель стоимости bcrypt: время растёт вдвое на
	// каждую единицу, память от него не зависит вовсе.
	CostParamBcryptCost PasswordHashCostParam = "cost"
	// CostParamArgon2Memory — память argon2id в КиБ: она же и есть память
	// одной проверки, которую страж старта сверяет с пределом процесса.
	CostParamArgon2Memory PasswordHashCostParam = "memory"
	// CostParamArgon2Iterations — число проходов argon2id.
	CostParamArgon2Iterations PasswordHashCostParam = "iterations"
	// CostParamArgon2Parallelism — степень параллельности argon2id.
	CostParamArgon2Parallelism PasswordHashCostParam = "parallelism"
)

type PasswordHashFormat added in v0.5.0

type PasswordHashFormat string

PasswordHashFormat — признак формата проверочного значения: первый сегмент его разметки, как он лежит в самом значении.

const (
	// PasswordHashFormatBcrypt — НАСЛЕДУЕМЫЙ формат: значения, заведённые
	// прежним поставщиком личности. Продукт их читает и не пишет.
	//
	// Признак — `2a`. Соседние написания bcrypt (`2b`, `2y`) в перечень НЕ
	// внесены: популяция их не несёт, а запись без предмета обещала бы
	// возможность, которой нет. Встретится такое значение — его назовёт
	// перепись переноса, громко и с числом строк, а не примет молча.
	PasswordHashFormatBcrypt PasswordHashFormat = "2a"

	// PasswordHashFormatArgon2id — ОБЪЯВЛЕННЫЙ и растущий формат: им заводится
	// каждое новое значение.
	PasswordHashFormatArgon2id PasswordHashFormat = "argon2id"
)

func (PasswordHashFormat) Admissibility added in v0.5.0

Admissibility — область допустимости параметра у этого формата.

func (PasswordHashFormat) CostParams added in v0.5.0

func (f PasswordHashFormat) CostParams() []PasswordHashCostParam

CostParams — параметры стоимости формата, в устойчивом порядке.

type PasswordHashFormatRecord added in v0.5.0

type PasswordHashFormatRecord struct {
	// Format — признак формата.
	Format PasswordHashFormat
	// Writable — пишет ли продукт значения этого формата.
	Writable bool
	// Ceiling — потолок по каждому параметру стоимости формата.
	Ceiling map[PasswordHashCostParam]uint32
	// Floor — пол; ТОЛЬКО у записываемой записи.
	Floor map[PasswordHashCostParam]uint32
	// Why — чем выбран потолок. Потолок выводится из ЁМКОСТИ, а не из переписи
	// одной установки, и причина обязана стоять рядом с числом.
	Why string
}

PasswordHashFormatRecord — запись перечня.

func PasswordHashFormatByMarker added in v0.5.0

func PasswordHashFormatByMarker(marker string) (PasswordHashFormatRecord, bool)

PasswordHashFormatByMarker — запись по признаку формата.

Второго исхода, кроме «нашлась» и «такой записи нет», у поиска НЕТ: корзины «прочее» перечень не имеет. Признак вне перечня — наша ошибка данных либо настройки, а не человека и не временный сбой соседа, и отвечать за него обязан отдельный ГРОМКИЙ исход проверяющего, а не запись-заглушка.

func PasswordHashFormats added in v0.5.0

func PasswordHashFormats() []PasswordHashFormatRecord

PasswordHashFormats — перечень целиком, КОПИЕЙ: вызывающий не может его расширить, а расширенный на месте перечень перестал бы быть закрытым.

func (PasswordHashFormatRecord) MemoryPerVerificationAtCeilingBytes added in v0.5.0

func (r PasswordHashFormatRecord) MemoryPerVerificationAtCeilingBytes() uint64

MemoryPerVerificationAtCeilingBytes — память, которую занимает ОДНА проверка значения на потолке этой записи. Это та величина, которую страж старта умножает на ёмкость и сверяет с пределом памяти процесса (PWV-15, 15.7).

ВЫВОДИТСЯ из потолка, а не выписывается рядом с ним: выписанная, она стала бы вторым местом об одном предмете и разошлась бы с потолком при первой же его правке — молча, потому что расхождение не наблюдаемо ничем.

func (PasswordHashFormatRecord) Validate added in v0.5.0

func (r PasswordHashFormatRecord) Validate() error

Validate — запись, годная к объявлению. Зовётся пробой перечня и гейтом дерева; самопроверяющаяся запись не даёт перечню разойтись с нормой молча.

type PasswordHashParamRange added in v0.5.0

type PasswordHashParamRange struct {
	// Min — наименьшее значение, которое читатель считает без искажения.
	Min uint32
	// Max — наибольшее. Где строже спецификация, берётся она; где строже
	// читатель — он.
	Max uint32
	// Why — чем граница обоснована. Запись без причины неотличима от записи
	// без предмета.
	Why string
}

PasswordHashParamRange — область ДОПУСТИМОСТИ параметра: пересечение того, что допускает спецификация формата, и того, что читатель исполняет без искажения. Обе границы включительны.

type Permission

type Permission string

func (Permission) Validate

func (p Permission) Validate() error

Validate — Permission: one element with wildcard semantics.

type Permissions

type Permissions []Permission

func CompileRules

func CompileRules(rules []Rule) (Permissions, error)

CompileRules deterministically compiles authored rules into the INTERNAL compiled permission set (4-segment `module.resource.resourceName.verb`), for FGA emission/Check-reuse. Pure + deterministic:

  • ARM_ANCHOR → `m.r.*.v` for each (resource, verb) over the rule's module.
  • ARM_NAMES → `m.r.<id>.v` for each (resource, id, verb) over the module.
  • ARM_LABELS → NOT compiled (excluded).
  • verb `*` → projection keeps the `*` verb segment (`m.r.*.*` / `m.r.<id>.*`), it does NOT expand the closed per-verb set (that is the FGA-emit concern). The projection holds `*`.

The result is de-duplicated (order-preserving over first occurrence) and capped at MaxCompiledPermissions; exceeding the cap → INVALID_ARGUMENT "compiled permissions exceed 1024" (not silent truncation, not INTERNAL).

The projection is system-context-independent (a system role's `*`-segments are authored verbatim into the rules and projected as-is), so CompileRules takes no systemCtx flag — the wildcard/system policy is enforced upstream by Rule.Validate.

func (Permissions) Validate

func (p Permissions) Validate() error

Validate — Permissions: cardinality 1..1024; each — Permission.Validate.

The ≥1 lower bound applies to the LEGACY permissions-only role path (a role authored as a bare permission set must carry at least one). Cap raised 256→1024 in lockstep with the DB CHECK iam_permissions_valid (migration 0025) and the proto (size) bound, so the compiled-permission set derived from a role's rules (CompileRules, ≤MaxCompiledPermissions) always passes domain + DB validation (acceptance R-12 / A-12). A rules-role uses ValidateCompiled (no lower bound) — a label-only role compiles to an EMPTY set by design (ARM_LABELS excluded).

func (Permissions) ValidateCompiled

func (p Permissions) ValidateCompiled() error

ValidateCompiled validates the INTERNAL compiled-permission projection of a rules-role: the 4-segment grammar parity + the ≤1024 cap, but WITHOUT the ≥1 lower bound. A rules-role whose rules are ALL ARM_LABELS compiles to an EMPTY permission set (matchLabels is not compiled — R-7 / fix #8), which is valid; the authority lives in rules[], not permissions[]. Only the legacy permissions-only path keeps the ≥1 floor (Validate). Acceptance A-10 (label-only positive).

type PrivilegeDerivation

type PrivilegeDerivation string

PrivilegeDerivation — how a subject obtained a privilege.

const (
	// DerivationDirect — the binding's subject IS the requested subject. Also the
	// zero value's meaning (see SubjectPrivilege.Derivation).
	DerivationDirect PrivilegeDerivation = ""
	// DerivationGroup — the binding's subject is a GROUP the requested subject is
	// a member of. Groups do not nest, so this is exactly one hop.
	DerivationGroup PrivilegeDerivation = "GROUP"
)

type Project

type Project struct {
	ID          ProjectID
	AccountID   AccountID
	Name        ProjectName
	Description Description
	Labels      Labels
	CreatedAt   time.Time
}

Project — child Account-а («folder» в YC-стилистике, но без промежуточного Cloud). Уникальное имя per-Account (DB UNIQUE projects_account_name_unique).

FK: account_id → accounts(id) ON DELETE RESTRICT. account_id — hard-immutable после Create (Update его reject'ит).

func (Project) StructuralFacts

func (p Project) StructuralFacts() []StructuralTuple

StructuralFacts returns the project's cluster and account pointers — both of which `project.super_admin: admin from account or any_admin from cluster` reads.

func (Project) Validate

func (p Project) Validate() error

type ProjectID

type ProjectID string

type ProjectName

type ProjectName string

func (ProjectName) Validate

func (n ProjectName) Validate() error

type PrunedSelectorType

type PrunedSelectorType struct {
	// ObjectType — точечный тип объекта, чью строку каталога сняли.
	ObjectType string
	// Outcome — что стало со строкой отбора, из которой тип вырезан.
	Outcome SelectorPruneOutcome
	// Reason — причина снятия строки каталога, записанная применителем
	// манифеста. Причина ПЛАТФОРМЫ, а не действие арендатора: своё правило он не
	// менял. ПУСТАЯ строка означает «строка каталога причины не несла» (снята
	// ранней миграцией), а не «причину потеряли».
	Reason string
	// PrunedAt — момент вырезания. Время ТРАНЗАКЦИИ применения: у всего, что
	// вырезано одним применением, он совпадает дословно.
	PrunedAt time.Time
	// AppliedBy — АВТОР применения, вырезавшего этот тип (kacho#2005). Та же величина
	// и тот же источник, что у соседа: вопрос «кто снял» у обеих ведомостей
	// общий — арендатор не различает, какой из проекций правила он лишился.
	//
	// ПУСТАЯ строка означает «строка вырезана ДО заведения колонки».
	AppliedBy string
}

PrunedSelectorType — точечный тип, ВЫРЕЗАННЫЙ применителем каталога из отбора правила роли при снятии строки ресурса платформы (kacho#1988).

Чем это отличается от соседней [WithdrawnGrant], и почему они не сложены

Переселение адресуется парой «тип объекта + глагол» и означает «право по этой паре действовало и перестало». Вырезание глагола не имеет ВООБЩЕ: отбор правила называет типы, а не пары, — поэтому у него своя ведомость и свой тип здесь. Сложить их значило бы дать вырезанному пустой глагол, а пустой глагол в соседе есть ЯКОРЬ объявления правила, то есть уже занятое значение.

Отпечаток правила сюда НЕ ПЕРЕЕЗЖАЕТ

Ведомость ключуется тройкой «роль + отпечаток правила + тип»: отпечаток нужен ХРАНЕНИЮ, чтобы два правила, потерявшие один тип, не схлопнулись в одну строку. Читающему он не нужен и не отдаётся — это содержательный хеш, которого нет ни в одном контракте платформы; выставь мы его, способ хеширования стал бы частью контракта, а арендатор своё правило знает содержанием, а не дайджестом.

Следствие названо прямо: строки, различавшиеся ТОЛЬКО отпечатком, для читающего есть ОДИН факт и произносятся один раз.

type PublicReadContainer added in v0.5.0

type PublicReadContainer struct {
	// Type — точечный тип, о котором ответ (kaname#874): по нему ворота находят
	// вложенные правила, стоящие под этим типом.
	Type string
	// Declared — тип объявлен контейнером публичного чтения.
	Declared bool
	// ChildTypes — точечные типы живых строк каталога, чей указатель называет
	// этот тип, отсортированно. Пусто — детей у типа нет либо каталог их не знает.
	ChildTypes []string
}

PublicReadContainer — что живой каталог говорит о типе цели выдачи классу.

func (PublicReadContainer) CoveredChild added in v0.5.0

func (c PublicReadContainer) CoveredChild(rules Rules) (string, bool)

CoveredChild — первый (в порядке ChildTypes) тип-ребёнок, чтение которого роль открывает ЦЕЛИКОМ (Rule.ReadsWholeType); ok=false — такого ребёнка нет.

Читаются ОБЕ формы (kaname#874, M1 круга 1): правило верхнего уровня над ребёнком (исход SC-65 соседа не меняется) и вложенное правило под правилом над ЭТИМ типом (ND-05 (в)). Прежде ворота читали только верхний уровень, и роль, открывающая детей вложенным правилом, проходила на необъявленный контейнер молча.

type PublishedKey

type PublishedKey struct {
	KID          KeyID
	Algorithm    SigningAlgorithm
	PublicKeyPEM string
}

PublishedKey — ПУБЛИКУЕМАЯ форма ключа.

Поля приватной половины у этого типа НЕТ и быть не может (F1-05): положить её сюда не выражается, а не «запрещено правилом».

type RecoveryCode added in v0.5.0

type RecoveryCode struct {
	ID     RecoveryCodeID
	UserID UserID
	Digest CodeDigest
	// IssuedAt — момент чеканки (часы полосы).
	IssuedAt time.Time
	// ExpiresAt — абсолютный срок, ОДИН столбец; величина — настройка.
	ExpiresAt time.Time
	// ConsumedAt — момент применения; nil — не применён. Ставит его один
	// оператор базы (Ф5-05).
	ConsumedAt *time.Time
}

RecoveryCode — запись кода (Р1). Состав закрыт: личность, свёртка, момент выдачи, срок, момент применения.

func (RecoveryCode) Expired added in v0.5.0

func (c RecoveryCode) Expired(now time.Time) bool

Expired — истёк ли срок на данный момент. Граница включающая: в сам момент срока кода уже нет.

func (RecoveryCode) Validate added in v0.5.0

func (c RecoveryCode) Validate() error

Validate — запись, годная к выдаче: судится до базы то, что база держит ограничениями.

type RecoveryCodeID added in v0.5.0

type RecoveryCodeID string

RecoveryCodeID — идентификатор ПОТОКА восстановления: строка кода и ключ идемпотентности журнала завершений (Р4). Наружу не адресуется.

type RecoveryCodeValue added in v0.5.0

type RecoveryCodeValue struct {
	// contains filtered or unexported fields
}

RecoveryCodeValue — значение кода (Р1). Тип не печатается и не сериализуется; значение выходит ровно двумя методами: `Digest` (в хранилище) и `Letter` (в письмо).

func NewRecoveryCodeValue added in v0.5.0

func NewRecoveryCodeValue() (RecoveryCodeValue, error)

NewRecoveryCodeValue — свежий код из криптографического источника случайности: каждый знак — независимый выбор из алфавита.

func PresentedRecoveryCode added in v0.5.0

func PresentedRecoveryCode(value string) RecoveryCodeValue

PresentedRecoveryCode — код, как его прислал человек: регистр и разделители (пробелы, дефисы) не значимы, знаки-двойники приводятся к своим. Пустое после нормализации — отсутствие кода.

func (RecoveryCodeValue) Digest added in v0.5.0

func (v RecoveryCodeValue) Digest() CodeDigest

Digest — свёртка, по которой хранилище находит строку. Хранится и сравнивается только она.

func (RecoveryCodeValue) Format added in v0.5.0

func (v RecoveryCodeValue) Format(f fmt.State, verb rune)

Format — заглушка на любом глаголе форматирования.

func (RecoveryCodeValue) GoString added in v0.5.0

func (v RecoveryCodeValue) GoString() string

GoString — заглушка и для `%#v`.

func (RecoveryCodeValue) IsZero added in v0.5.0

func (v RecoveryCodeValue) IsZero() bool

IsZero — кода нет.

func (RecoveryCodeValue) Letter added in v0.5.0

func (v RecoveryCodeValue) Letter() string

Letter — форма для письма: две группы через дефис. Единственный выход к человеку.

func (RecoveryCodeValue) LogValue added in v0.5.0

func (v RecoveryCodeValue) LogValue() slog.Value

LogValue — заглушка для журнала.

func (RecoveryCodeValue) MarshalJSON added in v0.5.0

func (v RecoveryCodeValue) MarshalJSON() ([]byte, error)

MarshalJSON — отказ.

func (RecoveryCodeValue) MarshalText added in v0.5.0

func (v RecoveryCodeValue) MarshalText() ([]byte, error)

MarshalText — отказ.

func (RecoveryCodeValue) String added in v0.5.0

func (v RecoveryCodeValue) String() string

String — заглушка.

type RecoveryCompletion

type RecoveryCompletion struct {
	RecoveryJTI         string
	ExternalID          ExternalSubject
	UserID              UserID
	RevokedSessionCount int32
}

RecoveryCompletion — one row of the recovery-completed idempotency ledger. PK recovery_jti dedups at-least-once delivery. The row stores the deterministic primary user_id and the revoked session count so a duplicate delivery can replay the same Operation.metadata without re-running any side-effect.

Источника события ДВА (Ф5 Р4): обратный вызов поставщика личности называет внешнего субъекта; наш поток восстановления (Ф5, `kacho#1271`) называет человека его строкой, а внешнего субъекта не несёт — у личности, заведённой нашей регистрацией, его нет. Поэтому `ExternalID` необязателен; заданный по-прежнему ограничен длиной.

func (RecoveryCompletion) Validate

func (r RecoveryCompletion) Validate() error

Validate — self-validating domain entity. Length bounds mirror the migration CHECK constraints + the proto field annotations.

type ResourceRef

type ResourceRef struct {
	Type string
	ID   string
}

ResourceRef — пообъектный указатель {type,id} (redesign-2026 F8 / B1). В отличие от общего Referrer имени НЕ несёт: цель iam — строгая форма {type,id}. `Type` — точечный ключ `<module>.<resource>`.

type ResourceType

type ResourceType string

ResourceType — ЯКОРЬ ОБЛАСТИ привязки: один из трёх ярусов иерархии (`cluster`/`account`/`project`). Вокабуляр объявлен ОДИН раз — `scopeAnchorTiers` в access_binding_scope.go, — и `Validate()` судит по нему. Пообъектный тип якорем не бывает: он живёт на оси `target` (F8).

func (ResourceType) Validate

func (r ResourceType) Validate() error

Validate — ResourceType — ярус иерархии, и только он.

Судит ЕДИНСТВЕННОЕ объявление вокабуляра якоря (`scopeAnchorTiers`), а не своя копия: копия здесь была, разошлась с ним на 20 записей и принимала пообъектные типы, которых не производит ни один путь записи. Разбор — у самого объявления.

type RevocationReason added in v0.7.0

type RevocationReason string

RevocationReason — закрытый словарь причин отзыва выдачи (kaname#963, Д2). Написание — слово хранения (`access_bindings.revocation_reason`).

const (
	// RevocationReasonNone — выдача не отозвана либо отозвана до заведения поля.
	RevocationReasonNone RevocationReason = ""
	// RevocationReasonRevoke — глагол `Revoke`.
	RevocationReasonRevoke RevocationReason = "REVOKE"
	// RevocationReasonExpired — истёк срок, отозвал проход срока.
	RevocationReasonExpired RevocationReason = "EXPIRED"
	// RevocationReasonResourceUnregistered — снята регистрация объекта цели.
	RevocationReasonResourceUnregistered RevocationReason = "RESOURCE_UNREGISTERED"
	// RevocationReasonDelegationRevoked — отозвано делегирование, по которому
	// выдача сделана (kaname#986, Р4).
	RevocationReasonDelegationRevoked RevocationReason = "DELEGATION_REVOKED"
)

type Role

type Role struct {
	ID          RoleID
	ClusterID   ClusterID // set for system role
	AccountID   AccountID // set for account-scoped custom
	ProjectID   ProjectID // set for project-scoped custom
	Name        RoleName
	Description Description
	// Rules — authored policy (RBAC rules-model 2026). Source of truth + public
	// API surface. Compiled into Permissions (internal, FGA-emit) by CompileRules.
	Rules Rules
	// Permissions — INTERNAL compiled form (anchor/names arms; match_labels NOT
	// compiled). Derived from Rules via CompileRules; NOT a public API field for
	// rules-roles. Legacy permissions-only roles (no Rules) keep their stored set.
	Permissions Permissions
	IsSystem    bool
	// OwnerModule — модуль, которому роль принадлежит. Пусто у ПЛАТФОРМЕННОЙ
	// роли (admin/edit/view/owner, system.*), непусто у роли, объявленной
	// манифестом этого модуля.
	//
	// Носит ровно ОДИН смысл — владение, — и потому отделяет послабление
	// подстановки от кластерного якоря: `IsSystem` продолжает означать «арендатор
	// эту роль не правит» и больше не означает «этой роли можно подставлять
	// звёздочку». Политика выводится из пары ОДНИМ местом — [PolicyOfRole].
	//
	// Наружу не проецируется: `Role` публичного контракта этой колонки не несёт
	// (задача продукта kacho#1032, тип изменения — ВВОДЯЩЕЕ, без нового поля API).
	OwnerModule string
	CreatedAt   time.Time
	// CreatedByUserID — authoring principal (governance/audit). Optional.
	CreatedByUserID UserID
	// UpdatedAt — last-mutation timestamp. Zero until first Update.
	UpdatedAt time.Time
	// Labels — tenant-facing метки САМОГО ресурса Role (НЕ путать с
	// Rule.MatchLabels, отбирающим объекты под грантом). Делают Role
	// label-selectable наравне с account/project (ARM_LABELS-грант на iam.role →
	// v_list по `labels @> matchLabels`; List фильтрует viewer ∪ v_list).
	Labels Labels
	// Integrity — целость роли: даёт ли она то, что объявляет (kacho#1035).
	//
	// ВЫВОДИТСЯ НА ЧТЕНИИ и НЕ ХРАНИТСЯ: колонки у неё нет, в перечень колонок
	// писателя она не входит и [Role.Validate] её не судит. Нулевое значение
	// означает «этим путём не вычислено» — так её и видят пути, которые роль не
	// читают, а возвращают эхом мутации.
	Integrity RoleIntegrity

	// Lifecycle — ОБЪЯВЛЕНА роль манифестом модуля сегодня либо СНЯТА (kacho#1913).
	//
	// Отдельно от `Integrity` рядом, и различие несущее: `RoleHealthEmpty` даёт
	// и снятая роль, и объявленная, чьи строки каталога сняты, — а следующий шаг
	// у арендатора разный. Довод целиком — [RoleLifecycle].
	//
	// ПРИХОДИТ ЧТЕНИЕМ: колонки пометки у строки есть, но заполняют это поле
	// `Get` и `List`, а ответ операции его не несёт — нулевое состояние там
	// означает «этим ответом не вычислено», ровно как у соседа.
	Lifecycle RoleLifecycle

	// Withdrawn — ЧТО у роли отобрано и почему (kacho#1992): строки ведомости
	// переселения. ОБЪЯСНЯЕТ состояние целости и не определяет его — у роли,
	// пострадавшей третьим путём, переселения не было вовсе, и список пуст при
	// нездоровом состоянии.
	//
	// Пустой срез означает «отобранного нет», а не «не читали»: разводит их
	// [RoleIntegrity.Health] рядом, у которого нулевое состояние есть «этим
	// ответом не вычислено».
	Withdrawn []WithdrawnGrant

	// PrunedSelectorTypes — какие точечные типы ВЫРЕЗАНЫ из отбора правил этой
	// роли и почему (kacho#1988): строки ведомости вырезания.
	//
	// Сосед [Role.Withdrawn] отвечает про ДВЕ проекции правила, у которых есть
	// пара «тип + глагол»; эта — про ТРЕТЬЮ, где глагола нет вовсе. Разные
	// ведомости, разные события, поэтому два поля, а не одно.
	//
	// Пустой срез означает «вырезанного нет», а не «не читали»: разводит их
	// [RoleIntegrity.Health] рядом, у которого нулевое состояние есть «этим
	// ответом не вычислено».
	PrunedSelectorTypes []PrunedSelectorType

	// RuleStates — состояние КАЖДОГО правила роли (kacho#1962): действует, отозвано
	// платформой либо не разрешилось. Записей ровно по числу правил.
	//
	// Отвечает на вопрос, которого нет ни у `Integrity`, ни у `Withdrawn`: КАКОЕ
	// правило пострадало и по КАКОЙ ИЗ ДВУХ причин. Картина счётчиков у причин
	// ОДИНАКОВА, а следующий шаг арендатора — разный.
	//
	// ПРИХОДИТ ЧТЕНИЕМ, НЕ ХРАНИТСЯ — тем же доводом, что `Integrity` рядом.
	// ПУСТОЙ СРЕЗ означает «этим путём не вычислено» и законен: ответ операции
	// состояния не несёт. У роли БЕЗ правил он пуст и на чтении — состояние есть
	// свойство правила, и у роли без правил его нет.
	RuleStates []RuleState

	// TypeVerbs — набор глаголов ТИПА, каким его объявляет ЖИВАЯ строка каталога
	// (kacho#1994). Тем же набором идёт материализация, поэтому превью и эмиссия не
	// могут разойтись.
	//
	// ПРИХОДИТ ПРОВЯЗКОЙ, НЕ ХРАНИТСЯ — как и `Integrity` рядом: колонки у неё
	// нет, писатель её не пишет, [Role.Validate] её не судит. Отличие от
	// `Integrity` в том, чем является НУЛЕВОЕ значение: там оно означает «этим
	// путём не вычислено» и законно, здесь — ОТКАЗ проекции. Показ по словарю,
	// порождённому сборкой, неотличим от честного превью, поэтому «нечем
	// ответить» обязано быть отказом, а не тихим запасным путём.
	TypeVerbs TypeVerbLookup

	// HeldNames — НЕ ХРАНИТСЯ. Ключи объектов, названные в `resourceNames`
	// правил, которые на приёме роли были зарегистрированы (Р8 приёмки
	// `unregistration-revokes-its-grants.md`). Запись роли берёт на каждом замок
	// строки регистрации и отказывает, если строки уже нет: роль, принятая до
	// снятия и записанная после, снятого ключа не называет (URG-26).
	HeldNames []RuleNameHold
}

Role — multi-scope. Exactly one scope field is non-NULL:

  • is_system=true + ClusterID set: system role (`system.admin`, ...).
  • is_system=false + AccountID set: account-scoped custom role.
  • is_system=false + ProjectID set: project-scoped custom role.

Enforced by DB CHECK `roles_definition_tier_xor` + a partial UNIQUE per scope. Domain.Validate duplicates the CHECK to give friendly errors before reaching the DB. (The legacy B2B-tenant role scope was removed; a custom role is scoped to exactly one of {account, project}.)

func (Role) AuthoredVerbs

func (r Role) AuthoredVerbs(lookup TypeVerbLookup) []string

AuthoredVerbs is the deduped, canonically-ordered union of the role's rule verbs (`*` expands to the verb set of the addressed type). Empty for a label-only / rules-less role.

func (Role) CanonicalRank

func (r Role) CanonicalRank() int

CanonicalRank returns the ordering rank of a canonical system role (0=viewer … 3=owner) so the catalog can present the four first among system roles; a non-canonical role sorts after them.

func (Role) DefinitionTierID

func (r Role) DefinitionTierID() string

DefinitionTierID returns the anchor object id of the role's definition tier (cluster / account / project id), matching DefinitionTierType.

func (Role) DefinitionTierType

func (r Role) DefinitionTierType() string

DefinitionTierType returns the dotted definition-tier type of the role: iam.cluster for a system role (cluster_id set), iam.account / iam.project for a custom role. Empty when no anchor is set (should not happen under the XOR CHECK).

func (Role) DisplayName

func (r Role) DisplayName() string

DisplayName is the friendly catalog label. For a canonical system role it is the curated name (Viewer/Editor/Admin/Owner); otherwise it defaults to the role name.

func (Role) EffectiveVerbs

func (r Role) EffectiveVerbs(lookup TypeVerbLookup) []string

EffectiveVerbs is AuthoredVerbs plus the editor `delete*` qualifier for an editor-tier role.

func (Role) GrantsReadsOnly added in v0.5.0

func (r Role) GrantsReadsOnly() bool

GrantsReadsOnly — каждый глагол роли — `get` либо `list` (Р4, SC-21).

Судится АВТОРСКОЕ написание, а не раскрытый набор: глагол `*` раскрывается в набор ТИПА, который растёт вместе с каталогом, и роль, читающая сегодня, завтра писала бы — поэтому `*` пишущим считается всегда. Роль без правил судится по своей хранимой проекции прав (`<модуль>.<ресурс>.<…>.<глагол>`), и в ней действует то же правило.

То же правило держит база (`kaname.role_grants_reads_only`) — на пути записи, где у выдачи и правки роли последнее слово (ban #10); здесь оно ради синхронного отказа до заведения операции.

func (Role) IsClusterAdminRole

func (r Role) IsClusterAdminRole() bool

IsClusterAdminRole reports whether r is THE system cluster-admin superuser role.

It is identified by its PINNED deterministic id (ClusterAdminRoleID `admin` or SystemAdminRoleID `system.admin`) AND is_system AND (defence-in-depth) that it still carries the full `*.*.*` ARM_ANCHOR rule (module:*, `*` resource, `*` verb). Matching by id — NOT by the bare `*.*.*` shape — is load-bearing for #8: the `owner` system role (OwnerRoleID, migration 0035) carries the SAME `*.*.*` shape, so a shape-only predicate would misclassify owner as cluster-admin and let an owner@GLOBAL+all binding slip past the A-05 reject. owner is auto-bound at ACCOUNT scope only and is NOT the GLOBAL+all exception.

This is the ONLY role for which a GLOBAL+all binding is legal (A-05c) — its binding is materialized as the D-9 cluster-relation, not per-object.

func (Role) IsSystemDerived

func (r Role) IsSystemDerived() bool

IsSystemDerived reports whether the role is a system role, DERIVED from its definition tier (tierType == iam.cluster) rather than a stored provenance flag (redesign-2026 F4). Equivalent to ClusterID != "".

func (Role) Purpose

func (r Role) Purpose() string

Purpose is the one-line description of a canonical system role; empty otherwise.

func (Role) Validate

func (r Role) Validate(modules ModuleSet) error

Validate — multi-scope XOR formula + rules/permissions.

A role is valid when EITHER it carries authored Rules (the rules-model 2026 authority) OR a legacy compiled Permissions set (back-compat read of pre-rules roles).

When Rules is set (a rules-role) it is validated through Rules.Validate with the policy derived from the row (PolicyOfRole of IsSystem + OwnerModule) and the compiled Permissions projection is validated for the 4-seg grammar + cap ONLY — NOT the ≥1 lower bound (ValidateCompiled): a label-only role (all rules ARM_LABELS) compiles to an EMPTY permission set by design and must be accepted. The ≥1 floor is retained for the LEGACY permissions-only path (no Rules) so a degenerate legacy role with an empty set cannot exist. `modules` — набор модулей платформы, каким его знает вызывающий: правило роли называет модуль, а домен закрытого набора не объявляет (module_set.go).

func (Role) VerbNotes

func (r Role) VerbNotes(lookup TypeVerbLookup) map[string]string

VerbNotes returns the per-verb clarifications for the effective preview. Only the editor `delete*` qualifier carries a note today.

func (Role) WithoutComputedState

func (r Role) WithoutComputedState() Role

WithoutComputedState — копия роли без вычисленного состояния.

Приёмник — ЗНАЧЕНИЕ, и это не стиль: вызывающий переводит роль в ответ операции и продолжает пользоваться своей, поэтому проекция обязана быть неразрушающей.

TypeVerbs НЕ обнуляется намеренно: у него нулевое значение означает не «не вычислено», а ОТКАЗ проекции (см. комментарий поля), и путь мутации его заполняет сам — превью глаголов роль несёт и в ответе операции.

type RoleHealth

type RoleHealth uint8

RoleHealth — состояние целости роли.

Нулевой вариант несёт ЕДИНСТВЕННЫЙ смысл: «этим ответом не вычислено». Он не является ни одним из трёх исходов и HealthOf его не возвращает никогда — иначе «посчитано, терять нечего» стало бы неотличимо от «не считали».

const (
	// RoleHealthUnknown — не вычислено ЭТИМ ответом. НИКОГДА не «здорова».
	RoleHealthUnknown RoleHealth = iota
	// RoleHealthHealthy — каждый адресуемый сегмент имеет строку проекции.
	RoleHealthHealthy
	// RoleHealthDegraded — часть сегментов проекции не даёт, часть даёт.
	RoleHealthDegraded
	// RoleHealthEmpty — сегменты объявлены, проекции нет НИ ОДНОЙ.
	RoleHealthEmpty
)

type RoleID

type RoleID string

func SystemRoleID

func SystemRoleID(name RoleName) RoleID

SystemRoleID — идентификатор системной роли по её ИМЕНИ.

Имя роли берётся ДОСЛОВНО: приведение имени к другому написанию перед хешированием (змеиное к камельному и обратно) даёт другой идентификатор и разрывает выданные права. Имя роли и имя ресурса — разные словари, и применитель их не сводит (приёмка §3.7).

type RoleIntegrity

type RoleIntegrity struct {
	Health RoleHealth
	// Declared — сколько адресуемых сегментов объявляют правила роли.
	Declared int
	// Unresolved — сколько из них не дают ни одной строки проекции.
	Unresolved int
}

RoleIntegrity — состояние целости и пара величин, из которых оно выведено.

Счётчики без состояния были бы двусмысленны: ноль есть И законная величина (роль без адресуемых сегментов), И признак «не считали». Разводит их RoleHealth: у вычисленной целости состояние непустое всегда.

func HealthOf

func HealthOf(declared, unresolved int) RoleIntegrity

HealthOf выводит состояние из пары величин. ЕДИНСТВЕННОЕ место, где решается, какое состояние несёт роль.

Роль без адресуемых сегментов (подстановка в модуле и ресурсе — форма `*.*` администратора кластера, либо унаследованная роль без правил) читается ЗДОРОВОЙ, а не пустой: терять ей нечего, и тревога на ней была бы ложной. Прибор, чьи находки ложны, перестают читать — вместе с настоящими находками.

type RoleLifecycle

type RoleLifecycle struct {
	State RoleLifecycleState
	// RetiredAt — момент снятия. Нулевое время у объявленной роли.
	RetiredAt time.Time
	// RetiredReason — причина снятия. Без неё «отобрали» неотличимо от
	// «сломалось».
	RetiredReason string
	// RetiredBy — кто снял. Сегодня производитель один — путь старта, и он
	// называет процессного актора; глагол применения придёт с `kacho#1034` и назовёт
	// проверенную личность вызывающего. Пустая строка у снятой роли означает
	// «помечено до заведения колонки», а не «автора потеряли».
	RetiredBy string
}

RoleLifecycle — состояние роли и обстоятельства её снятия.

Три сопутствующие величины непусты ТОЛЬКО у снятой роли, и это свойство СХЕМЫ, а не кода: `roles_live_matches_retired` делает состояние «снята и жива» неконструируемым.

func (RoleLifecycle) Withdrawn

func (l RoleLifecycle) Withdrawn() bool

Withdrawn — снята ли роль. Отдельный вопрос от `State != Declared`: у невычисленного состояния ответ «нет», и он верен — утверждать снятие по молчанию нельзя.

type RoleLifecycleState

type RoleLifecycleState uint8

RoleLifecycleState — объявлена роль манифестом либо снята.

Нулевой вариант несёт ЕДИНСТВЕННЫЙ смысл: «этим ответом не вычислено». Он не является ни одним из двух исходов, и путь чтения его не возвращает никогда — иначе «прочитано, роль объявлена» стало бы неотличимо от «не читали».

const (
	// RoleLifecycleUnknown — не вычислено ЭТИМ ответом. НИКОГДА не «объявлена».
	RoleLifecycleUnknown RoleLifecycleState = iota
	// RoleLifecycleDeclared — строка жива: объявление за ней стоит.
	RoleLifecycleDeclared
	// RoleLifecycleWithdrawn — строка помечена снятой: объявления больше нет.
	RoleLifecycleWithdrawn
)

func (RoleLifecycleState) String

func (s RoleLifecycleState) String() string

String — состояние словом. Для журналов и текстов отказа, не для контракта.

type RoleName

type RoleName string

func (RoleName) Validate

func (n RoleName) Validate() error

Validate — форма имени роли БЕЗ различения яруса: годна любая из двух. Оставлена для вызывающих, которым ярус неизвестен; сама сущность судится `ValidateAtTier`, потому что ограничений в таблице ДВА и каждое условлено вычисляемым `is_system`.

func (RoleName) ValidateAtTier

func (n RoleName) ValidateAtTier(isSystem bool) error

ValidateAtTier — форма имени роли ПО ЯРУСУ, зеркало двух ограничений таблицы.

Различать обязательно: в базе стоят ДВА условленных ограничения (`roles_custom_name_check` под `is_system OR …` и `roles_system_name_check` под `NOT is_system OR …`), и правило, не различающее ярусов, расходится с ними в обе стороны сразу — принимает точечное имя у пользовательской роли и отвергает его у системной. Отказ тогда приезжает от базы (SQLSTATE 23514) без имени поля и без координаты.

type RoleRetirement

type RoleRetirement struct {
	// Marked — строка роли помечена снятой. Ложно означает, что оператор пометки
	// не нашёл своей строки: роль уже снята либо владелец не тот.
	Marked bool
	// ResettledVerbs — строк проекции глаголов переселено в ведомость и снято.
	ResettledVerbs int
	// ResettledRuleRefs — строк проекции сегментов переселено и снято.
	ResettledRuleRefs int
	// RemovedSelectors — строк проекции отбора снято. В ведомость они не
	// переселяются: у строки селектора нет пары «тип + глагол», которой
	// адресуются сироты.
	RemovedSelectors int
	// RemovedTargetMembers — строк материализованного состава цели снято.
	RemovedTargetMembers int
}

RoleRetirement — перепись ОДНОГО отзыва роли: что унесено и что помечено.

Числа возвращаются, а не логируются внутри: вызывающий собирает из них перепись применения, а «снято» без чисел неотличимо от «прошло мимо».

type RoleRuleRef

type RoleRuleRef struct {
	Module   string
	Resource string
	// Verb — пустая строка означает ЯКОРЬ (глаголы не сужены), а не отсутствие
	// значения: см. абзац выше.
	Verb string
}

RoleRuleRef — один ОБЪЯВЛЕННЫЙ сегмент правила роли: «эта роль называет вот этот (модуль, ресурс) и вот этот глагол».

Чем это НЕ является — и различие несущее

Это НЕ `RoleVerb`. Та проекция отвечает на вопрос вердикта «разрешено ли действие» и содержит только то, что РЕЗОЛВИТСЯ: тип, которого не знает словарь модели, пар не даёт (`RoleVerbsFromSelectors`). Здесь наоборот — строка кладётся на КАЖДЫЙ объявленный сегмент, резолвится он или нет, потому что предмет этой таблицы есть ссылочная целостность: молчаливый пропуск и есть тот дефект, ради которого заводится ключ, и воспроизвести его в новом писателе значило бы завести ключ, которому нечего отвергать.

Пустой глагол — это ЯКОРЬ, а не «глагол не задан»

Правило, не сузившее глаголы (`verbs: ["*"]`), даёт строку с пустым `Verb`, которая ложится в хранилище значением NULL. Ключ ресурса на ней проверяется, ключ глагола — пропускается `MATCH SIMPLE`, и это ПРАВИЛЬНО: ресурс уже проверен первым ключом. Ключей поэтому два, а не один: под одним составным ключом `MATCH SIMPLE` снял бы проверку целиком, и правило, называющее несуществующий ресурс, принималось бы успешно.

func RuleRefsOf

func RuleRefsOf(rules Rules) []RoleRuleRef

RuleRefsOf — объявленные сегменты правил в форме строк проекции.

Источник — АВТОРСКОЕ правило, а не селекторы: селекторы уже прошли через словарь модели и потеряли то, что он не знает, — то есть ровно те сегменты, ради которых ключ и заводится. Подстановка `*` в ресурсе и модуле сегментов не даёт: она называет не имя, а «все», и адресовать ею строку каталога нечего (системная роль с `*.*` материализуется коротким замыканием администратора кластера).

func (RoleRuleRef) Dotted

func (r RoleRuleRef) Dotted() string

Dotted — точечная форма имени типа (`vpc.network`), та самая, какой говорят `role_verb.object_type`, `role_rule_selectors.object_types` и колонка `catalog_resource.dotted` под своим CHECK.

ЕДИНСТВЕННОЕ место, где эта склейка пишется в Go: второе написание разошлось бы с первым молча, а соединение по разным словарям не совпадает никогда — и отличить это от «права нет» было бы нечем.

func (RoleRuleRef) IsAnchor

func (r RoleRuleRef) IsAnchor() bool

IsAnchor — правило не сузило глаголы.

type RoleScopeGroup

type RoleScopeGroup int8

RoleScopeGroup — server-computed scope tier of a role, surfaced to the UI in AssignableRole.scope_group so the picker groups without client-side logic Maps 1:1 to the proto ScopeGroup enum.

const (
	RoleScopeGroupUnspecified RoleScopeGroup = 0
	RoleScopeGroupSystem      RoleScopeGroup = 1
	RoleScopeGroupAccount     RoleScopeGroup = 2
	RoleScopeGroupProject     RoleScopeGroup = 3
)

func ScopeGroupOf

func ScopeGroupOf(r Role) RoleScopeGroup

ScopeGroupOf derives the scope tier from the role's scope columns. System roles are SYSTEM; otherwise the non-empty custom scope decides (account → ACCOUNT, project → PROJECT).

func (RoleScopeGroup) String

func (g RoleScopeGroup) String() string

String — debug rendering (matches the proto enum names).

type RoleSegment

type RoleSegment struct {
	RoleID     RoleID
	ObjectType string
	// Verb — пустая строка означает ЯКОРЬ (правило не сузило глаголы): годится
	// любая строка проекции своего типа.
	Verb string
}

RoleSegment — объявленный сегмент роли в форме, которой спрашивают проекцию.

Точечный тип приходит ОДНИМ выражением (RoleRuleRef.Dotted) — тем же, каким его строит переселение и каким его хранит каталог. Второе написание разошлось бы молча: соединение по разным словарям не совпадает никогда.

func SegmentsOf

func SegmentsOf(id RoleID, rules Rules) []RoleSegment

SegmentsOf — объявленные сегменты роли в форме вопроса к проекции.

type RoleVerb

type RoleVerb struct {
	ObjectType string
	Verb       string
}

RoleVerb — одна пара проекции «роль даёт этот глагол на этом типе».

Тип — тип объекта модели прав (`vpc_network`), не точечное имя ресурса: вердикт спрашивает именно им. Глагол — в канонической форме и БЕЗ приставки отношения: приставку знает компилятор модели, и дублировать её здесь значило бы завести второе место, где она может смениться.

type RotatedRefreshToken added in v0.5.0

type RotatedRefreshToken struct {
	Context    CeremonyContext
	Generation int32
}

RotatedRefreshToken — то, что вернул ОДИН оператор ротации.

type Rule

type Rule struct {
	Module        string // exactly one module per rule
	Resources     []string
	Verbs         []string
	ResourceNames []string
	MatchLabels   map[string]string
	// Children — вложенные правила (kaname#874, nested_rule.go): правила над
	// типами, лежащими под типом-контейнером этого правила. nil — вложенных нет;
	// пустой непустой срез формой не является (второй способ сказать «нет»).
	// У вложенного правила Module пуст (берётся у владельца) и своих Children нет.
	Children []Rule
}

Rule — one authored grant over ONE module. The element lists are validated 1..16 each, the selector is resource_names XOR match_labels.

func (Rule) Arm

func (r Rule) Arm() Arm

Arm reports the selector arm of the rule (resource_names XOR match_labels).

func (Rule) EffectiveChildren added in v0.5.0

func (r Rule) EffectiveChildren() []Rule

EffectiveChildren — вложенные правила с модулем правила-владельца, как их читают проекции (селекторы, сегменты, «только чтение»). Хранимая и контрактная форма модуля у вложенного правила не несёт.

func (Rule) Fingerprint

func (r Rule) Fingerprint() string

Fingerprint returns the content-hash of a rule (rule_fp). It is order-stable: the element lists and matchLabels keys are sorted before hashing, so the SAME rule authored in a different field order produces the SAME fp. The arm marker is folded in so an ARM_NAMES and an ARM_LABELS rule over the same modules/resources/verbs never collide.

The digest covers EVERY semantic field of the rule (module, resources, verbs, resource_names, match_labels) so a change to any of them — including a single label value or one verb — yields a different fp (the reorder/remove invariant rests on this).

func (Rule) OwnerType added in v0.5.0

func (r Rule) OwnerType() string

OwnerType — точечный тип правила-владельца (`<модуль>.<ресурс>`); пусто у правила, которое владельцем быть не может (не ровно один ресурс либо `*`).

func (Rule) ReadsWholeType added in v0.5.0

func (r Rule) ReadsWholeType(dotted string) bool

ReadsWholeType — правило называет тип `dotted` и открывает его чтение целиком ([readsWhole]). Им судят ворота выдачи классу (SC-65).

func (Rule) Validate

func (r Rule) Validate(policy RulePolicy, modules ModuleSet) error

Validate enforces the rule form. The wildcard relaxation is carried by RulePolicy — a single value derived from the row by PolicyOfRole — rather than by a boolean "system context": the module tier needs a THIRD policy, not a second boolean (see rule_policy.go). Errors carry the stable texts so the API contract is preserved.

`modules` — НАБОР МОДУЛЕЙ, каким его знает вызывающий (см. module_set.go). Он приходит параметром, потому что домен закрытого набора не объявляет: на пути запроса это ЖИВЫЕ строки каталога, у оснастки дерева — канон. Отсутствие набора отвергается самой строгой ветвью: «не знаю» не есть «можно».

type RuleLabelSelector

type RuleLabelSelector struct {
	RuleFP      string
	ObjectTypes []string
	MatchLabels map[string]string
	Verbs       []string
}

RuleLabelSelector — the reconciler-facing projection of ONE ARM_LABELS rule: the rule_fp it is keyed by, the dotted object types it selects (cartesian modules×resources), the matchLabels equality selector, and the authored verbs (which drive the per-object tier + v_* relations the reconciler emits — the tier is NOT taken from the role's compiled permissions, since ARM_LABELS rules are excluded from CompileRules). It carries no pgx/grpc — pure domain.

type RuleLifecycle

type RuleLifecycle uint8

RuleLifecycle — состояние ОДНОГО правила роли.

Нулевой вариант несёт ЕДИНСТВЕННЫЙ смысл: «этим ответом не вычислено». RuleStatesOf его не возвращает никогда — иначе «посчитано, терять нечего» стало бы неотличимо от «не считали».

const (
	// RuleLifecycleUnknown — не вычислено ЭТИМ ответом. НИКОГДА не «действует».
	RuleLifecycleUnknown RuleLifecycle = iota
	// RuleLifecycleActive — ни один адресуемый сегмент правила не потерян.
	RuleLifecycleActive
	// RuleLifecycleWithdrawn — потерянные сегменты есть, и КАЖДЫЙ объяснён
	// ведомостью переселения: объявление сняла платформа.
	RuleLifecycleWithdrawn
	// RuleLifecycleUnresolved — потерянные сегменты есть, и ХОТЯ БЫ ОДИН не
	// объяснён: правило ссылается на то, чего сейчас нет.
	RuleLifecycleUnresolved
)

type RuleNameHold added in v0.7.0

type RuleNameHold struct {
	Rule int
	Ref  ResourceRef
}

RuleNameHold — ключ объекта, названный правилом `Rule` (индекс в `Rules`).

type RulePolicy

type RulePolicy struct {
	// contains filtered or unexported fields
}

RulePolicy — политика послабления подстановки в правилах роли.

Почему политик ТРИ, а не две (задача продукта kacho#1032)

До неё политика была одна булева — «системный контекст». Признак `is_system` вычисляется ровно из непустого кластерного якоря и несёт ДВА смысла сразу: «арендатор эту роль не правит» и «этой роли можно подставлять звёздочку». Базовая роль модуля обязана быть системной в первом смысле — иначе её правит арендатор, — и вместе с ним получала второй: прямой путь к `*.*.*`. В диффе это выглядит обычной строкой роли, и ни один обзор её не поймает.

Политики различает ВЛАДЕЛЕЦ, а не ярус:

арендаторская   якоря кластера нет            module: "*" ✗   ресурс "*" ✗
модульная       якорь есть, владелец назван   module: "*" ✗   ресурс "*" ✓ только в своём модуле
платформенная   якорь есть, владельца нет     module: "*" ✓   ресурс "*" ✓

Одно правило, а не три исключения: подстановка в роли с владельцем законна ровно в пределах её модуля. Отсюда обе строки механически — `module: "*"` не находится ни в одном модуле, поэтому отвергается всегда; ресурс `*` находится в модуле своего правила, поэтому законен ровно тогда, когда этот модуль и есть владелец.

Глагол `*` не затрагивается — решение, а не пропуск

Он разрешён и в арендаторской роли, безусловно ([validateVerbs]): он не сегмент пространства имён, а «все действия названного типа». Сузить его здесь значило бы отобрать уже выданное под видом починки.

Носитель — ЗНАЧЕНИЕ, а не второй булев флаг

Два булевых параметра дают четыре сочетания при трёх законных, и четвёртое («не системная, но с владельцем») пришлось бы отвергать четвёртым правилом. Политика передаётся одним значением закрытого перечня, выводимым из строки функцией ОДНОГО места — PolicyOfRole. Второе объявление «системная ли роль для целей подстановки» есть находка, и её ловит гейт дерева.

func PolicyOfRole

func PolicyOfRole(isSystem bool, ownerModule string) RulePolicy

PolicyOfRole выводит политику ИЗ СТРОКИ — единственное место в дереве, где это делается.

`isSystem` берётся оттуда же, откуда его берёт база: из непустого кластерного якоря. `ownerModule` пуст у платформенной роли и непуст у роли, объявленной манифестом модуля.

Сочетание «не системная, но с владельцем» законным не является: владельца пишет только применитель ролей модуля, а он объявляет роль системной безусловно. Такой вход политику получает АРЕНДАТОРСКУЮ — самую строгую: послабление выдаётся по доказанному признаку, а не по частично совпавшему.

func TenantPolicy

func TenantPolicy() RulePolicy

TenantPolicy — арендаторская политика в чистом виде.

Нужна вызывающему, у которого строки роли нет вовсе: загрузчик манифеста судит правила ДО того, как роль станет строкой, и обязан судить их самой строгой политикой. Порядок — часть контракта (сценарий IAM-OM-1-18): отказ приходит от загрузчика с координатой строки YAML, а не от применителя, у которого координаты нет.

func (RulePolicy) OwnerModule

func (p RulePolicy) OwnerModule() string

OwnerModule — модуль-владелец роли; пусто у арендаторской и платформенной.

func (RulePolicy) UpdateImpliesDelete added in v0.7.0

func (p RulePolicy) UpdateImpliesDelete() bool

UpdateImpliesDelete — влечёт ли правка удаление на листовом типе (`authzmap.GrantedVerbsWithDeclared`, правило «правка влечёт удаление»).

Почему у модульной роли — нет (kaname#1006)

Правило заведено для ролей, чей набор глаголов пишет человек в плоской модели: редактор, получивший правку, без удаления не убрал бы за собой свой же объект. Роль модуля устроена иначе — её классы объявляет манифест модуля, и объявляет ТОЧНО: модуль, желающий дать удаление, называет его классом (так устроена роль-администратор модуля), а роль, назвавшая только правку, удалять не вправе. Добавленное здесь удаление было бы правом, которого модуль не объявлял.

Нулевая политика ответа «да» не получает: собранная не через PolicyOfRole политика судится самой строгой ветвью — меньше прав, а не больше.

type RuleSelector

type RuleSelector struct {
	RuleFP        string
	Arm           Arm
	ObjectTypes   []string
	ResourceNames []string          // ARM_NAMES only
	MatchLabels   map[string]string // ARM_LABELS only
	Verbs         []string
	// Container — контейнер вложенного правила (kaname#874); nil у правила
	// верхнего уровня. Селектор с контейнером охватывает объекты своих
	// ObjectTypes, у которых среди предков есть объект типа Container.Type,
	// прошедший отбор Container и входящий в цель выдачи.
	Container *ContainerSelector
	// Grants — глаголы, которые ЭТО правило даёт на каждом своём типе (ключ —
	// точечный тип из ObjectTypes), по живому каталогу (kaname#878). Решение о
	// доступе даёт глагол на объекте только тем правилом, чей селектор объект
	// выбрал: глагол соседнего правила той же роли охвата этого правила не
	// получает.
	//
	// nil — НЕ ВЫЧИСЛЕНО (заполняет `catalog.Facts.GrantSelectors`), и писатель
	// селекторов такую строку отвергает: молча записанное «правило не даёт
	// ничего» было бы отказом, неотличимым от честного. Пустая карта — вычислено:
	// живой каталог не даёт правилу ни одного глагола.
	//
	// Карта — функция ОТПЕЧАТКА правила (RuleFP — хэш всего содержимого правила,
	// включая глаголы и вложенные), каталога и владельца роли (политика роли
	// решает, влечёт ли правка удаление, kaname#1006): от прочих правил роли она
	// не зависит.
	Grants map[string][]string
}

RuleSelector — the UNIFIED reconciler-facing projection of ONE materializing rule (RBAC explicit-model 2026). It carries the arm so the reconciler picks the match strategy: ARM_ANCHOR(all) → every object of ObjectTypes inside scope; ARM_NAMES → only ResourceNames; ARM_LABELS → labels @> MatchLabels. The per-object FGA tuples (v_* + tier) are derived from Verbs (the tier is NOT taken from the role's compiled permissions — ARM_LABELS/ARM_ANCHOR-materialized rules are not in CompileRules). Pure domain, no pgx/grpc.

func (RuleSelector) PublicReadsChildOf added in v0.5.0

func (s RuleSelector) PublicReadsChildOf(containerType string, children []string) bool

PublicReadsChildOf — селектор вложенного правила под контейнером `containerType`, читающий ЦЕЛИКОМ одного из его прямых детей `children`. Им судит разворачивание выдачи классу, писать ли `public_reader` (ND-03). Правило верхнего уровня (прежняя форма) `public_reader` не пишет (ND-07 (б)).

func (RuleSelector) ReadsWholeType added in v0.5.0

func (s RuleSelector) ReadsWholeType(dotted string) bool

ReadsWholeType — то же правило в форме селектора материализации ([readsWhole]). Им судит разворачивание выдачи, когда решает, писать ли `public_reader`.

type RuleState

type RuleState struct {
	// RuleIndex — индекс правила в `Role.Rules`. КЛЮЧ записи: порядок самих
	// записей не значим, сверять надо по нему.
	//
	// Индекс законен как ключ потому, что `roles.rules` — массив JSONB под одним
	// кодеком, и порядок сохраняется by construction: правило читается тем же
	// индексом, каким записано. Действителен он в пределах ОДНОГО ответа:
	// `Update` перестраивает массив, и мутацию индекс не переживает.
	RuleIndex int
	// State — состояние правила. У вычисленного оно непусто ВСЕГДА.
	State RuleLifecycle
	// Segments — сколько АДРЕСУЕМЫХ сегментов объявляет это правило.
	// Ноль законен (подстановка в модуле или ресурсе сегментов не даёт).
	//
	// ЕДИНИЦА ДРУГАЯ, ЧЕМ У [RoleIntegrity.Declared], и потому имя другое:
	// счётчик роли дедуплицирует сегмент по ВСЕЙ роли, здесь — только внутри
	// правила. Равенство сумм поэтому НЕ ГАРАНТИРОВАНО и нарушается ровно
	// тогда, когда один сегмент назван двумя правилами; на прочих ролях суммы
	// совпадают, и это совпадение, а не инвариант.
	Segments int
	// Lost — сколько из объявленных сегментов этого правила не дают ни одной
	// строки проекции, которую читает вердикт.
	//
	// ПРЕДИКАТ ТОТ ЖЕ, что у [RoleIntegrity.Unresolved]: ведомость здесь не
	// спрашивается — её спрашивает [RuleState.Explained] рядом. Различает
	// величины только единица дедупликации.
	Lost int
	// Explained — сколько из потерянных ОБЪЯСНЕНЫ ведомостью переселения.
	// Необъяснённые выводятся вычитанием: третье поле, равное разности двух
	// соседних, разошлось бы с ними при первой же правке.
	Explained int
	// Children — состояния вложенных правил ЭТОГО правила (kaname#874), в
	// порядке `Rule.Children`; RuleIndex у записи — индекс в Children. Пусто у
	// правила без вложенных.
	Children []RuleState
}

RuleState — состояние одного правила роли и три величины, из которых оно выведено.

Почему слово И счётчики, а не одно из двух

Правило, у которого часть потерь объяснена, а часть нет, читается RuleLifecycleUnresolved — слово называет состояние, ТРЕБУЮЩЕЕ разбора. Схлопыванием это не является ровно потому, что рядом стоят обе величины: `Withdrawn` и `Unresolved` видны одновременно, и ни одна не потеряна.

Четвёртое значение («частично отозвано») рассмотрено и отвергнуто: у него нет ни своего действия у арендатора, ни своего производителя, при том что счётчики уже несут ровно ту же величину.

func RuleStatesOf

func RuleStatesOf(rules Rules, unresolved []RoleSegment, withdrawn []WithdrawnGrant) []RuleState

RuleStatesOf — состояние каждого правила роли. ЕДИНСТВЕННОЕ место, где решается, какое состояние несёт правило.

Вход:

  • rules — авторские правила роли;
  • unresolved — сегменты роли, не давшие НИ ОДНОЙ строки проекции, которую читает вердикт. Источник состояния;
  • withdrawn — ведомость переселения. Только ОБЪЯСНЯЕТ: судить состояние по ней значило бы читать роль, пострадавшую вторым путём, действующей.

Записей в результате ровно `len(rules)`, и `RuleIndex` каждой указывает на своё правило.

type Rules

type Rules []Rule

Rules — a role's authored policy. Cardinality 1..64.

func DecodeRules

func DecodeRules(raw []byte) (Rules, error)

DecodeRules decodes the roles.rules JSONB payload into domain Rules. An empty/nil payload yields nil (a legacy permissions-only role, rules='[]').

func OwnerRoleRules

func OwnerRoleRules() Rules

OwnerRoleRules is the canonical authored policy of the `owner` system-role: `[{module:"*", resources:["*"], verbs:["*"]}]` (the `*.*.*` "selector all" shape). It MUST stay byte-for-byte semantically in lockstep with the migration 0035 seed (`rules` JSONB column). Exposed so the seed layer can derive the owner role's materializing selectors (role_rule_selectors) for the forward fast-path WITHOUT re-encoding the wildcard-expansion type set in SQL.

func (Rules) CoversType

func (rs Rules) CoversType(dottedType string) bool

CoversType сообщает, дают ли авторские правила роли глаголы на точечный тип `<module>.<resource>` (redesign-2026 F9, ворота 3 / IAM-1-24). Правило покрывает тип, когда совпадает Module, а Resources перечисляет ресурс (или "*"). Пустые правила не покрывают ничего.

func (Rules) HasAnchorRule

func (rs Rules) HasAnchorRule() bool

HasAnchorRule reports whether any rule in the set uses the ARM_ANCHOR selector (selector=all: neither resource_names nor match_labels). An ARM_ANCHOR rule materializes over ALL instances under scope — on a GLOBAL (cluster) scope that is the cluster-wide per-object set Q-2/A-05 forbids for ordinary roles.

func (Rules) HasChildren added in v0.5.0

func (rs Rules) HasChildren() bool

HasChildren — у роли есть хотя бы одно вложенное правило.

func (Rules) LabelSelectors

func (rs Rules) LabelSelectors() []RuleLabelSelector

LabelSelectors projects a role's rules to the ARM_LABELS selectors the reconciler materializes. ARM_ANCHOR / ARM_NAMES rules are excluded (they emit at Create-time, not via the reconciler). The dotted types are `{module}.<resource>` over each label rule's resources (the module is scalar — no module unroll; wildcards are already rejected on a label rule by Rule.Validate, so no `*` reaches here in a well-formed role).

Retained as the ARM_LABELS-only view (back-compat). RBAC explicit-model 2026 routes the reconciler through MaterializingSelectors (ALL arms); use that for the unified materializer.

func (Rules) MaterializingSelectors

func (rs Rules) MaterializingSelectors() []RuleSelector

MaterializingSelectors projects a role's rules to the UNIFIED selector set the reconciler materializes — ARM_ANCHOR(all) + ARM_NAMES + ARM_LABELS. Binding-time scope_grant emission is removed; the reconciler is the single path.

This is the SCOPE-AGNOSTIC (role-level) projection used to persist role_rule_selectors (the forward fast-path JOIN index): a wildcard `*.*` rule is EXPANDED to the full materializable type set so a freshly-registered object fast-path-matches an owner binding. The per-binding scope gate (MaterializingSelectorsInScope, consumed by the reconciler's LoadBinding) still prevents a GLOBAL/CLUSTER binding from per-object materializing, so expanding the role-level index is safe — the index over-includes, the binding scope narrows.

func (Rules) MaterializingSelectorsInScope

func (rs Rules) MaterializingSelectorsInScope(scope Scope) []RuleSelector

MaterializingSelectorsInScope is the SCOPE-AWARE projection the reconciler uses to compute a binding's desired membership (LoadBinding). A wildcard `*.*` rule is expanded to the full materializable type set ONLY for a BOUNDED scope (ACCOUNT/PROJECT) — per-object owner content. For a GLOBAL/CLUSTER scope the wildcard yields NO ObjectTypes: cluster super-admin is the flat short-circuit, never per-object. A non-wildcard rule is scope-independent (its dotted types are explicit).

func (Rules) NamedKeys added in v0.7.0

func (rs Rules) NamedKeys() []RuleNameHold

NamedKeys — каждый ключ объекта, который правила роли называют в `resourceNames`, вместе с индексом правила: тип — точечное имя `<module>.<resource>`, вложенное правило берёт модуль владельца. Подстановка конкретного типа не называет.

func (Rules) ScopeSelfVerbs

func (rs Rules) ScopeSelfVerbs(scopeResource string, typeVerbs []string) []string

ScopeSelfVerbs returns the UNION of authored verbs the role's rules grant on the binding's OWN scope resource-type — i.e. on the scope object itself (`account:<X>`/`project:<X>`/`cluster:<X>`). RBAC explicit-model 2026 P4 (D-7 / КФ-3 / C-01): a rules-role bound on a scope must materialize its tier (+ verb- bearing v_*) ON THE SCOPE ANCHOR ITSELF — the write-authz anchor / self-access that the removed binding-time scope_grant/anchor emit produced. The reconciler is now the SINGLE materialization path, so this projection feeds a scope-self desired member (reconcile.desiredRuleMembers), NOT a binding-time emit.

A rule contributes its verbs when EITHER its (module,resource) is the FULL `*.*` wildcard (the system superuser shape — migration 0031: admin/edit/view) OR its (module,resource) is exactly ("iam", scopeResource) — e.g. an `iam.account` rule on an account-scoped binding. scopeResource is the scope's resource type: "account"|"project". (cluster has no per-resource iam rule; only the `*.*` superuser shape grants cluster-self — handled by the wildcard branch.)

Returns nil when no rule applies to the scope self (a content-only role — e.g. `compute.instance` rules — grants nothing ON the account/project object, only on its content; the scope-self member is then absent, fail-closed).

func (Rules) Validate

func (rs Rules) Validate(policy RulePolicy, modules ModuleSet) error

Validate validates the rule set: cardinality 1..64; each rule self-valid.

The policy is a single value derived from the row by PolicyOfRole; see rule_policy.go for why there are three of them and not two. `modules` — набор модулей вызывающего, см. module_set.go.

func (Rules) WithoutResourceName added in v0.7.0

func (rs Rules) WithoutResourceName(dottedType, id string) (out Rules, removed int, changed bool)

WithoutResourceName возвращает правила роли без ключа объекта (`dottedType`, `id`): id перестаёт называться на типе `dottedType` в каждом правиле и каждом вложенном правиле, которое называет его на этом типе.

КЛЮЧ — ПАРА (тип, id), А ПЕРЕЧЕНЬ ИМЁН ОДИН НА ВСЕ ТИПЫ ПРАВИЛА. Правило над `resources: [a, b]` с `resourceNames: [N]` даёт и (a, N), и (b, N); снятие (a, N) обязано отнять только первое — (b, N) другой объект (Д1, URG-05). Поэтому правило, называющее снятый тип вместе с другими, расщепляется: прежнее правило остаётся над остальными типами с прежними именами, а снятый тип уходит в своё правило с именами без id. Правило над одним снятым типом просто теряет имя.

ПУСТОЙ ПЕРЕЧЕНЬ ИМЁН НЕ ХРАНИТСЯ. Правило без имён — ветвь `anchor`, то есть все объекты типа в области; хранить его значило бы расширить доступ ровно там, где снятие обязано его сузить. Поэтому правило верхнего уровня, у которого имён не осталось, снимается (`removed` считает правила, исчезнувшие целиком), а вложенное — снимается из `Children` (правило-владелец остаётся).

ПРЕДЕЛ ЧИСЛА ПРАВИЛ. Расщепление добавляет правило. Если правил роли (или вложенных правил одного владельца) после него стало бы больше предела (`MaxRules`, `maxChildren`), снятый id вычёркивается из общего перечня этого правила целиком — сужение вместо расширения: доступ к (b, N) теряется, но снятый ключ не остаётся названным, и роль не выходит за предел, который её чтение отвергло бы.

Тип правила — точечное имя `<module>.<resource>`; у вложенного правила модуль берётся у владельца. Правило над подстановкой (`*`) имён не несёт (`Validate`) и не трогается. Правило с меткой (`MatchLabels`) имён не несёт.

`changed = false` — ни одно правило ключ не называло; вызывающий тогда роль не пишет вовсе (сужения нет, строки аудита нет — URG-22, URG-37).

type SAOAuthClientID

type SAOAuthClientID string

SAOAuthClientID — новый формат `soc<17-crockford>` (corelib `ids.NewID`, без подчёркивания). id существующих строк immutable (id = имя клиента + JWK kid), поэтому валидатор принимает и прежнюю форму `soc_<17-crockford>`.

func (SAOAuthClientID) Validate

func (id SAOAuthClientID) Validate() error

type Scope

type Scope int8

Scope — anchor tier for an AccessBinding.

const (
	ScopeUnspecified Scope = 0
	ScopeCluster     Scope = 1
	ScopeAccount     Scope = 2
	ScopeProject     Scope = 3
)

func DeriveFromResourceType

func DeriveFromResourceType(resourceType string) Scope

DeriveFromResourceType — best-effort fallback for code paths that have resource_type but no explicit Scope (e.g. legacy callers that pre-date the W4 scope plumbing). Mirrors the BEFORE INSERT trigger of the schema; that the two agree is held by a probe, not by this sentence — см. §«Третье место» у [scopeVocabulary].

Вид вне словаря даёт ярус проекта — самый узкий из трёх, то есть ошибка умолчания не расширяет доступ.

func (Scope) String

func (s Scope) String() string

String — debug-friendly rendering (matches proto enum names).

func (Scope) ValidateAgainst

func (s Scope) ValidateAgainst(resourceType, resourceID string) error

ValidateAgainst checks that the Scope is consistent with the binding's (resource_type, resource_id). Returns ErrScopeMismatch if not.

CLUSTER ⇒ resource_type='cluster', resource_id='cluster_root' ACCOUNT ⇒ resource_type='account', resource_id starts with 'acc' PROJECT ⇒ resource_type='project', resource_id starts with 'prj'

Виды и их требования к идентификатору берутся из [scopeVocabulary]; своего перечисления здесь нет — оно было и разошлось бы с картой молча.

type ScopeAnchor

type ScopeAnchor struct {
	Type string
	ID   string
}

ScopeAnchor — the resource the binding is scoped to (its containment anchor). Type is one of "project" | "account" | "cluster"; ID is the resource id.

type SeedIdentityRename added in v0.3.0

type SeedIdentityRename struct {
	// Previous — написание применённого свода. Его продолжают присылать
	// непереведённые манифесты.
	Previous string
	// Declared — написание, объявленное §2.3 приёмки.
	Declared string
	// Subject — ЧИТАТЕЛЬ, ради которого запись существует. Без него запись
	// неотличима от послабления «на всякий случай».
	Subject string
}

SeedIdentityRename — одно переименование посевной идентичности: как её звали в применённом своде и как зовут по объявленному решению.

func SeedIdentityWindow added in v0.3.0

func SeedIdentityWindow() []SeedIdentityRename

SeedIdentityWindow возвращает окно целиком. Копия — чтобы вызывающий не мог расширить приём, правя чужой срез.

type SelectorPruneOutcome

type SelectorPruneOutcome uint8

SelectorPruneOutcome — что стало со строкой ОТБОРА, из которой вырезан тип.

Величины разделены намеренно: строка, потерявшая последний живой тип, снята целиком — правило перестало отбирать вовсе; строка, у которой живой тип остался, укорочена — правило сузилось. Для разбирающего последствия это события разного рода, и сложив их, мы потеряли бы ровно это различие.

const (
	// SelectorPruneOutcomeUnknown — исход не прочитан ЭТИМ ответом. Никогда не
	// означает «исход неизвестен»: ведомость несёт его всегда, закрытым набором
	// на уровне схемы (`role_selector_prune_outcome_known`).
	SelectorPruneOutcomeUnknown SelectorPruneOutcome = iota
	// SelectorPruneOutcomeShortened — живой тип у строки остался, она укорочена:
	// правило продолжает отбирать, но уже меньше.
	SelectorPruneOutcomeShortened
	// SelectorPruneOutcomeDropped — живого типа не осталось, строка снята
	// целиком: правило не отбирает ничего.
	SelectorPruneOutcomeDropped
)

type ServiceAccount

type ServiceAccount struct {
	ID          ServiceAccountID
	AccountID   AccountID
	Name        SvcAccountName
	Description Description
	Enabled     bool // default true
	CreatedAt   time.Time
	// Labels — tenant-facing метки. Делают ServiceAccount label-selectable
	// наравне с account/project (ARM_LABELS-грант на iam.serviceAccount → v_list
	// по `labels @> matchLabels`; List фильтрует viewer ∪ v_list).
	Labels Labels
}

ServiceAccount — Account-scoped (account_id FK ON DELETE RESTRICT).

Проектной области у служебной учётки нет: поле `project_id` и его колонка сняты — заполнить их было нечем (ни запроса, ни записи, ни выборки), а claim, который из них выводился, не читал никто. Понадобятся проектные служебные учётки — они заводятся отдельной подсистемой со своей приёмкой.

func (ServiceAccount) MayAuthenticate

func (s ServiceAccount) MayAuthenticate() bool

MayAuthenticate reports whether this service account is allowed to obtain a token or a fresh credential. It is the single predicate every issuance path asks, the machine counterpart of InviteStatus.MayAuthenticate for users — so that no path can re-derive its own answer and quietly disagree with the rest.

It reads a field, which means it is only as truthful as the query that populated the struct: `enabled` is a bool, false in every zero value, so a read that does not select the column makes this method answer "no" for every account in existence. Callers therefore judge a row they actually read, and the reads that feed them are pinned by their own tests.

func (ServiceAccount) Validate

func (s ServiceAccount) Validate() error

type ServiceAccountID

type ServiceAccountID string

type ServiceAccountOAuthClient

type ServiceAccountOAuthClient struct {
	ID              SAOAuthClientID
	SvaID           ServiceAccountID
	Description     Description
	CreatedByUserID UserID
	CreatedAt       time.Time
	ExpiresAt       *time.Time
	LastUsedAt      *time.Time

	// PublicKeyPEM — SPKI-encoded ECDSA P-256 public key of the key pair; the
	// signature of `client_assertion` is checked against it. Empty for FEDERATED
	// and SECRET rows: the first presents the external issuer's key, the second
	// carries no key material at all.
	PublicKeyPEM string
	// KeyAlgorithm — JOSE alg of the key pair. One of {"ES256", "RS256",
	// "EdDSA"}; new private_key_jwt keys always set "ES256"; federated and
	// secret rows leave it empty.
	KeyAlgorithm string

	// TrustedSubjects — федеративный вид ключа. Непустой перечень означает, что
	// удостоверение предъявляет ВНЕШНИЙ издатель по RFC 7521/7523: своего
	// ключевого материала строка не несёт вовсе, а подпись проверяется ключом
	// издателя из НАШЕГО перечня доверенных издателей (задача kacho#1124), который
	// читает проверяющий утверждения на пути запроса.
	//
	// Каждый элемент сужает, какая внешняя пара `(iss, sub)` вправе выступать
	// за этот ключ. Пустой перечень — обычный вид с ключевым материалом.
	TrustedSubjects []TrustedSubject

	// DeclaredAudiences — сужение адресатов, ОБЪЯВЛЕННОЕ заказчиком при выдаче
	// (`IssueSAKeyRequest.audience`, задача kacho#1136). Create-only, как и всё
	// остальное на этом ресурсе: глагола правки у ключа нет.
	//
	// Перечней в тракте выдачи ДВА, и этот — внутренний: он говорит, для чего
	// заведён ЭТОТ ключ, и действует ВНУТРИ перечня, объявленного посадкой.
	// Расширить внешнюю границу он не может ничем.
	//
	// Пустой перечень означает «сужения не объявлено», а не «любой адресат»:
	// внешняя граница остаётся и требуется непустой стражем старта выдачи.
	DeclaredAudiences []string

	// CredentialKind — вид удостоверения. ЗАПИСЫВАЕТСЯ при вставке; читателем
	// не вычисляется и из состава прочих полей не выводится.
	CredentialKind CredentialKind
	// SecretHash — sha256 по идентификатору строки И секретной части вместе,
	// 32 байта. Непуст ТОЛЬКО у вида SECRET. Сам секрет не хранится нигде: он
	// существует только в теле ответа, полученного вызывающим выдачи.
	SecretHash []byte

	// Name — человекочитаемое имя ключа, выставляется на Issue (create-only,
	// immutable — ресурс несёт только Issue/List/Revoke). Пусто для legacy-строк.
	Name OAuthClientName
	// Labels — произвольные метки ключа, выставляются на Issue (create-only,
	// immutable). Пусто для legacy-строк.
	Labels Labels
}

ServiceAccountOAuthClient — ключ служебной учётки (Class A workload identity).

Клиентом ключ называется по идентификатору ЭТОЙ строки: им подписывается `client_assertion`, по нему наш реестр утверждений разрешает клиента, и им же ключ называют докерная полоса и отзыв. Второго имени у ключа нет (kaname#362): имя клиента у прежнего внешнего издателя снято вместе со столбцом, где оно лежало.

private_key_jwt: служба чеканит пару ключей ECDSA P-256 на каждый ключ, хранит открытую половину (SPKI PEM) и алгоритм и отдаёт закрытую половину вызывающему ровно один раз. Общего секрета у этого вида нет.

func (ServiceAccountOAuthClient) Validate

func (c ServiceAccountOAuthClient) Validate() error

type SessionBearer added in v0.5.0

type SessionBearer struct {
	// contains filtered or unexported fields
}

SessionBearer — непрозрачное значение носителя (Р1). Тип не печатается и не сериализуется, как `LoginVerifier`; значение выходит ровно двумя методами: `Digest` (в хранилище) и `CookieValue` (клиенту).

func NewSessionBearer added in v0.5.0

func NewSessionBearer() (SessionBearer, error)

NewSessionBearer — свежий носитель из криптографического источника случайности.

func PresentedSessionBearer added in v0.5.0

func PresentedSessionBearer(value string) SessionBearer

PresentedSessionBearer — носитель, как его прислал клиент. Значение не разбирается: единственное, что с ним делают, — берут дайджест. Пустое — отсутствие носителя.

func (SessionBearer) CookieValue added in v0.5.0

func (b SessionBearer) CookieValue() string

CookieValue — значение для `Set-Cookie`. Единственный выход к клиенту.

func (SessionBearer) Digest added in v0.5.0

func (b SessionBearer) Digest() BearerDigest

Digest — свёртка носителя, по которой хранилище находит запись. Хранится и сравнивается только она.

func (SessionBearer) Format added in v0.5.0

func (b SessionBearer) Format(f fmt.State, verb rune)

Format — заглушка на любом глаголе форматирования.

func (SessionBearer) GoString added in v0.5.0

func (b SessionBearer) GoString() string

GoString — заглушка и для `%#v`.

func (SessionBearer) IsZero added in v0.5.0

func (b SessionBearer) IsZero() bool

IsZero — носителя нет.

func (SessionBearer) LogValue added in v0.5.0

func (b SessionBearer) LogValue() slog.Value

LogValue — заглушка для журнала.

func (SessionBearer) MarshalJSON added in v0.5.0

func (b SessionBearer) MarshalJSON() ([]byte, error)

MarshalJSON — отказ.

func (SessionBearer) MarshalText added in v0.5.0

func (b SessionBearer) MarshalText() ([]byte, error)

MarshalText — отказ.

func (SessionBearer) String added in v0.5.0

func (b SessionBearer) String() string

String — заглушка.

type SessionRevocation

type SessionRevocation struct {
	TokenJTI     string
	RevokedAt    time.Time
	Reason       string
	UserID       UserID
	TTLExpiresAt time.Time
}

SessionRevocation — fast lookup table keyed by token_jti (PK). A cron cleanup deletes rows past ttl_expires_at (`DeleteExpired`, `pg/audit_session_revocation_repos.go`). Migration `0001_initial.sql`.

Номер миграции здесь был неверен — стояло 0013, а это про снятие перечня условий обхода. Правится вместе с тем же дефектом у соседа (`audit_outbox_entry.go`): радиус берётся по МЕХАНИЗМУ — «номер миграции, названный в шапке сущности», — а не по файлу, где дефект заметили.

func (SessionRevocation) Validate

func (s SessionRevocation) Validate() error

type SigningAlgorithm

type SigningAlgorithm string

SigningAlgorithm — алгоритм подписи токена. Словарь ЗАКРЫТ: значение вне него отвергается разбором конфигурации и стражем старта (приёмка F1 §3 строка 1, F1-03). Открытый словарь означал бы «принимаем любой» — тот же класс, что пустой перечень.

const (
	SigningAlgRS256 SigningAlgorithm = tokenpolicy.AlgRS256
	SigningAlgES256 SigningAlgorithm = tokenpolicy.AlgES256
	SigningAlgEdDSA SigningAlgorithm = tokenpolicy.AlgEdDSA
)

Закрытый словарь алгоритмов подписи. Значения ВЫВЕДЕНЫ из платформенного объявления (pkg/tokenpolicy), а не выписаны здесь второй раз: приёмная сторона живёт в другом сервисе, и две копии словаря разошлись бы молча — в сторону «принимаем больше», потому что расширять проще.

func ParseSigningAlgorithm

func ParseSigningAlgorithm(raw string) (SigningAlgorithm, error)

ParseSigningAlgorithm разбирает значение конфигурации. Пустое значение — НЕ «умолчание»: у алгоритма подписи умолчания нет, потому что подпись умолчанием — решение, принятое за оператора.

func SigningAlgorithms

func SigningAlgorithms() []SigningAlgorithm

SigningAlgorithms возвращает закрытый словарь целиком. Перечень ВЫВОДИТСЯ отсюда всеми, кому он нужен (страж старта, текст отказа, проверка ключа), а не выписывается по месту.

func (SigningAlgorithm) MinBits

func (a SigningAlgorithm) MinBits() int

MinBits — нижний порог стойкости ключа, объявленный ЧИСЛОМ и ровно в одном месте дерева (F1-02). Для RSA это длина модуля; для кривых и Ed25519 длина задана самой кривой, поэтому порог у них выражен размером, который эта кривая даёт.

type SigningKeyRecord

type SigningKeyRecord struct {
	KID               KeyID
	Algorithm         SigningAlgorithm
	State             SigningKeyState
	PublicKeyPEM      string
	PrivateKeyWrapped []byte
	CreatedAt         time.Time
	NotAfter          time.Time
	ActivatedAt       *time.Time
	RetiredAt         *time.Time
	RemovedAt         *time.Time
	CompromisedAt     *time.Time
}

SigningKeyRecord — ХРАНИМАЯ форма ключа: несёт обёрнутую приватную половину.

Пара к PublishedKey. Разделение типов держит §6.10 КОМПИЛЯТОРОМ: форма «поле есть, но мы его не заполняем» держалась бы вниманием.

func (SigningKeyRecord) Published

func (r SigningKeyRecord) Published() PublishedKey

Published — проекция строки в ПУБЛИКУЕМУЮ форму. Единственный переход между двумя типами, и он односторонний: обратного конструктора нет.

type SigningKeyState

type SigningKeyState string

SigningKeyState — состояние ключа в ключнице.

Состояний ПЯТЬ, и «скомпрометирован» существует отдельно от «выведен» (приёмка §2.1): первое снимает ключ из набора немедленно, принимая отказ живых токенов, второе — нет. Глагол, делающий и то и другое, лишил бы второе решение его цены.

const (
	// SigningKeyPublished — ключ в наборе, но ещё не подписывает. Этап
	// существует ровно ради порядка «в наборе → подписывает» (§6.1).
	SigningKeyPublished SigningKeyState = "PUBLISHED"
	// SigningKeyActive — ключ подписывает. Такой РОВНО ОДИН, и это держит
	// частичный уникальный индекс, а не проверка в коде (§6.2).
	SigningKeyActive SigningKeyState = "ACTIVE"
	// SigningKeyRetired — выведен из подписи, остаётся в наборе всю отсрочку:
	// подписанные им токены доживают свой срок (§6.4).
	SigningKeyRetired SigningKeyState = "RETIRED"
	// SigningKeyRemoved — отсрочка истекла, ключа в наборе нет.
	SigningKeyRemoved SigningKeyState = "REMOVED"
	// SigningKeyCompromised — покидает набор НЕМЕДЛЕННО; живые токены
	// отвергаются, и это принятая цена, а не дефект.
	SigningKeyCompromised SigningKeyState = "COMPROMISED"
)

Состояния ключа.

func (SigningKeyState) CanActivate

func (s SigningKeyState) CanActivate() bool

CanActivate отвечает, допускает ли машина состояний переход в ACTIVE.

Переход из REMOVED и COMPROMISED НЕ ВЫРАЖАЕТСЯ (F1-29): скомпрометированный ключ, вернувшийся в подпись, — не «редкий случай», а конструкция, которая получается сама, если её не запретить.

func (SigningKeyState) InKeySet

func (s SigningKeyState) InKeySet() bool

InKeySet отвечает, попадает ли ключ в публикуемый набор.

Ответ следует СОСТОЯНИЮ, а не факту существования строки: набор, отдающий все строки подряд, отдал бы и снятый, и скомпрометированный.

type StepUpChallenge added in v0.5.0

type StepUpChallenge struct {
	// Challenge — случайные байты; ключ строки. Длина — та же, что у прочих
	// испытаний ключа (`AccessKeyChallengeBytes`).
	Challenge []byte
	// SessionID — запись сессии, которой испытание выдано. Одно ЖИВОЕ
	// испытание на запись держит частичная уникальность схемы (ban #10).
	SessionID HumanSessionID
	// IssuedAt и ExpiresAt — момент выдачи и предел, часы службы.
	IssuedAt  time.Time
	ExpiresAt time.Time
}

StepUpChallenge — выдаваемое испытание повышения: значение, запись сессии, которой оно выдано, и его срок.

func (StepUpChallenge) Validate added in v0.5.0

func (c StepUpChallenge) Validate() error

Validate — самопроверка выдаваемого испытания.

type StepUpChallengeState added in v0.5.0

type StepUpChallengeState int

StepUpChallengeState — почему испытание повышения не сгорело: три причины наружу различимы (приёмка SUA Р3), и каждая — свой отказ состояния.

const (
	// StepUpChallengeNotIssued — строки этой пары (сессия, испытание) нет: не
	// выдавалось, выдано другой сессии либо сменено новой выдачей.
	StepUpChallengeNotIssued StepUpChallengeState = iota + 1
	// StepUpChallengePresented — строка есть и уже предъявлена.
	StepUpChallengePresented
	// StepUpChallengeExpired — строка есть, не предъявлена, предел вышел.
	StepUpChallengeExpired
)

type StructuralTuple

type StructuralTuple struct {
	// User — the SOURCE of the relation: the parent object ("account:<id>") for a
	// parent-pointer, or the subject ("user:<id>") for the account owner.
	User string
	// Relation — the relation name on Object, named after the parent's type for a
	// pointer ("account"/"project"/"cluster") or "owner".
	Relation string
	// Object — the object the relation is written on ("iam_access_binding:<id>").
	Object string
}

StructuralTuple — one relation triple. Transport-neutral on purpose: the emitter converts it to its repo tuple type, and domain stays free of that type.

func AccountScopedStructuralFact

func AccountScopedStructuralFact(accountID, objectType, objectID string) (StructuralTuple, bool)

AccountScopedStructuralFact is the one-line projection shared by the account-scoped iam types, whose cascade is `super_admin: admin from account`:

account:<accountID>  →  account  →  <objectType>:<objectID>

An empty accountID yields nothing: a row with no owning account (a system role) must not become reachable from any account's administrator.

type Subject

type Subject struct {
	Type SubjectType
	ID   SubjectID
}

Subject — one grantee of an AccessBinding (RBAC rules-model). A binding may carry 1..32 subjects; each yields an INDEPENDENT FGA tuple-set + emitted-tuple ledger lineage, so per-subject revoke/audit never touches another subject's tuples. A GROUP subject grants the role to every member (userset) — resolved to concrete principals by ExpandAccess and, for admin/editor-tier roles, requiring requireGrantAuthority on the scope (group-amplification guard).

func NormalizeSubjects

func NormalizeSubjects(subjects []Subject, legacyType SubjectType, legacyID SubjectID) ([]Subject, error)

NormalizeSubjects resolves the canonical subjects[] set from the request input (two-way projection between the new subjects[] and the legacy single subject_type/subject_id):

  • subjects[] is the canonical (preferred) input. When it is set, the legacy single pair (if also present) MUST equal subjects[0]; otherwise INVALID_ARGUMENT (a wire client cannot disagree with itself).
  • When subjects[] is empty, the legacy single pair projects to a one-element subjects[] (legacy clients keep working).
  • Empty subjects[] AND empty legacy single → INVALID_ARGUMENT ("Illegal argument subjects (must be 1..32)").
  • More than 32 → INVALID_ARGUMENT (same text).
  • A duplicate (type,id) → INVALID_ARGUMENT (the DB UNIQUE would also reject; fail sync with a clear message).
  • Each subject is self-validated (closed type + non-empty id).

Pure domain — no DB / no transport. The use-case maps the returned error to the gRPC code (shared.MapValidationErr → INVALID_ARGUMENT).

func (Subject) IsGroup

func (s Subject) IsGroup() bool

IsGroup reports whether this subject is a GROUP (userset) — the case that triggers the group-amplification guard and userset expansion.

func (Subject) IsSystemClass added in v0.5.0

func (s Subject) IsSystemClass() bool

IsSystemClass — субъект есть системный класс.

func (Subject) Validate

func (s Subject) Validate() error

Validate — субъект судит себя сам: закрытый вид и непустой идентификатор; системный класс называет имя закрытого словаря (`anonymous` | `authenticated`).

type SubjectID

type SubjectID string

SubjectID — opaque id (user, service_account, or group). The paired SubjectType defines the semantics.

const (
	// ClassAnonymous — любой вызывающий, в том числе без личности.
	ClassAnonymous SubjectID = "anonymous"
	// ClassAuthenticated — любой принципал кластера с личностью, из любого
	// аккаунта: выдача этому классу видна за границей арендатора (Р5).
	ClassAuthenticated SubjectID = "authenticated"
)

Имена классов закрытого словаря.

type SubjectPrivilege

type SubjectPrivilege struct {
	BindingID       AccessBindingID
	RoleID          RoleID
	RoleName        RoleName // resolved via JOIN; "" for a dangling/deleted role
	ResourceType    ResourceType
	ResourceID      string // opaque id (any prefix, cross-service OK)
	Scope           Scope  // CLUSTER / ACCOUNT / PROJECT
	Status          AccessBindingStatus
	CreatedAt       time.Time
	GrantedByUserID UserID
	ExpiresAt       *time.Time // nullable — TTL
	// Derivation — DIRECT | GROUP. The zero value is treated as DIRECT by the
	// transport projection (a row produced without an explicit derivation is a
	// direct grant), so the proto enum never leaks UNSPECIFIED.
	Derivation PrivilegeDerivation
	// ViaGroupID — the group carrying the privilege when Derivation==GROUP; empty
	// for a DIRECT grant. Without it a GROUP row is un-actionable: the binding's
	// own subject is the group, so the administrator cannot tell which membership
	// to remove.
	ViaGroupID GroupID
}

SubjectPrivilege — enriched, public-safe projection of an AccessBinding for the subject-privileges view (RPC AccessBindingService.ListSubjectPrivileges).

It is an AccessBinding row JOINed with its Role so the human-readable RoleName is resolved server-side in ONE query (access_bindings ⋈ roles on role_id, same kaname schema, FK access_bindings_role_fk) — no per-row N+1 GetRole fan-out. A dangling role (deleted after a revoke) yields an empty RoleName; the consumer (UI) falls back to the raw RoleID (graceful).

Carries only tenant-facing, publicly-safe fields: id / role / scope / status / created_at / granted_by — никаких инфра-чувствительных данных и никаких condition/builtin_condition-internals (вне scope v1).

Derivation says HOW the subject holds the privilege: DIRECT (the binding names the subject itself) or GROUP (the binding names a group the subject belongs to, named by ViaGroupID). It is computed by the read query, not stored on the binding — a binding does not know which of its subjects' memberships a given reader is asking about.

type SubjectType

type SubjectType string

SubjectType — enum: user|service_account|group|system_class.

const (
	SubjectTypeUser           SubjectType = "user"
	SubjectTypeServiceAccount SubjectType = "service_account"
	SubjectTypeGroup          SubjectType = "group"
)

SubjectType values.

const SubjectTypeSystemClass SubjectType = "system_class"

SubjectTypeSystemClass — вид субъекта «системный класс» (Р1).

func SubjectTypeOfPrincipal added in v0.7.0

func SubjectTypeOfPrincipal(t string) (SubjectType, bool)

SubjectTypeOfPrincipal — тип субъекта по виду принципала вызова. Единственная нормализация вида для сравнения «вызывающий — делегат» и «субъект — делегат» (условие CE-22 разбора классов): вид принципала пишется словом хранения ("service_account"), тип субъекта контракта — именем значения (`SUBJECT_TYPE_SERVICE_ACCOUNT`); обе формы приводятся к `SubjectType`.

func (SubjectType) Validate

func (s SubjectType) Validate() error

Validate — SubjectType — enum check.

type SubordinateResource

type SubordinateResource struct {
	// Kind — двухчастный токен, которым подчинённый ресурс называется в видах
	// учёта (`iam.user.credential` → `iam.credential`).
	Kind LimitKind
	// Parents — типы модели прав, от которых производен доступ. Их МОЖЕТ быть
	// несколько: один класс сущности живёт в нескольких таблицах ровно тогда,
	// когда видов родителя несколько.
	Parents []LimitKind
	// Tables — таблицы, чьи строки этот ресурс называет, в форме `схема.имя`.
	// Это АНКЕР записи в дереве, а не справка.
	Tables []string
	// Why — почему своего типа модели прав нет. Запись без причины неотличима
	// от записи без предмета.
	Why string
}

SubordinateResource — ресурс, адресуемый арендатором и НЕ имеющий своего типа модели прав, потому что доступ к нему производен от РОДИТЕЛЯ.

Зачем понадобился второй источник имён

Вид учёта обязан называть реальные типы модели прав — так гейт каталога и устроен, и обоснован он утверждением «модель прав знает КАЖДЫЙ адресуемый арендатором тип». На удостоверениях это утверждение ложно: они адресуются (`/iam/v1/users/{user_id}/tokens/{token_id}`), имеют свой идентификатор и свои глаголы, а типа модели прав у них нет — право на удостоверение вычисляется от человека (`token_issuer: subject`), и заводить объект, на который его можно было бы ВЫДАТЬ, модель намеренно избегает.

Почему это расширение, а не ослабление

Множество допустимых имён остаётся закрытым и сверяемым с деревом. Разница с первой редакцией — в поле `Tables`: без него запись была бы САМОЗАЯВЛЕНИЕМ. У закрытой таблицы типов истинность анкерена гейтом дрейфа против канонической модели; здесь анкер — сами таблицы строк и стоящие на них триггеры списания (утверждения G5/G6 гейта каталога). Опечатка `iam.credentials` внутреннюю согласованность записи проходит и ловится ровно этим анкером.

func SubordinateResourceOf

func SubordinateResourceOf(k LimitKind) (SubordinateResource, bool)

SubordinateResourceOf отдаёт запись подчинённого ресурса по его токену.

func SubordinateResources

func SubordinateResources() []SubordinateResource

SubordinateResources отдаёт КОПИЮ перечня: перечень — предмет гейта, и вызывающий, способный его изменить, сделал бы гейт зависимым от порядка вызовов.

type SvcAccountName

type SvcAccountName string

func (SvcAccountName) Validate

func (n SvcAccountName) Validate() error

type TargetMember

type TargetMember struct {
	BindingID          AccessBindingID
	RoleID             RoleID
	RuleFP             string
	ObjectType         string
	ObjectID           string
	VerificationStatus VerificationStatus
}

TargetMember — one materialized member of a binding's target with its current verification status. object_type is a closed-table dotted key (e.g. "compute.instance"); object_id is an opaque cross-DB soft-ref.

RuleFP attributes the member to the (role) RULE that produced it: the content-hash of the ARM_LABELS rule (domain.Rule.Fingerprint) for a role.rules-driven member, or the sentinel "legacy-selector" for a legacy binding.selector member. Keying membership by rule_fp (not a positional index) lets a Role.Update that removes one rule eager-revoke ONLY that rule's members and lets the SAME object be a member under two different rules at different tiers (distinct rows).

type TargetTypeCatalog added in v0.6.0

type TargetTypeCatalog interface {
	// FGAObjectType — имя типа модели для точечного имени каталога; ok=false у
	// ресурса, чья строка снята либо которого в каталоге нет вовсе.
	FGAObjectType(dotted string) (string, bool)
}

TargetTypeCatalog — ПОРТ живого каталога типов, каким его видит проверка цели выдачи: есть ли у платформы ЖИВАЯ строка каталога с таким точечным именем. Объявлен здесь, у потребителя; реализация — `catalog.Facts` (снимок живых строк). Порт узкий намеренно: цели нужен ровно один факт — есть ли строка.

type TrustedIssuer

type TrustedIssuer struct {
	// ClientID — НАША строка федеративного ключа, которую эта запись
	// уполномочивает. Внешний субъект получает токен от её имени, никогда от
	// своего.
	ClientID string
	// Issuer — идентификатор внешнего издателя, сверяемый с `iss` дословно.
	Issuer string
	// Subject — ТОЧНЫЙ субъект, сверяемый с `sub` дословно.
	//
	// Точный, а не образец: доверие образцу означало бы, что запись покрывает
	// субъектов, которых называющий её не перечислял, — и узнать их состав
	// нельзя ни по записи, ни по журналу.
	Subject string
	// PublicKeyPEM — открытый ключ ИЗДАТЕЛЯ в форме SPKI PEM.
	PublicKeyPEM string
	// Algorithm — зарегистрированный алгоритм издателя. Пустое значение
	// означает «ключа нет», а НЕ «любой алгоритм».
	Algorithm string
	// ExpiresAt — момент истечения ДОВЕРИЯ в Unix-секундах; 0 означает
	// «бессрочно».
	ExpiresAt int64
}

TrustedIssuer — одна запись НАШЕГО перечня доверенных издателей.

Форма повторяет то, что прежде хранил у себя поставщик: издатель, ТОЧНЫЙ субъект, ключевой материал издателя и срок. Повторяет намеренно — перевод перечня к нам не должен был заодно менять то, о чём перечень.

func (TrustedIssuer) CanVouch

func (ti TrustedIssuer) CanVouch() bool

CanVouch отвечает, способна ли запись вообще служить основанием доверия.

Тот же класс, что `AssertionClient.CanPresentAssertion`: пустой ключ и пустой алгоритм — это «ключа нет». Запись без ключа не отвергала бы ничего, а принимала бы всё, что называет её пару, — то есть доверие издателю выродилось бы в доверие строке таблицы.

type TrustedSubject

type TrustedSubject struct {
	Issuer         string
	SubjectPattern string
	// PublicKeyPEM — открытый ключ ИЗДАТЕЛЯ (SPKI PEM). Тот, которым подписано
	// внешнее утверждение; нашего ключевого материала федеративная строка не
	// несёт вовсе.
	PublicKeyPEM string
	// KeyAlgorithm — зарегистрированный алгоритм издателя. Пустое значение
	// означает «ключа нет», а НЕ «любой алгоритм».
	KeyAlgorithm string
}

TrustedSubject — one (issuer, subject) tuple permitted to assert a federated ServiceAccountOAuthClient. `Issuer` MUST match the external OIDC `iss` claim verbatim; `SubjectPattern` is a LITERAL-anchored exact subject (`^<literal>$`, no regex metacharacters).

Точная форма субъекта требуется потому, что доверие выдаётся ПОИМЁННО: запись, покрывающая субъектов образцом, называет тех, кого выдававший не перечислял, и установить их состав нельзя ни по записи, ни по журналу.

Перечень — НАША таблица (задача kacho#1124)

Прежде решение о доверии принимал поставщик: запись жила у него, и там же лежал ключ издателя. Отсюда была выведена и прежняя редакция этого комментария — «служба прав вне пути запроса, поэтому образец было бы нечем применить». Сегодня служба прав НА пути запроса: перечень читает её проверка утверждения (`internal/clientassertion`, федеративная полоса). Точная форма осталась, но держит её теперь названный выше довод, а не чужая реализация.

func (TrustedSubject) LiteralSubject

func (ts TrustedSubject) LiteralSubject() (string, bool)

LiteralSubject returns the exact subject enclosed by a valid literal-anchored pattern (`^<literal>$` → `<literal>`), and false when the pattern is not a literal-anchored subject (wildcard / unanchored / regex metacharacters).

func (TrustedSubject) Validate

func (ts TrustedSubject) Validate() error

Validate — Issuer must be an https URL to a public host (anti-SSRF on the trust-config: no non-https / loopback / private / link-local host); SubjectPattern must be a literal-anchored exact subject. Length caps mirror the proto (≤512 each).

Ключевой материал издателя обязателен и проверяется на РАЗБИРАЕМОСТЬ здесь, а не при первом предъявлении: непригодный ключ, принятый на выдаче, даёт запись доверия, которая не примет никогда никого, — то есть возможность, объявленную и не работающую ни при каком входе. Отказ на выдаче виден тому, кто её заказал; отказ на предъявлении виден постороннему и неотличим для него от «доверия нет».

type TypeVerbLookup

type TypeVerbLookup func(module, resource string) (verbs []string, ok bool)

TypeVerbLookup — набор глаголов, объявленный типом (module, resource); ok=false, когда пара не резолвится ни в один известный тип.

Приходит ПАРАМЕТРОМ, а не импортом таблицы: владельцем таблицы остаётся authzmap, а домен — без внешних зависимостей (см. rule_verbs.go, «pure domain»).

func WithCommonFallback

func WithCommonFallback(lookup TypeVerbLookup, common []string) TypeVerbLookup

WithCommonFallback оборачивает lookup так, что пара-ПОДСТАНОВКА получает словарь, ОБЩИЙ для всех ресурсов.

Это решение ВЫЗЫВАЮЩЕГО, не домена: правило-подстановка (`*.*` роли- суперпользователя) своего набора не имеет by construction — перечислить ресурсы подстановки домену нечем, каталог ему не принадлежит, — а пустое превью читалось бы как «роль ничего не даёт».

Запасной словарь даётся ПОДСТАНОВКЕ, а не всякому промаху (kacho#1814)

Прежде его получала ЛЮБАЯ нерезолвящаяся пара, и это переворачивало смысл снятия ресурса: правило, называющее снятый `compute.disk`, разворачивалось в глаголы ВСЕЙ платформы, то есть после снятия превью показывало не «меньше», а БОЛЬШЕ. Роль обещала арендатору то, чего материализация не даёт, и обещала тем громче, чем уже правило.

Различает ФОРМА пары, а не исход резолва: подстановка называет «все» и потому законно берёт общий словарь; названный ресурс называет ОДИН тип, и если этого типа нет — давать нечего. Промах названной пары остаётся промахом (`ok=false`), и вызывающий разворачивает её ни во что.

type User

type User struct {
	ID           UserID
	AccountID    AccountID
	ExternalID   ExternalSubject
	Email        Email
	DisplayName  DisplayName
	InviteStatus InviteStatus
	InvitedBy    UserID // user.id of admin who invoked Invite; "" if self-signup
	CreatedAt    time.Time
	// InviteExpiresAt — срок строки приглашения (ID-MAIL-1, MAIL-23): после
	// него активация отвергается. Нулевое время — «срок не назначен»: так
	// выглядят строки, заведённые до появления колонки, и выкупленные строки,
	// у которых срок предмета не имеет.
	InviteExpiresAt time.Time
	// Labels — tenant-facing метки. Делают User label-selectable наравне с
	// account/project: ARM_LABELS-грант на iam.user материализует v_list по
	// `labels @> matchLabels`, а List фильтрует через viewer ∪ v_list.
	Labels Labels
}

User — зеркало личности из внешнего провайдера. Одна личность — ОДНА строка, сколько бы аккаунтов её ни пригласило.

Принадлежность аккаунту здесь НЕ живёт: её выражает строка `kaname.memberships` (одна на пару «человек × аккаунт»), и членств у одного человека бывает несколько. Ключ идентичности — глобальный: `users_identity_email_uniq` и `users_identity_external_id_uniq` (`20260823050000_users_identity_uniqueness_goes_global.sql`, стадия S4-expand перехода IAM-ID-1). PENDING-строка держит external_id="" до первого входа.

Поле AccountID — ЛЕГАСИ-колонка перехода: она жива и `NOT NULL` до стадии S4, но «его аккаунт» из неё не читается — у человека их несколько.

func (User) InviteExpired added in v0.5.0

func (u User) InviteExpired(now time.Time) bool

InviteExpired — истёк ли срок приглашения к моменту now. Строка без срока не истекает никогда — это законное состояние, а не отказ.

func (User) Validate

func (u User) Validate() error

Validate — fields + invite-status consistency.

PENDING ⇔ external_id="" ; ACTIVE/BLOCKED ⇔ external_id<>"" (matches DB CHECK users_invite_status_consistency).

type UserID

type UserID string

type UserOAuthClient

type UserOAuthClient struct {
	ID              UserOAuthClientID
	UserID          UserID
	Description     Description
	CreatedByUserID UserID
	CreatedAt       time.Time
	ExpiresAt       *time.Time
	LastUsedAt      *time.Time

	// PublicKeyPEM — SPKI-encoded ECDSA P-256 публичный ключ удостоверения.
	// По нему проверяется подпись `client_assertion` на пути выдачи токена.
	PublicKeyPEM string
	// KeyAlgorithm — JOSE alg зарегистрированного ключа. Всегда "ES256" для новых
	// токенов.
	KeyAlgorithm string

	// CredentialKind — вид удостоверения. ЗАПИСЫВАЕТСЯ при вставке; читателем
	// не вычисляется и из состава прочих полей не выводится.
	CredentialKind CredentialKind
	// SecretHash — sha256 по идентификатору строки И секретной части вместе,
	// 32 байта. Непуст ТОЛЬКО у вида SECRET. Сам секрет не хранится нигде: он
	// существует только в теле ответа, полученного вызывающим выдачи.
	SecretHash []byte

	// Name — человекочитаемое имя токена, выставляется на Issue (create-only,
	// immutable — ресурс несёт только Issue/List/Revoke). Пусто для legacy-строк.
	Name OAuthClientName
	// Labels — произвольные метки токена, выставляются на Issue (create-only,
	// immutable). Пусто для legacy-строк.
	Labels Labels
}

UserOAuthClient — персональный access-токен пользователя.

private_key_jwt: kaname генерирует пару ключей ECDSA P-256 на каждый токен, держит SPKI public PEM плюс алгоритм и возвращает приватный PEM вызывающему ровно один раз. Секрет не существует at-rest.

Клиентом это удостоверение называется по идентификатору ЭТОЙ строки: им подписывается `client_assertion`, и по нему же разрешает клиента наш реестр утверждений. Второго имени у него нет.

N:1 — у одного User может быть несколько токенов.

func (UserOAuthClient) Validate

func (c UserOAuthClient) Validate() error

Validate — self-validating инвариант доменной сущности.

type UserOAuthClientID

type UserOAuthClientID string

UserOAuthClientID — новый формат `uoc<17-crockford>` (corelib `ids.NewID`, без подчёркивания). id существующих строк immutable, поэтому валидатор принимает и legacy `uoc_<17-crockford>`.

func (UserOAuthClientID) Validate

func (id UserOAuthClientID) Validate() error

type UserTokenRevocation

type UserTokenRevocation struct {
	UserID       UserID
	RevokeBefore time.Time
	Reason       string
	RevokedBy    UserID
}

UserTokenRevocation — per-user "revoke-all-before" cutoff (migration 0012). Backs admin ForceLogout + Revoke(revoke_all_user_tokens): any token whose originating session authenticated at or before RevokeBefore is denied at refresh. One row per user (PK user_id); the cutoff only ever advances (monotonic GREATEST upsert at the repo layer) so a re-auth past the cutoff is allowed again (no permanent lockout).

func (UserTokenRevocation) Validate

func (u UserTokenRevocation) Validate() error

Validate — self-validating domain entity.

type VerificationCode added in v0.5.0

type VerificationCode struct {
	ID     VerificationCodeID
	UserID UserID
	// Email — значение адреса, на которое код выдан.
	Email  Email
	Digest CodeDigest
	// IssuedAt — момент выдачи (часы полосы); по нему считаются письма (Р9).
	IssuedAt time.Time
	// ExpiresAt — абсолютный срок; величина — настройка.
	ExpiresAt time.Time
	// Attempts — сколько неподошедших предъявлений засчитано коду.
	Attempts int
}

VerificationCode — запись кода подтверждения (Р7). Состав закрыт.

func (VerificationCode) Validate added in v0.5.0

func (c VerificationCode) Validate() error

Validate — запись, годная к выдаче: судится до базы то, что база держит ограничениями.

type VerificationCodeID added in v0.5.0

type VerificationCodeID string

VerificationCodeID — идентификатор строки кода подтверждения. Наружу не адресуется.

type VerificationCodeValue added in v0.5.0

type VerificationCodeValue = RecoveryCodeValue

VerificationCodeValue — значение кода подтверждения: тип кода-предъявителя Ф5 Р1.

func NewVerificationCodeValue added in v0.5.0

func NewVerificationCodeValue() (VerificationCodeValue, error)

NewVerificationCodeValue — свежий код из криптографического источника.

func PresentedVerificationCode added in v0.5.0

func PresentedVerificationCode(value string) VerificationCodeValue

PresentedVerificationCode — код, как его прислал человек: регистр, разделители и знаки-двойники не значимы. Пустое после приведения — отсутствие кода.

type VerificationStatus

type VerificationStatus string

VerificationStatus — the observable per-member containment verdict.

const (
	// VerificationPending — the object is NOT yet in resource_mirror (the grant
	// raced ahead of the owner's RegisterResource). No FGA tuple is emitted; the
	// reconciler verifies it when the mirror row arrives.
	VerificationPending VerificationStatus = "PENDING_VERIFICATION"
	// VerificationActive — the object is in resource_mirror AND under the
	// binding's scope-anchor. The per-object FGA tuple IS emitted.
	VerificationActive VerificationStatus = "ACTIVE"
	// VerificationRejected — the object is in resource_mirror but NOT under scope
	// (mirror.parent_* ⋢ scope). No tuple; an audit event is written (not silent).
	VerificationRejected VerificationStatus = "REJECTED"
)

type WithdrawnGrant

type WithdrawnGrant struct {
	// ObjectType — точечный тип объекта, чью строку каталога сняли.
	ObjectType string
	// Verb — глагол. ПУСТАЯ строка есть ЯКОРЬ объявления правила, а не «любой
	// глагол»: у объявления, не назвавшего глагол поимённо, его нет.
	Verb string
	// Source — из какой проекции переселена строка.
	Source WithdrawnGrantSource
	// Reason — причина снятия строки каталога, записанная применителем
	// манифеста. Причина ПЛАТФОРМЫ, а не действие арендатора.
	Reason string
	// WithdrawnAt — момент переселения. Время ТРАНЗАКЦИИ применения: у всего,
	// что отобрано одним применением, он совпадает дословно.
	WithdrawnAt time.Time
	// AppliedBy — АВТОР применения, снявшего эту строку (kacho#2005): проверенная
	// личность вызывающего на пути глагола либо названный процессный актор на
	// пути старта. НЕ учётка, под которой исполнялась транзакция.
	//
	// Для разбора «почему у меня отобрали право» это второй по важности вопрос
	// после «почему»: причина называет, ЧТО случилось с платформой, а автор — КТО
	// это сделал.
	//
	// ПУСТАЯ строка означает «строка переселена ДО заведения колонки», а не
	// «автора потеряли»: восстановить его у таких строк не из чего.
	AppliedBy string
	// Cause — ПОЧЕМУ строка переселена (kacho#1913). Различие наблюдаемо, потому что
	// от него зависит поведение: оживление роли снимает строки причины
	// [WithdrawnGrantCauseRoleRetired] и не трогает строк причины
	// [WithdrawnGrantCauseCatalogRetired].
	//
	// Отличается от [WithdrawnGrant.Source] предметом: тот называет, ИЗ КАКОЙ
	// проекции строка переселена, этот — ПОЧЕМУ. Величины независимы: каждая
	// причина встречается у обеих популяций.
	Cause WithdrawnGrantCause
}

WithdrawnGrant — одна проекция правила роли, потерявшая референт при снятии строки каталога платформы и ПЕРЕСЕЛЁННАЯ в ведомость, а не отобранная молча.

Зачем этот тип, когда рядом уже стоят счётчики

RoleIntegrity отвечает СКОЛЬКО сегментов перестало проецироваться и не отвечает, ЧТО именно и почему. Со стороны арендатора «право отобрали» без этого неотличимо от «права не было»: свою роль он не менял, а действовать она перестала.

Он ОБЪЯСНЯЕТ состояние и не определяет его

Целость выводится из того, что читает вердикт, а не из ведомости: у роли, пострадавшей вторым путём (тип объявлен, а модель прав его не знает), переселения не было ВОВСЕ, и ведомость о ней пуста при нездоровом состоянии. Судить целость по ведомости значило бы читать такую роль здоровой — форма инцидента 513001.

type WithdrawnGrantCause

type WithdrawnGrantCause uint8

WithdrawnGrantCause — почему проекция переселена в ведомость.

Нулевой вариант означает «этим ответом не вычислено» и НИКОГДА «причина неизвестна»: непонятая строка ведомости отвергается разбором, а не выдаётся за невычисленную.

const (
	// WithdrawnGrantCauseUnknown — не вычислено ЭТИМ ответом.
	WithdrawnGrantCauseUnknown WithdrawnGrantCause = iota
	// WithdrawnGrantCauseCatalogRetired — снята строка КАТАЛОГА платформы, на
	// которую ссылалось правило. Роль при этом объявлена и жива.
	WithdrawnGrantCauseCatalogRetired
	// WithdrawnGrantCauseRoleRetired — снята САМА РОЛЬ. Возврат объявления
	// оживляет её и снимает эти строки.
	WithdrawnGrantCauseRoleRetired
)

type WithdrawnGrantSource

type WithdrawnGrantSource uint8

WithdrawnGrantSource — из какой проекции правила переселена строка ведомости.

Популяции разделены намеренно: «право отобрано» и «правило перестало резолвиться» — разные события для того, кто разбирает последствия, и сложив их, мы потеряли бы именно это различие.

const (
	// WithdrawnGrantSourceUnknown — популяция не прочитана ЭТИМ ответом. Никогда
	// не означает «популяция неизвестна»: ведомость несёт её всегда, закрытым
	// набором на уровне схемы.
	WithdrawnGrantSourceUnknown WithdrawnGrantSource = iota
	// WithdrawnGrantSourceGrant — выдача глагола: право по паре действовало и
	// перестало.
	WithdrawnGrantSourceGrant
	// WithdrawnGrantSourceRuleRef — объявление правила: сегмент перестал
	// резолвиться в каталог.
	WithdrawnGrantSourceRuleRef
)

Jump to

Keyboard shortcuts

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