authzmodel

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

Documentation

Overview

Package authzmodel — каноническая модель прав, доступная коду iam в рантайме.

───────────────────────────────────────────────────────────────────────────── ЗАЧЕМ ЭТО СУЩЕСТВУЕТ

Реляционная форма вердикта обязана отвечать на ТОТ ЖЕ вопрос, что движок, а модель выводит отношения: право читателя следует из права редактора, право редактора — из права администратора, право администратора аккаунта распространяется вниз. Форма, ищущая имя отношения буквально, ответит «нет» держателю права, которое модель ему даёт, — и это не оттенок, а отказ.

Значит выводу нужен разбор модели, а разбору — сама модель, в процессе.

───────────────────────────────────────────────────────────────────────────── ПОЧЕМУ ПРОИЗВОДНАЯ КОПИЯ, А НЕ ЧТЕНИЕ ФАЙЛА В РАНТАЙМЕ

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

Цена копии — расхождение с источником, и она уплачена целиком: производная порождается целью сборки, а гейт побайтового равенства роняет прогон при расхождении (`identity_test.go`).

КОПИЙ ДВЕ: каноническая и эта. Здесь стояло «теперь три — каноническая, настройка кластера и эта»; третьей нет. Настройка кластера несла модель, пока её применял внешний движок отношений, — движок снят целиком (S6, эпик #747), а с ним подчарт и карта, которую тот подчарт монтировал. Их имена здесь намеренно НЕ воспроизводятся координатой: мёртвое имя, записанное обратными кавычками, читается как живое.

Утверждение пережило свой предмет и обошлось дороже опечатки: читатель, ищущий третью копию, ищет то, чего нет, и заводит гейт дрейфа для несуществующей производной. То же самое соседний пакет уже говорит прямо (`services/iam/internal/authzmap` — шапка гейта дрейфа).

───────────────────────────────────────────────────────────────────────────── ПОЧЕМУ РАЗБОР ОДИН РАЗ, А НЕ НА ЗАПРОС

Модель неизменна в пределах процесса: она вшита. Разбирать её на каждый вопрос значило бы платить разбором ВСЕХ её отношений — а их сотни — за каждое решение о доступе. Число здесь намеренно не названо: оно двигается с каждой правкой модели, и замороженным оно уже было — стояло «244» при 277 объявлениях в дереве, то есть довод опирался на величину, неверную на треть. Разбирается один раз, планы считаются по требованию и запоминаются — первый вопрос об отношении платит за разбор своего вывода, последующие не платят.

relationsubjects.go — КАКИЕ ВИДЫ ПОЛУЧАТЕЛЯ принимает объявление отношения.

Задача продукта #1936, приёмка `services/iam/docs/engineering/acceptance/module-manifest-relation-grant.md` §3.3 и §3.5 (APPROVED круг 1).

Почему чтение живёт ЗДЕСЬ, а не у вызывающего

Вызывающих у него два — загрузчик манифеста (судит объявленную выдачу) и сверка посева (судит живую строку), — и оба спрашивают об одном: что канон принимает получателем этого отношения. Копия у каждого разошлась бы молча, потому что расходятся такие копии только на невалидном входе.

Дом выбран не по удобству: здесь уже живёт СОСЕДНИЙ вопрос о том же предмете — Plans.Declares («объявляет ли модель это отношение у этого типа»), и обе стороны обязаны судить ОДИН набор. Разведённые по разным пакетам, они дали бы зазор между принимаемым и объявленным ровно там, где он не виден.

Прежде чтение жило в пакете сверки посева и было заведено ради ПРОБЫ ПРЕДПОСЫЛКИ, которую задача #1936 снимает вместе с её предметом. Оставить его там значило бы держать читатель в пакете, у которого предмета не осталось.

Собственного разбора канона здесь НЕ заводится, и довод уже оплачен

Разбор общий — `services/iam/internal/authzplan`. Свой разбор в этом дереве уже писали: он сравнивал запись со СТРОКОЙ `group#member` и потому читал `group#member with <условие>` как «членство не принимается», отвечая уверенно и неверно. Направление ошибки было худшим из двух — судья оставался зелёным.

Перевод написаний тоже ОДИН

Манифест пишет вид субъекта `serviceAccount`, канон — `service_account`. Перевод делается здесь, у владельца канона; второе написание того же предмета разошлось бы с первым молча.

Ответов ТРИ, а не два

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

Index

Constants

View Source
const (
	// KindUser — человек.
	KindUser = "user"
	// KindServiceAccount — служебная запись.
	KindServiceAccount = "serviceAccount"
	// KindGroup — группа; в каноне это членство (`group#member`), а не сам
	// объект группы.
	KindGroup = "group"
)

Виды получателя В НАПИСАНИИ МАНИФЕСТА. Перечень закрыт: манифест берёт его у контракта `SubjectType`, и третьего написания того же предмета не заводится.

Variables

DSL — каноническая модель.

Побайтовая копия `proto/kaname/cloud/iam/v1/fga_model.fga`. Порождается `make -C deploy fga-model-embed`; правится ТОЛЬКО канонический файл.

View Source
var ErrModelAlreadyInstalled = errors.New(
	"authzmodel: модель процесса уже установлена — второй установки не существует")

ErrModelAlreadyInstalled — модель процесса уже установлена.

View Source
var ErrModelAlreadyRead = errors.New(
	"authzmodel: модель процесса уже прочитана — установка после первого чтения есть тихая замена")

ErrModelAlreadyRead — модель процесса уже прочитана, и установка запрещена.

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

Functions

func Install

func Install(dsl string) error

Install ставит НАЗВАННЫЙ текст моделью процесса (#1969, §2 п. 7-8).

Почему явный вход, а не ленивый поставщик

Ленивый поставщик внутри Shared сделал бы момент сборки функцией того, кто первым спросил модель, — а спрашивают её пути с РАЗНЫМИ входами: разбор доставки читает её лишь на манифестах, объявивших выдачу отношения. Тогда стенд поднимался бы на одних манифестах и вставал на других, недетерминированно и неотличимо от «условие не создано».

Почему присваивание [DSL] установкой НЕ является

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

Types

type AdmissionReport

type AdmissionReport struct {
	// Judged — суждение СОСТОЯЛОСЬ: канон разобран, собранный текст разобран, и
	// правила исполнены (либо остановлены Д7, что тоже есть вердикт).
	//
	// Поле существует ради УМОЛЧАНИЯ ТИПА, а не ради диагностики. [Admit] на
	// входе, который не разобрался, возвращает ошибку И отчёт; отчёт этот —
	// нулевой, а у нулевого значения `len(Findings) == 0` и
	// `NothingToJudge == false`. Без этого признака нулевой отчёт отвечал бы
	// «допущено» и печатал «находок 0», то есть читался бы как ЧИСТЫЙ — ровно на
	// том входе, ради которого клауза fail-closed заведена (#2000).
	//
	// Контракт «вызывающий обязан проверить err раньше Admitted()» исполним, но
	// исполним только тем, кто о нём помнит: естественная форма
	// `rep, err := Admit(x); if !rep.Admitted() { … }` даёт fail-open. Величина,
	// которую построение подставляет молча, предметом стража быть не может
	// (`security.md` §«Пустой список — это „не сужаем"»), поэтому строгость здесь
	// держится УМОЛЧАНИЕМ, а не дисциплиной вызывающего: число вызывающих будет
	// расти, а нулевое значение останется одно.
	Judged bool
	// TypesSeen — сколько блоков типа осмотрено (перечень, а не множество имён).
	// Ноль означает, что до разбора дело не дошло, и [AdmissionReport.Census] это
	// говорит: «ноль находок» обязано быть отличимо от «ноль прочитанного».
	TypesSeen int
	// TypesNew — сколько РАЗЛИЧНЫХ имён собранной модели канон не несёт.
	TypesNew int
	// Findings — находки в порядке правил и координат, детерминированном.
	Findings []Finding
	// NothingToJudge — третий исход. Предикат ОДИН: TypesNew == 0. Прежние два
	// признака («суффикс пуст» и «текст равен канону») расходились на тексте
	// «канон + перевод строки»; выбран тот, что совпадает с предметом правила:
	// судить нечего ровно тогда, когда судить некого.
	NothingToJudge bool
	// TerminatedAtD7 — правила после Д7 не исполнялись. Без этого признака
	// «находок 1» читалось бы как «остальное чисто».
	TerminatedAtD7 bool
	// ComputedOnlyWildcards — величина СОБСТВЕННОЙ ПРЕМИССЫ допуска: сколько пар
	// достигают подстановки ТОЛЬКО шагом `Term.Computed` внутри канонического
	// типа. Замыкание сокращено до двух рёбер потому, что третье ведёт в
	// отношение ТОГО ЖЕ типа, а посев уже включает каждое объявление нового типа;
	// довод верен для шага внутри НОВОГО типа и не покрывает шаг внутри типа
	// КАНОНИЧЕСКОГО. Поэтому замыкание, войдя в канонический тип, идёт по
	// `Computed` внутри него, а эта величина печатается ВСЕГДА: иначе премисса
	// осталась бы утверждением без производителя — той же формой без содержания,
	// только этажом выше клаузы.
	ComputedOnlyWildcards int
}

AdmissionReport — отчёт допуска.

Исходов ТРИ: допущено всё · находка · СУДИТЬ НЕЧЕГО. Третий в успех НЕ засчитывается и из вердикта НЕ вычитается.

func Admit

func Admit(composed string) (AdmissionReport, error)

Admit судит собранную модель против канона ОБРАЗА.

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

Вызывающий обязан вести себя fail-closed: композиция, не прошедшая допуск, не «пропускается с предупреждением».

Канон разбирается ЗДЕСЬ, а не берётся у Shared: допуск — чистая функция, и ответ её не вправе зависеть от того, наполнил ли кто-то запомненные планы раньше. Цена — один разбор канона на вызов; допуск зовётся на пути ПУСКА, а не на пути запроса.

func (AdmissionReport) Admitted

func (r AdmissionReport) Admitted() bool

Admitted — допущена ли композиция.

Утверждение ПОЛОЖИТЕЛЬНОЕ: допущено ровно то, что было СУЖДЕНО и не дало находок. Ни третий исход, ни несостоявшееся суждение успехом НЕ являются, и отличать их друг от друга здесь не требуется — оба означают «не допущено».

func (AdmissionReport) Census

func (r AdmissionReport) Census() string

Census — перепись. Печатается ВСЕГДА, в том числе при находке и при третьем исходе.

type Finding

type Finding struct {
	// Rule — правило, которое нарушено.
	Rule Rule
	// Type — тип, о котором находка. Пусто у Д7: там ещё не разобрано ничего.
	Type string
	// Relation — отношение, о котором находка. Пусто, когда находка о типе целиком.
	Relation string
	// Term — терм либо запись субъекта, сделавшая находку. Пусто, когда предмет
	// находки не терм.
	Term string
	// Text — текст для оператора. Всякий перечень в нём ПРИВЕДЁН К ПОРЯДКУ:
	// неотсортированный делает текст функцией случая, а пробу — флейкующей by
	// construction.
	Text string
}

Finding — одна находка допуска.

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

func (Finding) String

func (f Finding) String() string

type Plans

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

Plans — разобранная модель с запомненными планами вывода.

func New

func New(dsl string) (*Plans, error)

New разбирает НАЗВАННУЮ модель.

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

Не «конструктор для проб»: Shared выражен через него, поэтому разбор вшитой модели и разбор названной — ОДИН код. Заведи их порознь — и доказательство поехало бы по одной ветке, а продукт по другой.

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

func Shared

func Shared() (*Plans, error)

Shared отдаёт единственный разбор модели ПРОЦЕССА.

Модель процесса — установленная Install либо, когда установки не было, вшитая. Второго экземпляра не существует: два разбора дали бы форму, где входной контроль RPC отвергает корректный запрос, а вердикт его разрешает.

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

func (*Plans) Declares

func (p *Plans) Declares(objectType, relation string) bool

Declares — объявляет ли модель отношение `relation` у типа `objectType`.

ЭТО ПРЕДПОСЫЛКА КОМПИЛЯЦИИ, СПРОШЕННАЯ ОТДЕЛЬНО. Plan разбирает вывод только после того, как убедился в двух вещах: тип объявлен и отношение у него есть. Входной контроль RPC обязан судить ТОТ ЖЕ набор — иначе пара из зазора между принимаемым и объявленным доезжает до компиляции и возвращает вызывающему внутреннюю ошибку на КОРРЕКТНОМ запросе (#1290). «Мы сломались» — неверное сообщение: сломались не мы, запрос назвал пару, которой не бывает.

Равенство двух суждений не объявлено, а ДОКАЗАНО по всей модели: declares_test.go обходит все типы × все имена отношений и требует `Declares(T,R) == (Compile(T,R) без ошибки)` в ОБЕ стороны, печатая объём осмотренного.

func (*Plans) DeclaresType

func (p *Plans) DeclaresType(objectType string) bool

DeclaresType — объявлен ли САМ тип.

Нужен, чтобы отказ называл ВИНОВНОЕ поле: у необъявленного типа не объявлено ни одно отношение, поэтому сообщение про отношение увело бы вызывающего править не ту координату. Та же предпосылка, на которой компиляция останавливается первой (`authzplan.ErrTypeNotDeclared`).

func (*Plans) Model

func (p *Plans) Model() *authzplan.Model

Model отдаёт разобранную модель — для гейтов и переписей.

func (*Plans) Plan

func (p *Plans) Plan(objectType, relation string) (authzplan.Plan, error)

Plan отдаёт план вывода для отношения типа.

Отсутствие типа или отношения — ОШИБКА, а не пустой план. Пустой план дал бы вердикт «нет» на вопрос, которого модель не знает: отказ, объяснимый только опечаткой в имени, ищут в правах и не находят.

func (*Plans) RelationNames

func (p *Plans) RelationNames(objectType string) ([]string, bool)

RelationNames — имена отношений, объявленных у типа, по возрастанию.

Существует ради ТЕКСТА отказа: автор, назвавший отношение с опечаткой, обязан узнать не только что ошибся, но и чем это чинить. Перечень БЕРЁТСЯ У КАНОНА, а не выписывается — выписанный разошёлся бы с ним молча.

ok=false — типа канон не объявляет; пустой перечень у объявленного типа законен и означает «отношений нет», а не «типа нет».

func (*Plans) RelationSubjects

func (p *Plans) RelationSubjects(objectType, relation string) (RelationSubjects, bool)

RelationSubjects — объявление отношения `relation` у типа `objectType`.

ok=false означает «канон такого не объявляет»: либо типа нет, либо у типа нет такого отношения. Ответ даётся ПО ОБЪЯВЛЕНИЮ СВОЕГО типа — одноимённое отношение соседнего типа подменять его не вправе, иначе судья ответил бы про чужой предмет.

type RelationSubjects

type RelationSubjects struct {
	// ObjectType и Relation — о чём этот ответ. Оба нужны в текстах отказа:
	// одноимённое отношение соседнего типа судьёй не является, и отказ обязан
	// называть тип, по объявлению которого он вынесен.
	ObjectType string
	Relation   string

	// Direct — были ли у отношения прямые записи субъекта ВООБЩЕ.
	//
	// false означает «отношение вычисляемое», а НЕ «субъектов нет»: разница
	// несущая, см. шапку файла. Вызывающий обязан спросить это ПЕРЕД тем, как
	// толковать пустой [RelationSubjects.Accepts].
	Direct bool

	// Accepts — виды субъекта, принимаемые прямо, В НАПИСАНИИ КАНОНА, без
	// повторов и в устойчивом порядке.
	//
	// Поле существует ради ТЕКСТА отказа: судья показывает автору то, что стоит
	// в каноне. Решения по нему НЕ принимаются — для этого есть
	// [RelationSubjects.AcceptsKind]: сравнение со строкой и было прежней
	// ошибкой.
	Accepts []string
	// contains filtered or unexported fields
}

RelationSubjects — объявление одного отношения у одного типа объекта, прочитанное как ответ на вопрос «кого оно принимает получателем».

func (RelationSubjects) AcceptedKinds

func (s RelationSubjects) AcceptedKinds() []string

AcceptedKinds — виды субъекта, принимаемые прямо, в написании канона.

Метод-зеркало поля RelationSubjects.Accepts; предназначено ТЕКСТУ отказа. Решения по нему не принимаются — для этого есть RelationSubjects.AcceptsKind: сравнение со строкой и было прежней ошибкой.

func (RelationSubjects) AcceptsKind

func (rs RelationSubjects) AcceptsKind(manifestKind string) bool

AcceptsKind — принимает ли объявление получателя названного вида.

Вид называется В НАПИСАНИИ МАНИФЕСТА (`serviceAccount`), перевод делается здесь. Вид вне закрытого перечня даёт `false`: судить о том, чего манифест написать не может, нечем, и «да» здесь было бы вымыслом.

Судится ПАРА (тип субъекта, userset), а не написание записи:

  • условие (`group#member with mfa_fresh`) сужает, КОГДА членство действует, и не отменяет того, ЧТО это членство, — значит принимает;
  • подстановка (`group:*`, `user:*`) называет тип субъекта и членством не является, а прямую выдачу поимённому субъекту не разрешает — значит НЕ принимает.

У вычисляемого отношения ответ `false` сам по себе ничего не значит: вызывающий обязан сперва спросить RelationSubjects.Direct.

func (RelationSubjects) IsDirect

func (s RelationSubjects) IsDirect() bool

IsDirect — были ли у отношения прямые записи субъекта ВООБЩЕ.

Метод-зеркало поля RelationSubjects.Direct. Существует не ради удобства: он вместе с RelationSubjects.AcceptedKinds и RelationSubjects.AcceptsKind составляет то, что спрашивает загрузчик манифестов, и позволяет ему задавать вопрос ИНТЕРФЕЙСОМ, не импортируя владельца модели (#2002).

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

type Rule

type Rule string

Rule — правило допуска. Значения — часть контракта: их читает оператор, и по ним же трассируются сценарии приёмки.

const (
	// RuleD7Prefix — текст собранной модели начинается текстом канона ПОБАЙТОВО.
	// Терминальна: не совпал префикс — о разобранном говорить нельзя, модель
	// может быть чужой целиком.
	RuleD7Prefix Rule = "Д7(а)"
	// RuleD7Suffix — всякая строка за границей префикса — одна из пяти форм:
	// пустая · комментарий · relations · define · type <идентификатор>.
	RuleD7Suffix Rule = "Д7(б)"
	// RuleD1 — имя типа встречается в перечне типов ровно один раз, имя отношения
	// — ровно один раз в перечне объявлений своего типа.
	RuleD1 Rule = "Д1"
	// RuleD3 — ни одна пара (тип, отношение), достижимая из объявлений нового
	// типа, не разрешается подстановкой.
	RuleD3 Rule = "Д3"
	// RuleD4Userset — всякий усерсет нового типа есть отношение, которое
	// тип-носитель действительно объявляет.
	RuleD4Userset Rule = "Д4(а)"
	// RuleD4Condition — всякое условие нового типа — имя из условий канона.
	RuleD4Condition Rule = "Д4(б)"
	// RuleD5 — план каждого объявления нового типа выразим.
	RuleD5 Rule = "Д5′"
	// RuleD8 — у типа не больше одного указателя на каждый тип-предок.
	RuleD8 Rule = "Д8"
)

Jump to

Keyboard shortcuts

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