ownerregister

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: 8 Imported by: 0

Documentation

Overview

Package ownerregister — ОДНА форма синхронной регистрации ресурса у владельца прав, общая для всех сервисов-потребителей.

Зачем один пакет, а не пять регистраторов

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

  • заголовок регистратора nlb объявлял себя зеркалом vpc, а его же тело двадцатью строками ниже разбирало, почему это ложь (версия у vpc из writer-транзакции, у nlb — с часов доставки);
  • тот же разбор ссылался на функцию, шагающую версию по tuple'ам, «в NewRegisterApplier» nlb — такой функции у nlb нет вовсе, она живёт у registry (предикат: `git grep -n stepSourceVersion` даёт один файл, и это не nlb).

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

Правило выбора семантики: САМАЯ УЗКАЯ, а не самая снисходительная

Общий код, принявший то, что строгий сервис отвергал, вернул бы отвергнутый дефект во ВСЕ сервисы разом. Поэтому по каждой оси взято самое строгое из существовавшего, и рядом названо, чей это был выбор и что он отвергает:

  1. МАРКЕР ВЕРСИИ — из writer-транзакции, явный (форма vpc). Обязателен: Registration с нулевым SourceVersion не отправляется вовсе (см. ErrUnversioned). Отвергает часы момента доставки (storage/compute/nlb) и полное отсутствие версии (registry).
  2. НЕСКОНФИГУРИРОВАННЫЙ КЛИЕНТ — отказ (форма registry): New возвращает ErrNoClient. Отвергает молчаливый no-op (nlb), при котором ускоритель выключен и это ничем не наблюдается.
  3. ЛИЧНОСТЬ ВЫЗЫВАЮЩЕГО — пробрасывается всегда (форма compute/nlb), чтобы least-priv-гейт принимающей стороны судил реального вызывающего, а не только предъявленный сертификат. Отвергает молчаливую потерю личности (vpc/storage/registry).
  4. ОТКАЗ НА ОДНОМ tuple'е — не прекращает набор (форма nlb): пробуются ВСЕ, ошибки объединяются. Отвергает короткое замыкание (vpc/storage/registry), при котором указатель принадлежности не доезжает из-за соседа по набору, а вызывающий видит один отказ вместо всех.
  5. ПОЛЯ ЗЕРКАЛА — форвардится всё, что у доставки есть, включая TraceID и ParentAccountID (форма nlb — самая полная). Пустое поле принимающая сторона обрабатывает штатно; недосланное — молча обедняет зеркало.

Чего здесь НЕТ и почему

Ветки «AlreadyExists — это успех» нет. Контракт RegisterResource идемпотентен и этого кода НЕ производит (см. godoc RegisterResourceUseCase и handler'а internal_iam: «neither AlreadyExists nor NotFound ever surfaces»). Ветка с отсутствующим производителем — это форма без содержания: она молчит на том, ради чего написана, и однажды проглотит настоящий отказ.

Признак первой доставки сюда НЕ добавляется

Он уже landed ПОЛЕМ ДЕСКРИПТОРА сервиса (corelib/servicecontract), а не параметром запроса. Слот под него в Registration был бы вторым местом об одном предмете; приёмка XC-7 §10 п.3 это прямо отвергает. Принимающая сторона принимает признак лишь В КОНЪЮНКЦИИ со своим вердиктом по данным — правило «это не слово вызывающего» сохраняется дословно и здесь не ослабляется.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoClient — регистратор собран без клиента к владельцу прав.
	//
	// Это ОТКАЗ, а не пустая операция. Пустая операция выглядит как исправно
	// работающий ускоритель, который никогда ничего не ускорял: ни одного
	// отказа за всю жизнь, ни одной строки в логе, и отличить её от «всё
	// хорошо» нечем (security.md §Hardening-инвариант 8).
	ErrNoClient = errors.New("owner-register: клиент владельца прав не сконфигурирован")

	// ErrUnversioned — регистрация не несёт маркера версии из writer-транзакции.
	//
	// РАЗМЕН НАЗВАН ЯВНО. Отказ означает, что эта регистрация не будет доставлена
	// синхронно, и окно видимости гранта расширяется до дренажа durable-очереди —
	// корректность при этом сохраняется полностью (доставка at-least-once уже
	// лежит в очереди той же writer-транзакции). Альтернатива — отправить без
	// версии — корректна тоже, но принимающая сторона тогда не имеет
	// доказательства редоставки и ОТКРЫВАЕТСЯ в сторону работы: сервис платит за
	// обе доставки на каждом создании, тихо и навсегда. Из двух исходов громкий
	// выбран намеренно: непротащенный маркер — ошибка программиста, и она обязана
	// быть слышна, а не оплачиваться в рантайме.
	ErrUnversioned = errors.New("owner-register: регистрация без маркера версии из writer-транзакции")
)

Functions

func ParentChain

func ParentChain(explicit []string, projectID, accountID string) []string

ParentChain — цепь предков ОДНОЙ доставки регистрации, от ближайшего предка к дальнему, каждый элемент в форме `"<type>:<id>"`.

Зачем это отдельная функция, а не поле, заполняемое на месте

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

Что функция делает и чего НЕ делает

explicit непуст ⇒ возвращается как есть. Иерархию глубже двух уровней
(репозиторий → реестр → проект) знает только владелец, и вывести её из
области нельзя: область знает ровно проект и аккаунт. Подмена укоротила бы
цепь до той, которой у объекта нет.

explicit пуст ⇒ цепь выводится ИЗ ОБЛАСТИ ЭТОЙ ЖЕ ДОСТАВКИ — из тех самых
`ParentProjectID`/`ParentAccountID`, что едут в том же сообщении. Ничего
нового не выдумывается: доставка уже утверждает эти два предка, функция лишь
называет их в форме цепи.

Пустой explicit означает «владелец не назвал», а НЕ «предков нет»: строка, поставленная в очередь до появления поля, декодируется именно так, и принять её за утверждение значило бы стирать предков на каждом повторе. «Предков нет» выражается пустой ОБЛАСТЬЮ — тогда результат тоже пуст, и это законный ответ для объекта корневого уровня.

Цепь непрерывна: без проекта ближайшим предком становится аккаунт. Дыра остановила бы обход вверх и объявила объект не принадлежащим аккаунту.

Types

type RegisterRPC

type RegisterRPC interface {
	RegisterResource(ctx context.Context, in *iamv1.RegisterResourceRequest, opts ...grpc.CallOption) (*iamv1.RegisterResourceResponse, error)
}

RegisterRPC — узкий порт к владельцу прав. Именно узкий, а не весь InternalIAMServiceClient: подставной дублёр обязан выполнять контракт настоящего, и чем меньше поверхность, тем меньше ему есть где оказаться снисходительнее.

type Registrar

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

Registrar — синхронная доставка регистраций владельцу прав.

УСКОРИТЕЛЬ, А НЕ ИСТОЧНИК ИСТИНЫ. Те же строки лежат durable-намерениями в очереди той же writer-транзакции, поэтому дренаж доводит регистрацию сам. Отсюда и форма отчётности: ошибка ВОЗВРАЩАЕТСЯ вызывающему, но вызывающий её логирует, а не роняет мутацию — провалить уже закоммиченный ресурс значило бы отдать клиенту отказ на существующую вещь (фантом, ban #9).

func New

func New(cli RegisterRPC) (*Registrar, error)

New собирает регистратор. nil-клиент — ОТКАЗ (см. ErrNoClient).

func (*Registrar) Register

func (r *Registrar) Register(ctx context.Context, regs []Registration) error

Register доставляет каждую регистрацию набора владельцу прав.

НЕ КОРОТКОЗАМЫКАЕТСЯ: отказ на одной строке не отменяет попытки по остальным, и все отказы возвращаются объединёнными. Причина не в полноте отчёта, а в том, ЧТО стоит первым в наборе: указатель принадлежности объекта. Через него администратор аккаунта достаёт объект вообще; потерять его из-за отказа на соседнем tuple'е значит потерять весь административный ярус на этом ресурсе до дренажа.

type Registration

type Registration struct {
	Tuple Tuple

	// TraceID — идентификатор ресурса, к которому относится доставка.
	TraceID string

	// Labels / ParentProjectID / ParentAccountID — кормление зеркала владельца
	// прав. Пустое значение законно (не у всякого ресурса есть метки, и не
	// всякий сервис резолвит проект→аккаунт на горячем пути).
	Labels          map[string]string
	ParentProjectID string
	ParentAccountID string

	// ParentChain — цепь предков от БЛИЖАЙШЕГО к дальнему, каждый элемент в
	// форме `"<type>:<id>"`.
	//
	// Двух полей выше хватает не всякому ресурсу: они несут ровно проект и
	// аккаунт, а модель прав требует цепи ПРОИЗВОЛЬНОЙ формы (репозиторий →
	// реестр → проект → аккаунт). Ресурс, чей ближайший предок не проект и не
	// аккаунт, без этой цепи регистрируется БЕЗ предка — и молча выпадает из
	// области выдачи и из каскада: вопрос о нём задаётся, ответ «нет», и это
	// неотличимо от честного отказа.
	//
	// Пусто означает «предков нет»: принимающая сторона заменяет набор рёбер
	// объекта целиком, поэтому доставка без цепи не «ничего не меняет», а
	// стирает уже записанных предков. Заполнять цепь обязан КАЖДЫЙ потребитель;
	// у кого иерархия не глубже проекта — вызовом [ParentChain] по области этой
	// же доставки, у кого глубже — своим перечислением звеньев.
	ParentChain []string

	// SourceVersion — маркер, проштампованный БД ВНУТРИ writer-транзакции,
	// породившей это намерение. Не часы момента доставки: обе доставки одной
	// строки обязаны нести ОДНО значение, иначе гашение редоставки у принимающей
	// стороны зависит от того, кто выиграл гонку. Нулевое значение — отказ
	// ([ErrUnversioned]), а не «сойдёт и так».
	SourceVersion time.Time
}

Registration — ОДНА строка durable-намерения, готовая к синхронной доставке.

Единица — именно tuple, а не «намерение целиком», и это существенно. Гейт редоставки у принимающей стороны ключуется на строке зеркала, а она ПО ОБЪЕКТУ; при этом одна доставка законно несёт на ОДИН объект несколько tuple'ов (у registry — указатель принадлежности, затем владелец-создатель). Была бы версия одна на набор — второй tuple не менял бы зеркала, был бы неотличим от редоставки и проглатывался вместе с постановкой владельца. Поэтому версию несёт КАЖДАЯ строка, ровно та, которой БД проштамповала её в writer-транзакции: у registry триггер даёт разные clock_timestamp() на разные строки, у nlb эмиттер шагает порядковым номером внутри транзакции, у vpc/storage/compute now() одинаков — но там на один объект приходится одна строка, и совпадение безвредно.

type Tuple

type Tuple struct {
	SubjectID string
	Relation  string
	Object    string
}

Tuple — отношение, которое регистрация ставит на объект.

Jump to

Keyboard shortcuts

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