domain

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2026 License: AGPL-3.0 Imports: 19 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 — доменные понятия аутентификации клиента подписанным утверждением (задача #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 — доменные понятия перечня доверенных издателей утверждения (задача #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 бит.

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 (`kacho-system.admin`), seeded by migration 0001 and
	// re-seeded with `*.*.*` rules by migration 0031. Also a legitimate
	// cluster-admin superuser.
	SystemAdminRoleID = "rol000000000sysadmin"

	// SystemViewerRoleID — рукописный детерминированный id второй системной роли
	// пола каталога (`kacho-system.viewer`), посеянной миграцией 0001.
	//
	// ДЛИНА ЕГО — 21, а не 20, и это ФАКТ ПРИМЕНЁННОЙ МИГРАЦИИ, а не описка,
	// которую можно поправить: id неизменяем на всю жизнь ресурса (ban #15,
	// «операции смены id НЕ существует»), а применённую миграцию править нельзя
	// (ban #5). Переименование поздней миграцией сверх того есть перенос выдачи с
	// одной роли на другую — тихое расширение прав, отвергаемое гейтом
	// `TestNoMigrationMovesGrantsBetweenRoles`.
	//
	// Поэтому литерал назван здесь и стоит в закрытом перечне
	// `SeededResourceIDs()`: проверка формы принимает то, что продукт сам посеял,
	// а не объявляет собственный посев негодным. Задача #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 (
	// ClaimPrincipalType — вид принципала: `user` либо `service_account`.
	ClaimPrincipalType = "kaname_principal_type"
	// ClaimPrincipalID — идентификатор принципала.
	ClaimPrincipalID = "kaname_principal_id"
	// ClaimPrincipalDisplay — отображаемое имя принципала. Косметическое.
	ClaimPrincipalDisplay = "kaname_principal_display_name"
)
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 CustomRoleNameForm = `^[a-z][a-z0-9_]{0,40}$`

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

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

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

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 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 — hard upper bound on target.resources[]. Anti-DoS + tractable per-object materialization: the reconciler intersects every rule-matched object with the target through the LINEAR AccessTarget.Contains, inside ONE synchronous create writer-tx holding the binding advisory lock, and re-runs it for every object created cluster-wide (forwardObjectForBinding). An unbounded set therefore costs |matched|×|target| comparisons on a hot path plus an unbounded JSONB row. 0 < n ≤ 256 enforced sync (INVALID_ARGUMENT), mirroring MaxSubjectsPerBinding.

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 до задачи #1824 никто не строил.

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

Variables

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 — ЕДИНСТВЕННЫЙ отказ полосы базового секрета.

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

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

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 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 ErrTrustedIssuerUnknown = errors.New(
	"federated assertion: (issuer, subject) does not resolve in the trusted-issuer list")

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

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

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

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 IsAssertionClientUnknown

func IsAssertionClientUnknown(err error) bool

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

func IsAssertionReplayed

func IsAssertionReplayed(err error) bool

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

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

func IsCountableKind

func IsCountableKind(k LimitKind) bool

IsCountableKind reports membership in the closed catalogue.

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 IsPostureStatedKind

func IsPostureStatedKind(k LimitKind) bool

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

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 materialization feed (`ValidTargetType`).

func IsSeededResourceID

func IsSeededResourceID(id string) bool

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

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

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 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 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 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 SeededResourceIDs

func SeededResourceIDs() []string

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

func ValidKeyIDForm

func ValidKeyIDForm(raw string) bool

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

func ValidTargetType

func ValidTargetType(dotted string) bool

ValidTargetType reports whether a dotted `<module>.<resource>` may be named as a per-object AccessBinding target. The answer is membership in the materialization feed (`AllMaterializableTypes`), because that is the only place a target type is ever used: the reconciler calls AccessTarget.Contains with the object type taken from the feed and keeps the object on an exact string match. The wildcard `*`, malformed input and anything the feed does not emit are all refused.

It is a lookup, not a derivation. The predicate used to re-spell the dotted form into the bare scope-anchor vocabulary (`compute.instance` → `compute_instance`, checked against a hand-written whitelist that has since been removed — the anchor vocabulary is the three hierarchy tiers and is declared once, `scopeAnchorTiers`) and claimed that kept the two in sync by construction; it did not, because the two vocabularies answer different questions and are spelled in different conventions. Deriving one name from another instead of resolving it against the one table that owns it is the same mistake as reading a region out of a zone's name (data-integrity.md) — it is silently wrong for every pair the two conventions disagree on, in BOTH directions: a type the feed emits is refused, so the grant can only be written as the whole anchor and the check widens what it appears to restrict; and a type the feed never emits is accepted, stored and reconciled, matching nothing and telling the caller nothing.

func ValidateAssertionID

func ValidateAssertionID(raw string) error

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

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

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`, `quota_reader`, `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
}

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 AccessTarget

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

AccessTarget is the object-selection of an AccessBinding (F8). Exactly one arm is meaningful; the zero value is the whole-anchor grant (AllInScope semantics).

func (AccessTarget) Contains

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

Contains reports whether the closed per-object target set lists the object (dottedType, id). It is the least-privilege membership test the reconciler applies so a role.rules match materializes ONLY an object the target also lists. An AllInScope/empty target lists NO explicit object here (its breadth is the whole anchor, materialized by the reconciler's all-in-scope path — not this test), so Contains returns false for it; callers gate on IsEmpty()/AllInScope before using it.

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

func (t AccessTarget) IsEmpty() bool

IsEmpty reports whether neither arm is set (no explicit selection). An empty target is treated as AllInScope for storage/digest, but the public Create RPC rejects it (target REQUIRED).

func (AccessTarget) ResourceIDsForTypes

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

ResourceIDsForTypes returns the ids of the per-object target resources whose dotted type is in `types` (order-preserving, de-duplicated). It lets the reconciler resolve a per-object target's ARM_ANCHOR candidates by id (MatchByIDs) instead of scanning the whole scope (MatchAllInScope) — the least-privilege materialization path (IAM-1-21). Empty when no listed resource matches the given types.

func (AccessTarget) Validate

func (t AccessTarget) Validate() error

Validate checks the target well-formedness: the two arms are mutually exclusive, and every per-object ResourceRef carries a non-empty id and a type drawn from the closed dotted type-registry. AllInScope / empty is always well-formed.

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

Names — every newtype 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`, задача #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`), дописываемая только вперёд.

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

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

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

Прежняя редакция объясняла, почему дренаж неконструируем: приёмника аудита не существовало ни одного, и из четырёх объявленных состояний достижимо было ровно одно. Оба утверждения были верны на день записи и перестали быть верными вместе с приёмником (#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 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 CountableKind

type CountableKind struct {
	Kind    LimitKind
	Carrier LimitCarrier
}

CountableKind — one catalogue record: WHAT is counted and WHERE it is counted.

func CountableEntries

func CountableEntries() []CountableKind

CountableEntries returns a COPY of the closed catalogue, in catalogue order.

type CredentialKind

type CredentialKind string

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

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

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

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

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

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

	// CredentialKindLegacy — строка прежнего потока. НЕ ВЫДАЁТСЯ ни одним
	// глаголом; появляется только обратным заполнением.
	CredentialKindLegacy CredentialKind = "LEGACY"
)

func ResolveIssuedKind

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

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

Это НЕ тот классификатор, что работает над содержимым уже лежащих строк: тот четырёхветвевой, живёт в обратном заполнении и обязан иметь ветвь LEGACY, потому что материал чеканили не всегда мы. Спутать их значило бы получить «корзину прочее» наоборот — вход без вида не отвергается, а получает ближайший.

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

func (CredentialKind) IsIssuable

func (k CredentialKind) IsIssuable() bool

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

func (CredentialKind) String

func (k CredentialKind) String() string

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

type Description

type Description string

func (Description) Validate

func (d Description) Validate() error

type DisplayName

type DisplayName string

func (DisplayName) Validate

func (d DisplayName) Validate() error

type EffectiveLimit

type EffectiveLimit struct {
	Kind  LimitKind
	Value int64
	// Carrier — ГДЕ этот вид считается: корень аренды (`project` / `account`)
	// либо двухчастный токен родительского типа.
	//
	// Едет вместе с величиной, потому что вывести его на стороне потребителя
	// НЕЛЬЗЯ: форма токена носителя не определяет (`iam.project` — двухчастный
	// вид, чей носитель не проект), а догадка здесь не отказывает громко — она
	// считает верные строки против неверного владельца, и потребление такой
	// строки не наполняется никогда.
	//
	// Пустым не бывает: вид вне каталога до этой структуры не доходит.
	Carrier       LimitCarrier
	SourceScope   LimitScope
	SourceScopeID string
}

EffectiveLimit — one resolved ceiling plus the scope it was won at.

The source travels with the value because an operator asking "why does this project stop at four" cannot otherwise tell a project override from an account one from the platform default without re-reading all three scopes by hand.

func ResolveEffective

func ResolveEffective(service string, stated []Limit) []EffectiveLimit

ResolveEffective folds the limits stated across the three scopes into one row per kind of the requested service.

`stated` may contain rows of any scope in any order; only rows whose kind belongs to `service` participate. A kind with nothing stated at ANY scope is omitted from the answer — the caller must not read a missing row as "no ceiling": nothing was said, and inventing a ceiling here would be this function's guess rather than the platform's decision.

type Email

type Email string

func (Email) Validate

func (e Email) Validate() error

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 (ExternalSubject) Validate

func (s ExternalSubject) Validate() error

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 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 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 confirmed identity through Kratos. ACTIVE — either self-signup via `UpsertFromIdentity` without a pending invite, or a PENDING row activated on first-login (matched by email). BLOCKED — административный запрет на членство в Account'е. Ставится и снимается ДЕЙСТВИЯМИ `UserService.Block` / `Unblock` (право `identity_suspender@iam_user` = админ аккаунта плюс каскад облака; `v_update` с этого типа снят, #1128); писатель — `userWriter.SetInviteStatus`.

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

Снимать запрет самостоятельным действием нельзя: восстановление пароля доказывает владение почтовым ящиком — ровно то, чего администратор, ставя запрет, под сомнение не ставил (см. internal_on_recovery.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) 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 Limit

type Limit struct {
	ID        LimitID
	CreatedAt time.Time

	Scope   LimitScope
	ScopeID string
	Kind    LimitKind
	Value   int64

	// WithdrawnAt — the moment the ceiling stopped applying. Zero → in force.
	//
	// A withdrawal is a tombstone rather than a deleted row because owner
	// services keep a PROJECTION of these values and refresh it by delta: a delta
	// that only ever reports writes can never drop a projection row, so a
	// withdrawn project override would keep overriding forever.
	WithdrawnAt time.Time

	// Revision — monotonic, assigned by the database. Advances on a change of
	// value or of withdrawal, and stands still when a write restates what was
	// already there.
	Revision int64
}

Limit — the ceiling on how many resources of one kind a tenant may hold.

The triple (Scope, ScopeID, Kind) IS the limit's identity among those in force; `Value` is the only mutable field. "Moving" a ceiling to another project or another kind is a different ceiling, created and withdrawn explicitly — an Update that could change the triple would silently transfer a tenant's headroom to a tenant who was never granted it.

func (Limit) Validate

func (l Limit) Validate() error

Validate — self-validating domain entity.

The scope/subject pairing is checked HERE as well as by a DB CHECK, and that is not a duplicate rule: the database makes the state inexpressible for every writer, and this makes the caller's answer name the FIELD they got wrong instead of a constraint they have never heard of.

func (Limit) Withdrawn

func (l Limit) Withdrawn() bool

Withdrawn reports whether the ceiling has been withdrawn.

type LimitCarrier

type LimitCarrier string

LimitCarrier — the type of object a kind is counted IN.

WHY IT IS DECLARED AND NOT DERIVED. The temptation is "two parts ⇒ counted in a project", and that rule is false on the first entry that already exists: `iam.project` has two parts and is counted in an ACCOUNT, because a project does not live inside a project. A guess here does not fail loudly — it counts the right rows against the wrong owner, and the tenant sees a ceiling that never moves. So the carrier travels beside the kind, and the pair is the unit of the catalogue.

const (
	// CarrierProject — counted per project. The common case.
	CarrierProject LimitCarrier = "project"
	// CarrierAccount — counted per account, for kinds that have no project to
	// live in.
	//
	// НИ ОДИН ВИД КАТАЛОГА ЕГО СЕГОДНЯ НЕ НАЗЫВАЕТ, и это состояние, а не
	// упущение: шестеро, кто им пользовался, — проекты и субъекты в области
	// аккаунта — сняты с каталога вместе со своим учётом
	// (`PRO-Robotech/kacho#2117`, сценарий `KAN-Q3-04`), потому что не
	// списывались ни разу.
	//
	// Константа ОСТАЁТСЯ, и снимать её этим изменением нельзя: величину
	// `'account'` по-прежнему принимает ограничение
	// `project_resource_quotas_carrier_ck` ПРИМЕНЁННОЙ миграции, а править
	// применённую нельзя (ban #5). Уходит она вместе с остальным механизмом
	// величин — стадией S4 той же приёмки, где снимаются таблицы и функции.
	CarrierAccount LimitCarrier = "account"
	// CarrierIdentity — counted per HUMAN, across every account they hold.
	//
	// # Why a third root had to exist
	//
	// A carrier must be EXTERNAL to the thing it counts, and for the account
	// neither of the two above is: an account cannot be counted inside an
	// account, and it has no project. That is not an implementation gap but the
	// shape of the tenancy: the account is its root, and the root has no parent
	// below the cluster.
	//
	// Counting per cluster was the obvious alternative and it is worse in the way
	// that matters: the refusal reaches the NEXT honest tenant rather than the one
	// who exhausted the shelf. The identity is the only thing that exists BEFORE
	// an account and outlives it, so it is the only carrier on which the refusal
	// lands on its cause.
	//
	// # What identifies it, and why not the user row
	//
	// The identity is the external login subject (`users.external_id`), NOT the
	// user row. A user row is a MEMBERSHIP: it is scoped to one account, and one
	// human legitimately holds one per account. Counting per user row would tie
	// the ceiling to the very thing that multiplies as soon as the account
	// coupling is removed — that is, it would hand out the bypass together with
	// the change it is meant to survive.
	CarrierIdentity LimitCarrier = "identity"
)

The carriers that are not resource kinds: the tenancy roots. Any other carrier is a two-part token of the closed table (`vpc.network`), naming the parent a nested kind is counted within.

func CarrierOfKind

func CarrierOfKind(k LimitKind) (LimitCarrier, bool)

CarrierOfKind returns the carrier a kind is counted in. The second result is false for a kind outside the catalogue — and the caller must not read a missing carrier as "project": that default is exactly the guess V2-2 forbids.

func (LimitCarrier) Validate

func (c LimitCarrier) Validate() error

Validate — the carrier names one of the tenancy roots, or is shaped like a two-part catalogue token.

That the token RESOLVES against the authorization model is proved by authzmap's gate, not here: this package must not import the authz map (that package's gate already imports this one), and a second copy of the closed table here would be the two-places-one-subject class the corpus warns about.

type LimitFilter

type LimitFilter struct {
	Scope   LimitScope
	ScopeID string
	Kind    LimitKind
}

LimitFilter — the three-valued narrowing a List accepts. Empty members mean "do not narrow by this"; the vocabulary is closed, so there is no filter grammar here and nothing to parse.

func (LimitFilter) Validate

func (f LimitFilter) Validate() error

Validate — a narrowing value that is not a legal value of its dimension is refused by the field's NAME rather than silently matching nothing: a filter that quietly returns an empty page is indistinguishable from "there is nothing here".

type LimitID

type LimitID string

LimitID — id of a Limit. Hyphen canon `lim-<17>` (ids.PrefixLimitHyphen), immutable for the life of the resource and the only externally addressable identity (core rule #15).

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 AuthorityStatedKinds

func AuthorityStatedKinds() []LimitKind

AuthorityStatedKinds — виды каталога, чью величину назначает ВНЕШНИЙ авторитет.

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

func CountableKinds

func CountableKinds() []LimitKind

CountableKinds returns just the kinds of the catalogue, in catalogue order.

func CountableKindsOfService

func CountableKindsOfService(service string) []LimitKind

CountableKindsOfService returns the catalogue entries owned by one service, in catalogue order. An unknown service yields an empty slice — and the caller must treat that as "this service counts nothing", not as "every kind".

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.

func (LimitKind) Validate

func (k LimitKind) Validate() error

Validate — membership in the closed catalogue, refused by the field's name.

Membership is the only check needed: the catalogue admits two shapes and no others, and every entry in it is proved well-formed and type-resolvable by authzmap's gates. A token of four parts, or of two parts naming nothing, is simply not a member.

type LimitScope

type LimitScope string

LimitScope — where a ceiling is stated. Three arms, ordered by specificity: PROJECT beats ACCOUNT beats DEFAULT.

const (
	// LimitScopeDefault — platform-wide fallback; exactly one row per kind.
	LimitScopeDefault LimitScope = "DEFAULT"
	// LimitScopeAccount — stated for one account.
	LimitScopeAccount LimitScope = "ACCOUNT"
	// LimitScopeProject — stated for one project; the most specific arm.
	LimitScopeProject LimitScope = "PROJECT"
)

LimitScope values. Mirrored by a DB CHECK.

func (LimitScope) Specificity

func (s LimitScope) Specificity() int

Specificity — how strongly this scope overrides the others. Higher wins.

It is a METHOD rather than a comparison written at each resolve site: the precedence rule is one rule, and three places asserting it independently would disagree the first time a fourth scope appeared.

func (LimitScope) Validate

func (s LimitScope) Validate() error

Validate — the scope is one of the three named arms.

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 — АККАУНТЫ, под которыми лежит объект. Множественное
	// число здесь не запас на будущее: у ЛИЧНОСТИ принадлежность аккаунту
	// перестала быть свойством её строки и стала отдельной связью, которых у
	// человека бывает несколько (#470/#471), поэтому скалярное поле называло бы
	// ОДИН аккаунт из многих — и выдача второго аккаунта не находила бы своего
	// человека by construction (#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.

ЧЛЕНСТВО ЗДЕСЬ — «хотя бы одно», и это не послабление. У личности аккаунтов столько, сколько у человека членств (#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 OAuthClientID

type OAuthClientID string

OAuthClientID — opaque hydra client id (length 1..128, [A-Za-z0-9._:-]).

func (OAuthClientID) Validate

func (h OAuthClientID) Validate() error

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 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 — АВТОР применения, вырезавшего этот тип (#2005). Та же величина
	// и тот же источник, что у соседа: вопрос «кто снял» у обеих ведомостей
	// общий — арендатор не различает, какой из проекций правила он лишился.
	//
	// ПУСТАЯ строка означает «строка вырезана ДО заведения колонки».
	AppliedBy string
}

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

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

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

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

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

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

type PublishedKey

type PublishedKey struct {
	KID          KeyID
	Algorithm    SigningAlgorithm
	PublicKeyPEM string
}

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

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

type RecoveryCompletion

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

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

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 is the closed-table {type,id} per-object pointer (redesign-2026 F8 / B1). Unlike a generic Referrer it carries NO name — the iam target is the strict {type,id} form. `Type` is the dotted `<module>.<resource>` key.

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 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, kacho-system.*), непусто у роли, объявленной
	// манифестом этого модуля.
	//
	// Носит ровно ОДИН смысл — владение, — и потому отделяет послабление
	// подстановки от кластерного якоря: `IsSystem` продолжает означать «арендатор
	// эту роль не правит» и больше не означает «этой роли можно подставлять
	// звёздочку». Политика выводится из пары ОДНИМ местом — [PolicyOfRole].
	//
	// Наружу не проецируется: `Role` публичного контракта этой колонки не несёт
	// (задача продукта #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 — целость роли: даёт ли она то, что объявляет (#1035).
	//
	// ВЫВОДИТСЯ НА ЧТЕНИИ и НЕ ХРАНИТСЯ: колонки у неё нет, в перечень колонок
	// писателя она не входит и [Role.Validate] её не судит. Нулевое значение
	// означает «этим путём не вычислено» — так её и видят пути, которые роль не
	// читают, а возвращают эхом мутации.
	Integrity RoleIntegrity

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

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

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

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

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

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

  • is_system=true + ClusterID set: system role (`kacho-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) 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 `kacho-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 — кто снял. Сегодня производитель один — путь старта, и он
	// называет процессного актора; глагол применения придёт с `#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 Rule

type Rule struct {
	Module        string // exactly one module per rule
	Resources     []string
	Verbs         []string
	ResourceNames []string
	MatchLabels   map[string]string
}

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

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

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

Почему политик ТРИ, а не две (задача продукта #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 — модуль-владелец роли; пусто у арендаторской и платформенной.

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
}

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.

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
}

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 reports whether the role's authored rules grant verbs on the dotted `<module>.<resource>` type (redesign-2026 F9 gate 3 / IAM-1-24). A rule covers the type when its Module matches and its Resources list the resource (or "*"). Empty rules cover nothing.

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

type SAOAuthClientID

type SAOAuthClientID string

SAOAuthClientID — новый формат `soc<17-crockford>` (corelib `ids.NewID`, без подчёркивания). id существующих строк immutable (id = Hydra client id + JWK kid), поэтому валидатор принимает и legacy `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 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
	OAuthClientID   OAuthClientID
	Description     Description
	CreatedByUserID UserID
	CreatedAt       time.Time
	ExpiresAt       *time.Time
	LastUsedAt      *time.Time

	// PublicKeyPEM — SPKI-encoded ECDSA P-256 public key registered with
	// Hydra as a JWK. Empty for legacy rows that pre-date the private_key_jwt
	// mode (migrated with DEFAULT ”) AND for FEDERATED rows where the
	// key material lives in the external IdP rather than kaname.
	PublicKeyPEM string
	// KeyAlgorithm — JOSE alg of the registered key. One of {"ES256",
	// "RS256", "EdDSA"}. Empty for legacy rows; new private_key_jwt keys
	// always set "ES256"; federated rows leave it empty.
	KeyAlgorithm string

	// TrustedSubjects — федеративный вид ключа. Непустой перечень означает, что
	// удостоверение предъявляет ВНЕШНИЙ издатель по RFC 7521/7523: своего
	// ключевого материала строка не несёт вовсе, а подпись проверяется ключом
	// издателя из НАШЕГО перечня доверенных издателей (задача #1124), который
	// читает проверяющий утверждения на пути запроса.
	//
	// Каждый элемент сужает, какая внешняя пара `(iss, sub)` вправе выступать
	// за этот ключ. Пустой перечень — обычный вид с ключевым материалом.
	TrustedSubjects []TrustedSubject

	// DeclaredAudiences — сужение адресатов, ОБЪЯВЛЕННОЕ заказчиком при выдаче
	// (`IssueSAKeyRequest.audience`, задача #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 (Hydra static client).

private_key_jwt mode: kaname mints an ECDSA P-256 keypair per SA key, registers the public JWK with Hydra (`token_endpoint_auth_method = private_key_jwt`), and returns the private PEM to the caller exactly once. Hydra stores only the JWK; kaname keeps the SPKI public PEM (for rotation diagnostics) plus the algorithm. The legacy `client_secret_basic` flow is dropped: no secret ever exists.

1:1 SA→client.

func (ServiceAccountOAuthClient) Validate

func (c ServiceAccountOAuthClient) Validate() error

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

func (s Subject) Validate() error

Validate — self-validating subject: closed type + non-empty id.

type SubjectID

type SubjectID string

SubjectID — opaque id (user, service_account, or group). The paired SubjectType defines the semantics.

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, security.md).

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.

const (
	SubjectTypeUser           SubjectType = "user"
	SubjectTypeServiceAccount SubjectType = "service_account"
	SubjectTypeGroup          SubjectType = "group"
)

SubjectType values.

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

Точная форма субъекта требуется потому, что доверие выдаётся ПОИМЁННО: запись, покрывающая субъектов образцом, называет тех, кого выдававший не перечислял, и установить их состав нельзя ни по записи, ни по журналу.

Перечень — НАША таблица (задача #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
	// 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) 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
	// OAuthClientID — идентификатор клиента у ВНЕШНЕГО поставщика.
	//
	// У строк нового выпуска ПУСТ и обязан быть пуст: выдача больше не заводит
	// клиента у поставщика, а пустое значение здесь означает ровно это —
	// регистрации нет. Непустое значение принадлежит строке прежнего выпуска и
	// держит окно двух издателей: отчеканенные поставщиком токены таких строк
	// действительны до своего истечения.
	//
	// На пути разрешения клиента эта колонка НЕ участвует (см.
	// repo/kaname/pg.AssertionClientRepo).
	OAuthClientID   OAuthClientID
	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 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 — АВТОР применения, снявшего эту строку (#2005): проверенная
	// личность вызывающего на пути глагола либо названный процессный актор на
	// пути старта. НЕ учётка, под которой исполнялась транзакция.
	//
	// Для разбора «почему у меня отобрали право» это второй по важности вопрос
	// после «почему»: причина называет, ЧТО случилось с платформой, а автор — КТО
	// это сделал.
	//
	// ПУСТАЯ строка означает «строка переселена ДО заведения колонки», а не
	// «автора потеряли»: восстановить его у таких строк не из чего.
	AppliedBy string
	// Cause — ПОЧЕМУ строка переселена (#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