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
- Variables
- func Install(dsl string) error
- type AdmissionReport
- type Finding
- type Plans
- func (p *Plans) Declares(objectType, relation string) bool
- func (p *Plans) DeclaresType(objectType string) bool
- func (p *Plans) Model() *authzplan.Model
- func (p *Plans) Plan(objectType, relation string) (authzplan.Plan, error)
- func (p *Plans) RelationNames(objectType string) ([]string, bool)
- func (p *Plans) RelationSubjects(objectType, relation string) (RelationSubjects, bool)
- type RelationSubjects
- type Rule
Constants ¶
const ( // KindUser — человек. KindUser = "user" // KindServiceAccount — служебная запись. KindServiceAccount = "serviceAccount" // KindGroup — группа; в каноне это членство (`group#member`), а не сам // объект группы. KindGroup = "group" )
Виды получателя В НАПИСАНИИ МАНИФЕСТА. Перечень закрыт: манифест берёт его у контракта `SubjectType`, и третьего написания того же предмета не заводится.
Variables ¶
var DSL string
DSL — каноническая модель.
Побайтовая копия `proto/kaname/cloud/iam/v1/fga_model.fga`. Порождается `make -C deploy fga-model-embed`; правится ТОЛЬКО канонический файл.
var ErrModelAlreadyInstalled = errors.New(
"authzmodel: модель процесса уже установлена — второй установки не существует")
ErrModelAlreadyInstalled — модель процесса уже установлена.
var ErrModelAlreadyRead = errors.New(
"authzmodel: модель процесса уже прочитана — установка после первого чтения есть тихая замена")
ErrModelAlreadyRead — модель процесса уже прочитана, и установка запрещена.
Сигнальная, а не текстовая: вызывающий — композиционный корень — принимает по этому различию решение об ОТКАЗЕ В ПУСКЕ, и сравнение по подстроке сломалось бы на первой правке формулировки, молча и в сторону продолжения.
Functions ¶
func Install ¶
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 — одна находка допуска.
Она называет КООРДИНАТУ, а не «модель не собралась»: цена эксплуатации — объявленная ось решения, и отказ обязан говорить оператору, что править.
type Plans ¶
type Plans struct {
// contains filtered or unexported fields
}
Plans — разобранная модель с запомненными планами вывода.
func New ¶
New разбирает НАЗВАННУЮ модель.
Существует потому, что у вопроса «что принимает объявление отношения» есть вторая сторона, и живого входа у неё нет: чтобы доказать, что судья отвечает «не принимает» там, где не принимает, нужен канон, которого в продукте нет. Подать его можно только текстом.
Не «конструктор для проб»: Shared выражен через него, поэтому разбор вшитой модели и разбор названной — ОДИН код. Заведи их порознь — и доказательство поехало бы по одной ветке, а продукт по другой.
Непонятое НЕ пропускается: вход, каноном не являющийся, даёт ошибку, а не пустую модель. Пустая модель отвечала бы «такого отношения нет» на всякий вопрос — уверенно и о модели, которой не существует.
func Shared ¶
Shared отдаёт единственный разбор модели ПРОЦЕССА.
Модель процесса — установленная Install либо, когда установки не было, вшитая. Второго экземпляра не существует: два разбора дали бы форму, где входной контроль RPC отвергает корректный запрос, а вердикт его разрешает.
Ошибка разбора возвращается, а не паникует: вызывающий обязан решить, что с ней делать. У сборки, где эта ошибка возможна, ровно один разумный исход — отказать в старте, — но принимать его за вызывающего значило бы уронить и те пути, которым модель не нужна.
func (*Plans) Declares ¶
Declares — объявляет ли модель отношение `relation` у типа `objectType`.
ЭТО ПРЕДПОСЫЛКА КОМПИЛЯЦИИ, СПРОШЕННАЯ ОТДЕЛЬНО. Plan разбирает вывод только после того, как убедился в двух вещах: тип объявлен и отношение у него есть. Входной контроль RPC обязан судить ТОТ ЖЕ набор — иначе пара из зазора между принимаемым и объявленным доезжает до компиляции и возвращает вызывающему внутреннюю ошибку на КОРРЕКТНОМ запросе (#1290). «Мы сломались» — неверное сообщение: сломались не мы, запрос назвал пару, которой не бывает.
Равенство двух суждений не объявлено, а ДОКАЗАНО по всей модели: declares_test.go обходит все типы × все имена отношений и требует `Declares(T,R) == (Compile(T,R) без ошибки)` в ОБЕ стороны, печатая объём осмотренного.
func (*Plans) DeclaresType ¶
DeclaresType — объявлен ли САМ тип.
Нужен, чтобы отказ называл ВИНОВНОЕ поле: у необъявленного типа не объявлено ни одно отношение, поэтому сообщение про отношение увело бы вызывающего править не ту координату. Та же предпосылка, на которой компиляция останавливается первой (`authzplan.ErrTypeNotDeclared`).
func (*Plans) Plan ¶
Plan отдаёт план вывода для отношения типа.
Отсутствие типа или отношения — ОШИБКА, а не пустой план. Пустой план дал бы вердикт «нет» на вопрос, которого модель не знает: отказ, объяснимый только опечаткой в имени, ищут в правах и не находят.
func (*Plans) RelationNames ¶
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" )