authzguard

package
v0.7.0 Latest Latest
Warning

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

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

Documentation

Overview

acr_floor.go — the per-RPC `required_acr_min` (step-up / MFA-freshness floor) enforcement on the cluster-internal listener (:9091) for the GATEWAY-FRONTED privileged RPCs.

AuthN+AuthZ-everywhere invariant ("Internal = trusted, mTLS is enough" is a FORBIDDEN assumption): `required_acr_min` is enforced on the public path (api-gateway StepUpGate), but the gateway does NOT re-run that gate when it re-dials :9091 on the caller's behalf — so a privileged gateway-fronted internal RPC with a raised floor would be un-enforced on the internal route. This interceptor closes that arm: the gateway forwards the validated acr as trusted metadata (grpcsrv.MDKeyTokenACR) and the floor enforces the catalog requirement here too.

WHICH RPC CARRIES WHICH FLOOR IS THE CONTRACT'S WORD, NOT THIS COMMENT'S. The floor reads `required_acr_min` from the permission catalog (generated from the proto options of each service) and restates nothing. On InternalClusterService the contract raises the floor for the MUTATIONS of the cluster-admin roster — InternalClusterService/GrantAdmin and InternalClusterService/RevokeAdmin carry required_acr_min=2 — and keeps the READS on the interactive floor: InternalClusterService/Get and InternalClusterService/ListAdmins carry required_acr_min=1. That split is a decision, not an omission (kaname#131): reading who administers the cluster exposes no capability that a password-only session does not already hold (the roster is visible to every system_admin through the catalog anyway), so a second factor is demanded where the roster CHANGES, not where it is looked at. A comment here that named a floor other than the contract's would be a trap: a security comment that contradicts the code makes the next reader "fix" one side to match the other without asking what was decided; acr_floor_header_claims_test.go checks every floor this header names against the embedded catalog.

For each RPC in the GATEWAY-FRONTED set (GatewayFrontedInternalRPCs — caller- context = api-gateway acting for an end user) whose catalog `required_acr_min > 0`, it applies THE step-up rule — grpcsrv.EvaluateStepUp, the single implementation the public api-gateway StepUpGate calls too. This floor decides nothing itself: it selects WHICH calls are subject to the rule (gateway-fronted ∧ production ∧ acr_min>0), reads the inputs from the trusted ctx, and renders the verdict as a gRPC status. It must never re-derive the ranking or the machine exemption — that split is precisely what let a machine principal clear the front door and then be denied here forever (acr_floor_stepup_parity_test.go drives this entrypoint against the shared rule, machine principal included).

Inputs are read ONLY under the trust invariant: grpcsrv.TrustedACRFromContext for the acr and grpcsrv.TrustedPrincipalFromContext for the principal type. On the mTLS-verified gateway→iam edge the gateway forwards x-kacho-token-acr and x-kacho-principal-type; on an unverified/foreign-SAN peer the acr is dropped upstream (corelib) and the principal is flagged untrusted — this floor then passes an EMPTY principal type to the rule, so a forged `service_account` header cannot buy the exemption (anti-spoof), and the absent acr ranks 0 → denied.

EXEMPT (deliberately not enforced here):

  • Non-gateway-fronted internal RPCs (InternalIAMService/Check, /RegisterResource, /InternalAddressService/Allocate*, …) — called by MODULE SAs (vpc/compute/nlb), not by a user. The floor never touches an RPC outside the gateway-fronted set (selection arm, independent of who calls).
  • MACHINE principals on gateway-fronted RPCs — exempt via the shared rule, not via a local branch. A service account has no interactive ceremony and can never present acr ≥ 1, so the floor would deny it permanently rather than protect anything. The exemption lifts assurance only: the in-handler ReBAC Check and internalCallerPolicy (gateway-SAN-only) are untouched.
  • gateway-fronted RPCs whose required_acr_min == 0 — no requirement (latent-until-policy: the floor fires for them the moment policy raises their acr_min, proving the mechanism is generic).

Ordering (serve.go): chained AFTER UnaryTrustedPrincipalExtract (sets the trusted acr) AND internalCallerPolicy (which already DENIES a non-gateway SAN on a gateway-fronted RPC BEFORE the acr-floor — so a compromised module cannot reach the floor with a spoofed acr; the acr-exemption of module SAs cannot be abused). Mirrors the SystemViewerFloor: default-OFF (dev/newman no-op, byte-identical), fail-closed in production.

Package authzguard — minimal per-use-case guard for kaname. It answers "did the caller name itself?", never "is the caller allowed": the per-RPC authorization Check is made against the rights model, at the edge and in the interceptor chain.

Use-cases call RequireAuthenticated(ctx) in the first sync step, BEFORE creating an Operation. If principal-type == "anonymous" (or missing) → PermissionDenied. The bootstrap admin (system/bootstrap) is rejected too — it is only used by backend-internal paths and tests, which bypass guards via WithPrincipal directly.

The layer was written as transitional, "until a permission_map interceptor lands". That interceptor exists and gates every RPC on both listeners, so what remains here is deliberately narrower: an authentication floor at the first sync step of a use-case, not a second opinion about rights.

caller_policy.go — the per-RPC CALLER policy for the cluster-internal listener (:9091). It does NOT re-ReBAC the end user — that is the api-gateway's job (the platform's single authZ front door validates the JWT and runs per-user ReBAC via iam.Check). iam's :9091 enforces only WHO MAY CALL each RPC:

  1. Floor — every internal RPC requires a VERIFIED mTLS module cert (SPIRE SAN spiffe://<домен>/ns/<ns>/sa/kacho-<svc>) in production. dev (no verified cert) → no-op (insecure back-compat, mirror RelationWriteGate). Модулем признаётся ТОЛЬКО пара (пространство, учётка), объявленная установкой (WithModuleRoster, kaname#984), и признанная личность зовёт только методы СВОЕГО профиля (module_profiles.go) — в том числе среди освобождённых от проверки отношений. Пол удовлетворяет ТАКЖЕ хоп собственного REST-фронта службы — лист того же внутреннего центра, чьё имя учётной записи приставки модулей не несёт. Он объявляется корнем (WithOwnFrontHop) и допускается РОВНО к тому, к чему допущен любой проверенный модуль: круга края ниже он не проходит никогда.
  2. Gateway-only — the gateway-fronted privileged admin RPCs (GatewayFrontedInternalRPCs) may ONLY be called by the api-gateway SA. A direct call from any other module (e.g. a compromised kacho-vpc) → DENY in prod (a data-plane module cannot escalate via :9091). dev → no-op.
  3. SAN-restricted — a small set of RPCs whose caller must appear on an EXPLICIT, operator-supplied allow-list of client-certificate SPIFFE SANs (WithSANAllowlist). Unlike arms 1-2 this arm is enforced in EVERY mode and an empty/absent allow-list denies EVERYONE (fail-closed: these RPCs have no default caller). Today: InternalBootstrapTokenService/MintBootstrapToken, which hands out a cluster `system_admin` Bearer and therefore cannot be gated by "any verified module cert" — a compromised data-plane module must not be able to mint cluster-admin — nor by a ReBAC relation (it exists to obtain the FIRST token, when no relation exists yet). The credential is the caller's verified certificate identity; network position is NOT a credential ("internal = trusted" is a forbidden premise).

WHY this replaces the former cert-bound ReBAC interceptor: the api-gateway re-dials :9091 with ITS OWN client cert (SAN .../sa/kacho-api-gateway) and forwards the end-user principal in x-kacho-principal-* metadata. A cert-bound ReBAC Check on the gateway SA would DENY legitimate admin calls (system_admin@ cluster is held by the user, not the gateway SA); ReBAC-ing the forwarded user would couple iam to the gateway's relation map. So iam does NO ReBAC and trusts NO metadata here — it only verifies the caller IS the gateway for admin RPCs.

The fga-proxy write RPCs (RegisterResource / UnregisterResource) are NOT gateway-only — their callers are vpc/compute/nlb MODULE SAs — and stay gated IN-HANDLER by RelationWriteGate (fga_writer), unchanged. They satisfy the floor like any other module RPC.

caller_service.go — признание края и пира на HTTP-слушателях.

Модулем, в том числе краем, признаётся только пара (пространство, учётка), объявленная установкой (`moduleidentity.Roster`, kaname#984). Разбор идентификатора один на все пути признания вызывающего — `moduleidentity.ParseSAN`; здесь стояла своя обёртка над разбором стража записи отношений, и вместе с ним она снята.

fgaproxy.go — the FGA-proxy authz gate.

RegisterResource / UnregisterResource carry `permission = "<exempt>"` in the proto catalog (like every Internal IAM RPC), so least-privilege is NOT expressed as a flat permission-string. It is enforced HERE as ReBAC:

  1. The mTLS client-cert SAN (SPIRE format spiffe://<trust-domain>/ns/<ns>/sa/kacho-<svc>, extracted by SEC-B's grpcsrv.CertIdentityFromContext under the domain the installation declared) is recognized against the installation's module roster (moduleidentity.Roster, kaname#984 — only a DECLARED namespace/account pair is a module) and mapped to a deterministic ServiceAccount id (`'sva' || substr(md5('kacho-<svc>'),1,17)`).
  2. A ReBAC Check `service_account:<sva>#fga_writer@cluster:cluster_root` is issued. ALLOW → the RPC proceeds; DENY → PermissionDenied. Право выдаётся системной выдачей на кластере (kacho#914) — оно видно перечислением выдач и закрывается отзывом.

Fail-closed: an unverified peer, a malformed / foreign-trust-domain SAN, an unknown SA, or a denied relation all collapse to PermissionDenied. The service→service (mTLS-SA) path never consults `required_acr_min` — ACR-floor is a user-token concern only; the gate decides purely on the ReBAC relation.

front_admission.go — допуск предъявителя собственного внутреннего REST-фронта и сужения довода, которые обработчики делают по личности вызывающего (SEC-1017-1, kaname#987 вместе с kaname#984).

Предмет: хоп фронта не переносит личность, а сужение довода её требует

Предел хопа по МЕТОДАМ ([hopMethodSet]) — пересечение перечней профилей: то, что позволено любой личности. Этого мало там, где обработчик сужает не метод, а ДОВОД вызова по личности вызывающего: внутренний Check плоскости данных отвечает только об объектах её модуля (kaname#987). Личность обработчик берёт у пира gRPC, а на хопе пир — сам фронт; личности за ним не видно, и сужение не применяется. Фронт же пускает держателя ЛЮБОГО листа внутреннего центра: плоскость данных спросила бы через HTTP о чужом модуле, а лист вне перечня — о чём угодно, хотя gRPC-полоса ему отказывает полом.

Решение: судит тот, кто видит предъявителя, — фронт

Личность предъявителя HTTP видит только фронт: состояние его рукопожатия. Поэтому допуск решается там, ТЕМ ЖЕ признанием, что на слушателях (moduleidentity.Roster.FromTLSState), и по ТОМУ ЖЕ перечню:

  • лист вне перечня — отказ: на gRPC ему отказывает пол;
  • личность профиля, у которого какой-либо метод хопа сужается по личности, — отказ: сузить довод на хопе нечем, и пропустить её значило бы отвечать ей шире, чем на gRPC. Полоса HTTP у такой личности УЖЕ gRPC-полосы, и это допустимое направление;
  • прочие признанные личности — допуск: их gRPC-полоса по методам хопа не сужена, и полоса HTTP ей равна.

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

module_profiles.go — ЗАКРЫТЫЕ ПЕРЕЧНИ методов внутреннего слушателя на каждый профиль личности модуля (kaname#984).

Предмет

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

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

Откуда взяты перечни — замер, а не допущение

Вызывающие внутреннего слушателя перемерены по дереву платформы (2026-10-26): модули-владельцы ресурсов зовут ровно `RegisterResource`, `UnregisterResource` и `Check` службы доступа и `Check`/`BatchCheck` службы решений; край — фронтируемые им глаголы и горячие пути предъявления (разбор и живость базовых удостоверений, отзыв сессии, сессия человека, поток изменений, решения о доступе). Плоскости данных нужен один вопрос о доступе; сужение его области — kaname#987 и kaname#943.

Чеканка (`MintBootstrapToken`) не входит ни в один профиль: её судит рукав точных идентификаторов, стоящий выше и терминально.

principal.go — helper: principal-id-as-string for handlers that need to stamp `created_by` / `reviewer` / similar identity-derived fields from the authenticated caller — never from request-body.

Anti-identity-spoofing: handlers must source these from PrincipalFromContext, not from request fields. PrincipalUserID is the canonical accessor used by sa_keys, jit_eligibility, jit_pending.

public_caller_policy.go — политика ВЫЗЫВАЮЩЕГО по глаголам для ПУБЛИЧНОГО слушателя (:9090). Сестра caller_policy.go (:9091): та же форма, другая таблица.

Зачем она публичному слушателю вообще. iam намеренно НЕ задаёт конечному пользователю повторный вопрос к модели доступа на своих слушателях: край (api-gateway) — единственная парадная дверь авторизации платформы. Он проверяет токен, задаёт пообъектный вопрос через iam.Check по каталогу прав и только после этого передаёт установленную личность метаданными x-kacho-principal-*. Всё, что стоит после этого решения, действует от имени ЛЮБОЙ личности, приехавшей в этих заголовках.

Но :9090 — не только для края. Пять служб-потребителей зовут его на пути своего запроса (ProjectService.Get — существование проекта и владеющий аккаунт перед Create), а четыре из них задают ему вопрос видимости страницы за своим List (AuthorizeService.BatchCheck). Все они предъявляют клиентский сертификат того же внутреннего удостоверяющего центра, что и край, а порт — обычный Service внутри пространства имён без своей NetworkPolicy. Значит, до этой политики ЛЮБОЙ под с сертификатом внутреннего центра доставал ЛЮБОЙ публичный глагол и — раз доверяющее извлечение верило всякому проверенному соседу при пустом перечне доверенных отправителей — делал это от имени названной жертвы: читал и правил её аккаунты, проекты, группы, роли и выдачи прав, выпускал личные токены и ключи служебных учёток.

Слоёв два, и нужны оба:

  • перечень доверенных отправителей (authn.trusted-forwarder-sans → corelib WithTrustedForwarders) решает, КТО ВООБЩЕ ВПРАВЕ ГОВОРИТЬ ЗА ЧЕЛОВЕКА. Он сужает «всякого с сертификатом» до «наших модулей», но модуль из этого перечня всё ещё мог бы говорить за человека на КАЖДОМ публичном глаголе;
  • эта политика решает, КТО ВПРАВЕ ЗВАТЬ КАКОЙ ГЛАГОЛ. Она сужает соседей до тех рёбер запроса, которыми они действительно пользуются, и скомпрометированный сосед не достаёт до выпуска удостоверений и записи выдач прав вовсе.

Ветви (по порядку):

  1. Пол — всякий публичный глагол в боевой посадке требует ПРОВЕРЕННОГО модульного сертификата mTLS (SAN SPIFFE spiffe://kacho.cloud/ns/<ns>/sa/kacho-<svc>). dev (проверенного сертификата нет) — проход, как у CallerPolicy и RelationWriteGate.
  2. Край — служебная учётка края зовёт всё: это парадная дверь, и пообъектное решение модели доступа, на котором стоит весь замысел, принимается там. Пол уровня из своего каталога служба судит и на этой ветви — у каждого глагола с полом, кроме PublicGatewayFloorExemptRPCs (второй замок, kaname#747, kaname#767).
  3. Вызываемые соседями — малая статичная таблица БЕЗ МУТАЦИЙ, называющая для каждого глагола, какие служебные учётки модулей вправе его звать. Всё, чего в ней нет, — только для края: prod — PermissionDenied, dev — проход.

Таблица СТАТИЧНА и живёт в коде, ровно как GatewayFrontedInternalRPCs: членство — свойство ребра во время работы (записанного в перечне рёбер полирепо), а не того, что оператор случайно настроил. Новая служба, которой нужен публичный глагол iam, вписывает себя сюда изменением под ревью — пропуск в настройке не должен расширять эту таблицу никогда.

И членство — свойство ребра, которое СУЩЕСТВУЕТ. Запись, называющая вызывающего, у которого в дереве нет модуля, по форме не отличима от живой, но сегодня не пускает никого, а в день, когда кто-то выпустит сертификат с этим SAN, пускает всё, что называет, — открытая поверхность, забронированная за именем. TestPublicPeerCallableRPCs_EveryCallerHasAModuleInTheTree выводит допустимых вызывающих из `services/*` и падает на разнице; допуск модуля возвращается сам, когда возвращается модуль.

system_viewer_floor.go — the per-RPC `system_viewer`-FLOOR on the cluster-internal listener's READ RPCs.

The AuthN+AuthZ-everywhere invariant requires EVERY internal RPC (not only public) to pass a per-RPC authz-Check beyond mTLS: read-RPC gate viewer-tier (system_viewer). Today :9091 READ-RPCs are gated ONLY by the coarse mTLS caller-policy floor (CallerPolicy) — no relation-tier Check. This interceptor closes that gap.

For the READ-RPC set (ReadFloorRPCs), it requires the CALLER MODULE ServiceAccount (derived from the verified mTLS cert SAN, the SAME SAN→sva derivation as fgaproxy) to hold a COARSE cluster relation `system_viewer` on the singleton `cluster:cluster_root`, checked via the SAME RelationChecker port used by RelationWriteGate / InternalIAMService.Check.

This is a COARSE "is this a legitimate internal reader" gate (defense-in-depth against a compromised module holding a valid cert but not authorized to read IAM) — NOT a per-resource viewer-check. Per-user authz is the api-gateway's job, run BEFORE it forwards to :9091. The floor never ReBACs the forwarded end-user principal (x-kacho-principal-*) — the subject of the Check is the caller MODULE-SA, not the end user.

Default-OFF / production-mode-only: in dev/newman (no mTLS, FGA-on-internal disabled) the floor is a NO-OP pass-through, byte-identical to today — the same prodMode signal the CallerPolicy / RelationWriteGate use. This keeps the newman E2E stand green.

Fail-closed in production-mode: an unverified/absent SAN → PermissionDenied; a checker/FGA backend error → Unavailable (retryable). Mirrors RelationWriteGate error semantics verbatim.

EXEMPT from the floor (see ReadFloorRPCs / the exemption rationale below):

  • InternalIAMService.Check — the PDP. The caller acts on behalf of an end-user, NOT as the subject; floor-gating it on "caller has viewer" would break the core authz path (every downstream Check would deny). It stays on the mTLS-module floor only.
  • InternalSessionRevocationsService.IsRevoked — курица и яйцо (шло бы до того, как может пойти пер-пользовательская проверка); пер-вызовный поход в движок добавил бы задержку, а его недоступность массово роняла бы обновление токенов. Остаётся на полу mTLS-модуля. ВЫЗЫВАЮЩИЙ У ЭТОГО ПОСЛАБЛЕНИЯ ЕСТЬ, и он ровно тот, ради которого оно заведено: клиент края (`IsSessionRevoked`) спрашивает полосу на КАЖДОМ предъявлении удостоверения, до того как может пойти проверка доступа (kacho#1122). Здесь стояло «вызывающего у метода в дереве нет ни одного (kacho#797)» — утверждение пережило свой предмет и читалось как «послаблению нечего исключать» (kacho#1156). refresh-хук эту проверку по-прежнему НЕ зовёт и пер-jti гейта не несёт вовсе — он прямо это оговаривает (в его теле нет claims предъявленного токена).
  • все мутации (Register/Unregister) остаются за fga_writer-// gated; ForceLogout/GrantAdmin/… stay system_admin / gateway-only) — this is a READ floor; the mutation surface is unchanged.

Index

Constants

View Source
const BootstrapMintFullMethod = "/kaname.cloud.iam.v1.InternalBootstrapTokenService/MintBootstrapToken"

BootstrapMintFullMethod — InternalBootstrapTokenService/MintBootstrapToken, the SAN-restricted cluster-admin token mint (arm 3). Exported so the composition root can key its allow-list without re-spelling the FQN.

View Source
const (
	// RelationWriteRelation — отношение, которое обязана держать служебная учётка
	// модуля, чтобы писать кортежи через iam. Экспортировано намеренно: пробы и
	// перепись обязаны спрашивать ТО ЖЕ имя, которое спрашивает гейт, — имя,
	// выписанное на стороне пробы, остаётся зелёным, когда гейт спрашивает другое.
	RelationWriteRelation = "fga_writer"
)
View Source
const TextAddressGateUnavailable = "request not performed; try again later"

TextAddressGateUnavailable — отказ операции, когда отметку прочесть не смогли: фиксированный текст, без эха причины.

View Source
const UnnamedCallerMessage = "no credential presented; authenticate with authorization: Bearer <token>"

UnnamedCallerMessage — ЕДИНСТВЕННЫЙ текст отказа тому, кто не назвался ничем.

Почему отдельный текст, а не «permission denied»

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

Почему это не оракул

Различаются здесь два состояния ЗАПРОСА, а не сервера, и оба известны самому вызывающему до ответа: он либо приложил удостоверение, либо нет. Отказ ПРЕДЪЯВИВШЕМУ негодное остаётся единственным побайтово равным текстом (`presentedcred.RefusalMessage`) — там различимость и правда сообщила бы, какая половина предъявленного неверна.

Текст — часть контракта, и он экспортирован затем, чтобы проба утверждала ЕГО, а не свою копию.

Variables

View Source
var MutateRelations = []string{"editor", "admin"}

MutateRelations — FGA relations that grant authority to mutate a resource. `editor` is sufficient for Update; `admin` (a superset) is accepted too.

Functions

func AllowsVGet

func AllowsVGet(ctx context.Context, checker RelationChecker, fgaType, id string) (bool, error)

AllowsVGet reports whether the ctx principal may read the object `<fgaType>:<id>`: it is a cluster-admin (flat super-gate, D-9) OR it holds the `v_get` relation on the object (owner-binding materializes it for the owner; an explicit `iam.<res>.get` grant materializes it for a delegate).

(false, nil) is a decision (hide existence); (false, err) means the decision could not be made (map to UNAVAILABLE, never to NotFound).

fgaType is the rights-model object_type (e.g. "account", "project", "iam_user"); id is the bare resource id (no type prefix). The object string is composed as `<fgaType>:<id>` — the SAME object the reconciler materializes and the gateway Check's against.

func AllowsVerb

func AllowsVerb(ctx context.Context, checker RelationChecker, relation, fgaType, id string) (bool, error)

AllowsVerb is the generic verb-bearing read-authorization gate: cluster-admin super-gate OR the ctx principal holds `relation` on `<fgaType>:<id>`. AllowsVGet is the get-specialization (relation == "v_get"). Kept generic so a future read path (e.g. a v_list object-existence probe) reuses the same fail-closed posture.

An allow is returned as soon as ONE question answers yes, even if the other failed — an allow needs no second opinion. A non-allow is only reported as a decision when EVERY question this gate asked was actually answered; otherwise the error of the unanswered one is returned.

func AntiAnonymousStream

func AntiAnonymousStream(logger *slog.Logger) grpc.StreamServerInterceptor

AntiAnonymousStream — symmetric stream-RPC interceptor (same policy).

func AntiAnonymousUnary

func AntiAnonymousUnary(logger *slog.Logger) grpc.UnaryServerInterceptor

AntiAnonymousUnary — gRPC unary server interceptor. Policy: default-deny anonymous unless (a) FullMethod is in whitelistFullMethod, or (b) method-name ends in a read-only suffix (Get/List/Watch/Resolve/…). Everything else (Create/Update/Delete/Approve/Deny/Issue/Revoke/Generate/ Cancel/Activate/…) is rejected for anonymous principals.

func AuthzBackendUnavailable

func AuthzBackendUnavailable() error

AuthzBackendUnavailable — canonical answer for "the model could not be asked".

It is NOT PermissionDenied, and the difference is the whole point: a refusal says "you may not", which tells the caller that retrying is pointless — the decision depends on (subject, relation, object) and an identical retry changes none of the three. A backend outage says nothing about rights at all; the same question a moment later gets an answer. Collapsing the two hands a caller a terminal verdict on a transient flap, and callers that classify peer answers by lane (outbox drainers, reconcilers, peer clients) will mark the intent permanently failed.

Fail-closed is unchanged either way: the request is refused, nothing runs. Only the code differs, and the code is the whole signal.

The text carries no detail of the backend failure (§Hardening-инварианты п.1 — no leak); the raw error is logged, not surfaced. Single source for the three gates that already answered this way inline (RelationWriteGate, SystemViewerFloor, scope) and for those that used to collapse it into a refusal.

func CallerAuthorityGatedMethods

func CallerAuthorityGatedMethods() []string

CallerAuthorityGatedMethods — RPC, которыми у модели СПРАШИВАЮТ, а не действуют: «можно ли субъекту S сделать A с объектом O».

Почему у них нет ОБЪЕКТА, о котором спросить

Субъект такого RPC стоит В ЗАПРОСЕ, а вызывающий — модуль: край на пути каждого запроса и сервисы-соседи при сужении списочной выдачи. Вопрос задаётся ДО того, как решение о доступе принято, и задаёт его не тот, о ком спрашивают, поэтому единого объекта, которым дверь могла бы их гейтить, не существует: у `Check` это и объявлено контрактом (`scope_filtered`).

ЛИЧНОСТЬ АРЕНДАТОРА У НИХ ПРИ ЭТОМ БЫВАЕТ, и здесь стояло обратное («её нет by construction»). Сужатель списочной выдачи зовёт соседа под личностью ИНИЦИАТОРА — `pkg/listnarrow/client.go` передаёт `auth.PropagateOutgoing(ctx)`, — так что на пути «арендатор перечисляет свою страницу» вызывающий назван всегда. Разбор следствия и цена — в разделе о безусловности поправки ниже.

Кто их решает — ОДИН, и он назван

`api/authorize/caller_authority.go` — единственный решатель вопроса «кто вправе спрашивать», и его шапка это заявляет прямо. Он строго fail-closed: вызывающий без личности проходит ТОЛЬКО с проверенным сертификатом модуля (`authorizeAnonymousPeer`), арендатор — только про себя, про объект, которым администрирует, либо будучи администратором кластера. Соседний рубеж того же слушателя (`AntiAnonymousUnary`) вычитает эти RPC по той же причине.

Дверь для них добавляла ровно одно требование — «вызывающий назван», — и именно оно неверно: модуль называет себя сертификатом, а не заголовком личности. Наблюдалось: пять шардов сквозных проб, у каждого 40+ отказов `authz_no_principal` на `AuthorizeService/Check`, при нуле таких записей на стволе; посев умирал на первой же мутации, потому что ОТКАЗОМ становилось всякое решение о доступе на стенде.

Круг узок намеренно, и у каждой записи назван ПРОИЗВОДИТЕЛЬ

  • `Check` — край, `gateway/internal/clients/iam_authorize_client.go` (вопрос на пути каждого запроса);
  • `BatchCheck` — сужатель списочной выдачи, `pkg/listnarrow/client.go` (вопрос о странице у сервисов-соседей).

`ListSubjects` и `ExpandRelations` тот же обработчик судит тем же гейтом, но производителя вне iam у них в этом дереве НЕТ (предикат: `git grep -l 'ExpandRelations\|ListSubjects' -- gateway pkg services ':!services/iam'`), поэтому записи им здесь не заводится: освобождение заводится ВМЕСТЕ с тем, что оно освобождает, а не вперёд него. Появится производитель — запись приедет вместе с ним.

Поправка живёт В ЗВЕНЕ, а не в карте — и это решение, а не вкус

Карту двери судит перепись публичной поверхности (`internal/publicauthzcensus`), и её шапка объявляет: перепись обязана читать ТУ ЖЕ карту, которую звено спрашивает в проде. Перекрой мы здесь выведенную запись — перепись назвала бы эти два RPC освобождёнными КОНТРАКТОМ, чего контракт не объявлял; получилось бы второе место об одном предмете, и разошлись бы они молча. Карта остаётся выведенной, а поправка стоит там, где её видно как отдельное звено, — рядом с двумя другими рубежами того же слушателя.

Поправка снимает дверь там, где у неё НЕТ ОБЪЕКТА, — и таких случаев ДВА

Здесь стояло «дверь снимается ТОЛЬКО когда личности арендатора нет вовсе», а основанием ему служил абзац выше — «личности арендатора в их исходящем контексте нет by construction». ОСНОВАНИЕ НЕВЕРНО, и это измерено: сужатель списочной выдачи зовёт соседа через `auth.PropagateOutgoing(ctx)` (`pkg/listnarrow/client.go`), а тот дописывает принципала в исходящие метаданные, когда он непуст. На пути «арендатор перечисляет свою страницу» принципал непуст ВСЕГДА, то есть вызывающий назван, — и снятие по первому случаю к нему не относилось.

Для `Check` это не значило ничего: контракт объявляет его `scope_filtered`, и звено выходит до вопроса об объекте при любой личности. Для `BatchCheck` значило всё — он несёт `required_relation: viewer` и `scope_extractor{project, scope_id}`, поэтому названный вызывающий доходил до вопроса об объекте. Объекта у ЭТОГО вопроса нет: `scope_id` контракт объявляет НЕОБЯЗАТЕЛЬНЫМ дословно («Optional scope id for authz of the batch as a whole. When set, the gateway gates the entire batch on this scope's `iam.authorize.batchCheck` permission instead of per-item»), и не заполняет его ни один вызывающий в дереве — предикаты: `git grep -n 'ScopeId' -- pkg/listnarrow` → пусто, `git grep -n 'ScopeId:' -- . | grep -v _test` → среди попаданий нет ни одного `BatchAuthorizeCheckRequest`. Выведенный извлекатель читает поле как заданное всегда и отдаёт `project:` с пустым идентификатором; пустой отвергается `FormatObject` ДО вопроса к модели, то есть и до плоского надзора администратора облака, который живёт внутри `checkAdapter.Check`. Прежняя редакция называла эту полосу штатной («по объекту у `BatchCheck`»), тогда как пройти её нельзя было НИКОГДА.

Наблюдалось на голове `d1c7a4a89b`: журнал звена называл причину прямо — `authz_object_format_failed err="authz: empty object id"`, — край отдавал 503 на `GET /vpc/v1/securityGroups`, консоль показывала «Сервер не смог ответить · list filter: AuthorizeService.BatchCheck PermissionDenied», а СПИСКИ ВСЕХ РЕСУРСОВ ПРОЕКТА были пусты у каждого арендатора. В шарде vpc этим объясняются все 123 отказа (118 прямых + 5 каскадом), в шарде iam — ещё 4; пять сквозных проб консоли падали тем же корнем. Сужение страницы ломалось у КАЖДОГО соседа, который его делает.

Почему снятие привязано к ОТСУТСТВИЮ ОБЪЕКТА, а не сделано безусловным

Починку писали две полосы одной волны, и разошлись они ровно в одном: снимать ли дверь и с запроса, НАЗВАВШЕГО `scope_id`. Измеренный дефект такого запроса не касается — у него объект ЕСТЬ, и вопрос `viewer @ project:<scope_id>` есть ровно тот, который контракт для этого поля и объявляет. Снять гейт заодно значило бы принять поле контракта и на него не смотреть, а послабление выдать ВПЕРЁД производителя, которого сегодня нет: запас здесь — не осторожность, а слепая зона, в которую уедет первый же настоящий вызывающий.

Поэтому снятие привязано к тому, что измерено: извлекатель отдал ПУСТОЙ идентификатор — спрашивать не о чем. Три исхода извлечения разведены в `questionNamesNoScope`, и средний, «извлечение отказало», двери НЕ снимает: неполученный ответ не есть «спрашивать не о чем».

Что остаётся и что не трогается

Запрос, назвавший `scope_id`, проходит дверь как прежде: объект есть, и гейт контракта на нём осмыслен. `Check` не затронут вовсе — он объявлен `scope_filtered`, извлекателя у него нет by construction, и его полосой остаётся полоса данных.

Второго рубежа этим RPC не нужно ТАМ, ГДЕ ОБЪЕКТА НЕТ: `authorizeCaller` полон для ОБОИХ видов вызывающего — безымянный проходит только проверенным сертификатом модуля, названный арендатор — про себя, про объект, которым администрирует, либо будучи администратором кластера, — и он fail-closed. Дверь добавляла им не второе мнение, а вопрос об объекте, которого у вопроса о правах нет.

Порог свежести подтверждения (`required_acr_min`) поправка НЕ трогает: его держит отдельный страж того же слушателя (`acr_floor.go`), а не это звено.

func ClusterObject

func ClusterObject() string

ClusterObject — the singleton every cluster-tier tuple hangs off. Named once so a caller composing the question by hand cannot spell it differently.

func DenyDetailUnary

func DenyDetailUnary(catalog DenyActionLookup) grpc.UnaryServerInterceptor

DenyDetailUnary returns an interceptor that attaches the machine-readable reason to a refusal that does not already carry one.

What it does NOT attach, and why:

  • the subject — the caller already knows who it is, and echoing it adds nothing a client can act on;
  • the resource — on a data-filtered method there is no single resource by construction; naming one would be a claim the service cannot make. The edge fills that field only where it resolved a scope object.

WHAT IT ALSO ATTACHES, AND WHY THAT IS NOT AN ORACLE ---------------------------------------------------- A refusal exists so the caller can build the NEXT STEP. A bare one does not restore it: the administrator learns neither which permission to request nor from whom. So the refusal carries the step — in the DETAILS, never in the prose. The message stays the verbatim "permission denied" on every refusal, byte for byte, because a distinguishable text is exactly the existence oracle the fixed wording exists to close.

The form is not invented here: it is the one the step-up refusal already uses (acr_floor.go — a PreconditionFailure violation the edge turns into an RFC 9470 challenge). Type "authz.grant_required", Subject = the scope object type, Description = the permission to ask for and where it is granted.

A refusal that ALREADY names its own next step (the step-up one) gets no second advice: the caller there may hold the grant in full, and "ask for the permission" would send them the wrong way.

A method with no catalog row gets NOTHING attached. That is the whole point: an absent action is how a caller recognises a catalog miss, so inventing an empty one would erase the distinction this exists to create.

func GatewayFrontedInternalRPCs

func GatewayFrontedInternalRPCs() []string

GatewayFrontedInternalRPCs returns the full-method set of internal RPCs that the api-gateway fronts on behalf of an end user (admin UI / admin tooling). These privileged RPCs may ONLY be called by the api-gateway SA — a direct call from any other module is a privilege-escalation attempt.

NOT in this set (any verified module — floor only):

  • InternalIAMService/{Check,LookupSubject,PollSubjectChanges} — hot-path service→service RPCs.
  • InternalSessionRevocationsService/IsRevoked — заведено под собственный запрос края, идущий до того, как может пойти пер-пользовательская проверка (курица и яйцо). ПРЕДМЕТ У ПОСЛАБЛЕНИЯ ЕСТЬ: клиент края экспонирует `IsSessionRevoked`, и он провязан в слой аутентификации (`middleware.NewLocalThenProviderRevocation`, kacho#1122) — то есть край спрашивает эту полосу на каждом предъявлении удостоверения. Здесь стояло «этого вызывающего в дереве СЕГОДНЯ НЕТ (kacho#797)»: утверждение пережило свой предмет (kacho#1156). Исчезнет метод у края — снимать и эту строку. Ответ метода покрывает и отзыв СЕМЕЙСТВА выпуска (kaname#319); послабление под эту возможность выведено заново, а не унаследовано, — разбор у `session_revocations.Handler.IsRevoked`.
  • InternalUserService/Get — service→service lookup.
  • identity-provider hook callbacks are not in this set: they were served over HTTP, never as gRPC methods, and left together with the external provider (kaname#363). The gRPC declaration that once mirrored them (InternalIamHooksService) had no implementation and was retired — see retiredRPCSurface in internal/repohygiene.
  • the fga-proxy writes InternalIAMService/{RegisterResource, UnregisterResource} — gated in-handler by RelationWriteGate (module SAs). The third one, WriteCreatorTuple, was retired with zero callers (kacho#788).

func GatewayServiceName added in v0.5.0

func GatewayServiceName() string

GatewayServiceName — короткое имя службы края, каким его называет SAN проверенного клиентского сертификата. ОДНО объявление на дерево (Ф3 §8 инв. 7): его читают ярус gateway-only внутреннего слушателя и слушатель полосы формы (`internal/handler/loginlanehttp`, Р16) — второго литерала имени края у второго читателя нет.

func HopAdmits added in v0.7.0

func HopAdmits(modules moduleidentity.Roster, d grpcsrv.TrustDomain, st *tls.ConnectionState) bool

HopAdmits — пускает ли собственный внутренний REST-фронт предъявителя с состоянием рукопожатия st (см. шапку файла).

Непроверенный лист, лист вне перечня и лист чужого центра у личности с привязкой — не личность, отказ.

func HumanUserID

func HumanUserID(ctx context.Context) string

HumanUserID returns the principal's id ONLY when the principal is a human user; "" for a service account, a system/bootstrap principal, an unknown type, anonymous, or an empty ctx.

Use this — NOT PrincipalUserID — for a column that is a foreign key into `users(id)` (`users.invited_by` and anything like it). The distinction is not cosmetic: PrincipalUserID deliberately answers for machine and system principals too, so a caller that wants "the user" and reaches for the user-shaped name gets `sva…`/`bootstrap` back and writes it into a column where no such row can exist. That is a constraint violation at insert time, surfaced to the caller as the unmapped-FK fallback text, which names neither the column nor the cause.

A non-user principal is not an error here and must not be turned into one: it is a legitimate actor with no inviting/creating USER to record. Callers leave the column NULL and rely on the Operation's `principalType`/`principalId` for attribution, which is where a non-user actor belongs.

func InternalAddressGateFindings added in v0.5.0

func InternalAddressGateFindings(circle, internalMethods []string, table map[string]InternalAddressGateRow) []string

InternalAddressGateFindings — сверка таблиц Р4в с двумя производителями (§9 п. 5 приёмки): кругом края (`GatewayFrontedInternalRPCs`) и перечнем методов внутренних служб контракта. Находки — строками с координатой: метод круга без строки круга, метод внутренней службы без строки ни в одной таблице, строка без метода. Пустой перечень любого производителя — находка: пустой обход не вердикт.

func InternalAddressGateTable added in v0.5.0

func InternalAddressGateTable() map[string]InternalAddressGateRow

InternalAddressGateTable — таблицы Р4в копией: метод → строка.

func InternalFrontAdmission added in v0.7.0

func InternalFrontAdmission(prodMode bool, modules moduleidentity.Roster, d grpcsrv.TrustDomain) func(*tls.ConnectionState) bool

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

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

func IsAnonymous

func IsAnonymous(ctx context.Context) bool

IsAnonymous — true if the principal is anonymous / empty / system+anonymous / system+bootstrap-fallback.

api-gateway injects anonymous as {Type:"system", ID:"anonymous"}; api-gateway may also fail to forward principal-headers entirely — in that case PrincipalFromContext returns SystemPrincipal{Type:"system", ID:"bootstrap"} (fallback). In a gRPC handler both cases = anonymous.

**Internal background-job bootstrap** uses `WithPrincipal` directly with a concrete {Type, ID} — never the empty-ctx fallback. So rejecting system/bootstrap here is safe.

func IsAnonymousSelfCheck added in v0.7.0

func IsAnonymousSelfCheck(ctx context.Context, req *iamv1.AuthorizeCheckRequest, modules moduleidentity.Roster) bool

IsAnonymousSelfCheck admits only the credential-free, subject-free Check shape (kaname#959). Both public unary admission and the handler use this predicate. It grants no resource permission: the normal decision service evaluates the server-selected anonymous subject.

func IsClusterAdmin

func IsClusterAdmin(ctx context.Context, checker RelationChecker) bool

IsClusterAdmin reports whether the ctx principal holds the flat cluster super-admin relation. fail-closed: nil checker / anonymous / empty id / Check error → false.

func IsClusterAdminE

func IsClusterAdminE(ctx context.Context, checker RelationChecker) (bool, error)

IsClusterAdminE is IsClusterAdmin with the reason kept: it separates "the store answered: not an admin" (false, nil) from "the store could not be asked" (false, err).

Когда обязателен именно он

Всюду, где несработавший супер-гейт МЕНЯЕТ наблюдаемый ответ, а не просто «не срабатывает». Списочный путь — главный такой случай: проглоченная неполадка отдаёт well-formed `200` с молча суженной страницей, которую вызывающий не отличит от отзыва прав. Ровно это и требует godoc SubjectIsClusterAdminPlainE ниже; булева обёртка остаётся для мест, где обычная пообъектная полоса всё равно отработает и сама сообщит о неполадке.

func IsSelf

func IsSelf(ctx context.Context, targetID string) bool

IsSelf — true if principal.ID == targetID.

func ModuleActor added in v0.7.0

func ModuleActor(ctx context.Context) (subject, module string, ok bool)

ModuleActor — машинная личность модуля-вызывающего по ПОДТВЕРЖДЁННОМУ сертификату: субъект `service_account:<учётка службы>` и имя службы (kaname#1005, аудит регистрации объектов модулем).

Личность выводится тем же разбором, которым ворота записи регистрации (`RelationWriteGate.Authorize`) узнают модуль: единственный разбор SAN (`moduleidentity.ParseSAN`, kaname#984) → учётка → служба (`moduleidentity.ServiceFromAccount`, та же функция, которой перечень личностей выводит `Identity.Service`) → учётка службы (`ServiceAccountIDForService`). Поэтому для вызова, пропущенного воротами, актор события аудита — ровно тот субъект, чьё право на запись проверено, а не слово вызывающего. Признание по перечню и привязку к выпустившему центру (kaname#984, kaname#988) судят ворота до use-case; здесь личность только называется. ok=false — подтверждённой личности по сертификату нет (вызов не по mTLS либо SAN не называет службу продукта).

func NarrowsCallerArgument added in v0.7.0

func NarrowsCallerArgument(fullMethod string, p moduleidentity.Profile) bool

NarrowsCallerArgument — сужает ли обработчик метода fullMethod довод вызова для личности профиля p.

func OwnDoorProtoPackages

func OwnDoorProtoPackages() []string

OwnDoorProtoPackages — пакеты контракта, чьи службы iam поднимает на своих слушателях.

Перечень ЗАКРЫТ и обязан покрывать всё, что регистрируется: карта тотальна над названными пакетами, а RPC из НЕ названного пакета в неё не попадает вовсе и отвергается как незамапленный. То есть пропуск пакета здесь — не послабление, а отказ обслуживать его целиком; ошибка громкая, и это выбрано намеренно.

func PeerIsGateway added in v0.5.0

func PeerIsGateway(modules moduleidentity.Roster, d grpcsrv.TrustDomain, st *tls.ConnectionState) bool

PeerIsGateway — пир HTTP-слушателя предъявил ПРОВЕРЕННЫЙ сертификат края: объявленная установкой личность, чья служба — api-gateway.

Один признак края на все HTTP-слушатели, читающие заголовок, который ставит край: полосу входа (допуск ровно края) и поверхность выдачи (адрес источника, приёмка ceremony-pace-is-named-by-number.md Р7). Сертификат, которого слушатель не проверил (нет проверенной цепочки), личности не доказывает. Необъявленный домен доверия и необъявленная пара не узнают никого.

Привязка к выпустившему центру (kaname#988) судится ТЕМ ЖЕ перечнем, что на gRPC-слушателях: лист края, выпущенный не центром края, краем не признаётся.

func PermissionDenied

func PermissionDenied() error

PermissionDenied — canonical PermissionDenied gRPC error (Kachō error text).

func PrincipalSubject

func PrincipalSubject(ctx context.Context) (string, bool)

PrincipalSubject is the ctx variant of SubjectFromPrincipal: it reads the principal from ctx and returns its FGA subject. Anonymous / empty ctx → ("", false) — fail-closed, the same posture as the FGA Check guards that consume it.

func PrincipalUserID

func PrincipalUserID(ctx context.Context) string

PrincipalUserID returns the principal's user-id for user / service-account / system-bootstrap principals; empty string for anonymous or empty ctx.

Use this when writing DB rows or audit-log entries that must carry the authenticated caller's id. Never trust a request-body field for these.

NOTE the name is wider than it reads: the returned id is NOT necessarily a `users(id)`. For a foreign key into that table use HumanUserID above.

Bootstrap-principal (system/bootstrap) is treated as a legitimate identity so internal seeds / migrations / fixtures continue to work; the audit row carries `created_by="bootstrap"` which is correct.

func ProfileMethods added in v0.7.0

func ProfileMethods(p moduleidentity.Profile) []string

ProfileMethods — закрытый перечень методов внутреннего слушателя профиля p. Неизвестный профиль — пустой перечень: по нему не открыт ни один метод.

func PublicGatewayFloorExemptRPCs added in v0.6.0

func PublicGatewayFloorExemptRPCs() map[string]string

PublicGatewayFloorExemptRPCs — публичные глаголы с полом уровня, на которых второй замок службы на ветви края НЕ стоит, с основанием каждого.

Что держит второй замок

На ветви края служба судит пол уровня из СВОЕГО встроенного каталога у КАЖДОГО глагола, чья запись объявляет пол выше «0» (kaname#767), а не у выписанного перечня. Перечень расходился бы с каталогом молча: новый глагол с полом в него сам не попадает, и его пол держал бы только край — по своей копии каталога, которая отстаёт от контракта службы на время между вливанием контракта и пересборкой края (так было с тремя глаголами ключа доступа: «1» в копии края при «2» в контракте, kaname#747). Выведенное из каталога множество закрыто по умолчанию: глагол, получивший пол в контракте, получает и второй замок тем же изменением.

Почему изъятие — перечень с основанием, а не настройка

Изъятие ослабляет замок, и потому живёт в коде под ревью, как таблица соседей выше: расширяет его изменение, называющее основание, а не настройка посадки. Запись, чьего глагола нет среди публичных с полом, — находка (TestKN767_ExemptionExpiresWithItsSubject): изъятие истекает вместе с предметом.

func PublicPeerCallableRPCs

func PublicPeerCallableRPCs() map[string][]string

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

Каждая запись — ЗАПРОС, и это правило, а не совпадение: мутирующий глагол здесь дал бы соседней службе возможность менять данные арендатора от имени переданного человека — ровно ту дыру, которую эта политика закрывает. Замок TestPublicPeerCallableRPCs_CarryNoMutation держит это по самому контракту: в Kachō мутация — это ровно глагол, возвращающий Operation, поэтому проверка читает контракт, а не привычку именования.

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

Членство выведено из рёбер во время работы, а не из поиска по имени:

  • ProjectService/Get — vpc, compute, nlb, storage и registry держат по ProjectServiceClient, направленному на ПУБЛИЧНЫЙ адрес iam, и зовут Get на пути своего Create (существование проекта и владеющий аккаунт);
  • AuthorizeService/BatchCheck — фильтр видимости страницы в vpc/compute/nlb/storage (internal/authzfilter — единственный метод AuthorizeService, который зовёт любой из них). Его ребру место на внутреннем слушателе, и большинство профилей его туда ставят, но профиль, направляющий его на публичный адрес, существует, и отказ там закрыл бы фильтр и опустошил собственный List арендатора.

func ReadFloorRPCs

func ReadFloorRPCs() []string

ReadFloorRPCs returns the full-method set of cluster-internal READ RPCs that must pass the `system_viewer@cluster` floor. Membership is the single source of truth for the floor; the exemption set is expressed by ABSENCE from this list and asserted by TestReadFloorRPCs_Membership.

NOT in this set (exempt — see the package doc-comment for the rationale):

  • InternalIAMService/Check — PDP, never floor-gated.
  • InternalSessionRevocationsService/IsRevoked — курица и яйцо: клиент края (`IsSessionRevoked`) спрашивает её до того, как может пойти проверка доступа (kacho#1122).
  • InternalIAMService/{RegisterResource,UnregisterResource} — fga_writer-gated mutations.
  • ForceLogout / Cluster GrantAdmin/RevokeAdmin / Authorize WriteTuples/ReloadModel / SessionRevocations Revoke — gateway-only / admin-tier mutations.

func RequireAuthenticated

func RequireAuthenticated(ctx context.Context) error

RequireAuthenticated returns PermissionDenied if the principal in ctx is anonymous or absent. Only `user` and `service_account` are passed through.

system/bootstrap is REFUSED, exactly as the package comment says: that pair is what PrincipalFromContext returns when the request carried no principal at all, so admitting it would turn "the edge forwarded nothing" into a privileged identity. The previous edition of this sentence listed it among the passed-through principals — two statements about one subject, of which one was true — and the false one was read as a licence: three listing use-cases carried an "unfiltered page for the bootstrap identity" branch that no input could reach, because IsAnonymous had already refused (kacho#648).

Message text is the Kachō canonical `"permission denied"` (no leak of internal details).

IMPORTANT: kacho-api-gateway injects an anonymous request as Principal{Type:"system", ID:"anonymous"} (see auth.go:189 — injectAnonymous). A principal with ID="anonymous" MUST NOT have privileges — despite type=system. Both fields are checked.

func RequireClusterAdmin added in v0.5.0

func RequireClusterAdmin(ctx context.Context, checker RelationChecker) error

RequireClusterAdmin — защитная проверка «вызывающий — администратор облака» в виде ответа RPC. Решение принимает модель (IsClusterAdminE); здесь только перевод её исхода в код:

модель ответила «да»                          → nil
модель ответила «нет», принципала не назвать,
проверяющий не подключён                      → PERMISSION_DENIED "permission denied"
спросить модель не удалось                    → UNAVAILABLE "authz backend unavailable"

Текст недоступности фиксированный: причина отказа хранилища в ответ не попадает (§Hardening-инварианты п. 1).

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

func RequireScopeRelation

func RequireScopeRelation(
	ctx context.Context,
	checker RelationChecker,
	scopeType, scopeID, ownerUserID string,
	relations ...string,
) error

RequireScopeRelation — defense-in-depth authority gate for a mutating use-case. Authority is granted when EITHER:

  • the principal owns the owning Account (ownerUserID, bootstrap path), OR
  • the principal holds one of `relations` on the FGA scope object (delegated administration — e.g. a project-editor who is not the owner).

`scopeType`/`scopeID` identify the FGA object (`project:<id>`, `account:<id>`). `ownerUserID` is the owning Account's owner_user_id (may be empty for scopes without an owning Account — then only the FGA path applies).

When `checker` is nil (unit tests / degraded mode) the guard falls back to owner-only and DENIES non-owners — fail-closed, never fail-open.

func SANRestrictedInternalRPCs

func SANRestrictedInternalRPCs() []string

SANRestrictedInternalRPCs returns the full-method set gated by arm 3 — an explicit client-certificate SPIFFE SAN allow-list.

The SET IS STATIC, deliberately independent of configuration: membership is a property of the RPC, not of what an operator happened to configure. A deployment that never supplies an allow-list must end up with the mint DENIED to everyone, not silently downgraded to the "any verified module cert" floor — a config omission must never open a cluster-admin mint.

func ServiceAccountIDForService

func ServiceAccountIDForService(svc string) string

ServiceAccountIDForService derives the deterministic module SA id from a service short-name (`'sva' || substr(md5('kacho-<svc>'),1,17)`). Single source of truth shared by the gate and the seed migration helper.

Формула ОДНА на дерево и живёт в `domain.DerivedIDSuffix`: своя копия здесь разошлась бы с постгресовой молча — полученный идентификатор остался бы синтаксически верным и перестал бы находить строку.

func SubjectFromPrincipal

func SubjectFromPrincipal(p operations.Principal) (string, bool)

SubjectFromPrincipal builds the FGA subject string for a principal: `user:<id>` for users, `service_account:<id>` for service accounts. It is the single source of truth consolidating the previously-inline (`subjType:="user"; if p.Type=="service_account" {…}; subject:=t+":"+id`) copies scattered across the authz call-sites (#10).

Fail-closed: an unknown principal type, or an empty id, yields ("", false). This is STRICTLY SAFER than the inline copies it replaces, which defaulted unknown types to "user:" (a latent over-grant). Callers must treat ok=false as "no resolvable subject → deny".

func SubjectIsClusterAdmin

func SubjectIsClusterAdmin(ctx context.Context, checker RelationChecker, subject string) bool

SubjectIsClusterAdmin is the subject-string variant of IsClusterAdmin — used by authorize_service.Check, whose request already carries a pre-formatted FGA subject ("user:usr_xxx" / "service_account:sva_xxx") rather than a ctx principal. fail-closed: nil checker / empty subject / Check error → false.

func SubjectIsClusterAdminE

func SubjectIsClusterAdminE(ctx context.Context, chk ContextRelationChecker, subject string) (bool, error)

SubjectIsClusterAdminE answers "is this caller a cluster administrator" and keeps the reason.

(false, nil) is an ANSWER — the store said no. (false, err) means the question was not answered, and the caller must refuse (UNAVAILABLE) rather than narrow.

A nil checker or an unresolvable subject yields (false, nil): those are not outages, they are callers this gate does not apply to, and the list decides what to do about them before it gets here.

func SubjectIsClusterAdminPlainE

func SubjectIsClusterAdminPlainE(ctx context.Context, checker RelationChecker, subject string) (bool, error)

SubjectIsClusterAdminPlainE is SubjectIsClusterAdmin with the reason kept: it separates "the store answered: not an admin" (false, nil) from "the store could not be asked" (false, err). Gates that turn a non-allow into a 404 need that distinction — see AllowsVerb. The bool-only wrappers above stay for the decision sites that are ALREADY fail-closed by construction (a super-gate that cannot be evaluated simply does not fire, and the ordinary per-object path still runs and still reports its own outage).

Which of the two E-variants to call

This one and SubjectIsClusterAdminE (subject_question.go) ask the SAME question — same relation, same singleton — and differ only in the port they ask it through: this one over RelationChecker (plain `Check`), that one over the context-carrying `CheckWithContext`. Call the one matching the port the use-case already holds; do not wire a second port to reach the other.

A list whose page is a page of the visible must use an E-variant: swallowing this failure there produces a well-formed, silently narrowed `200` that the caller cannot tell from a revocation (task kacho#645, acceptance §3.6).

func VerifiedPeerIdentity added in v0.6.0

func VerifiedPeerIdentity(d grpcsrv.TrustDomain, st *tls.ConnectionState) string

VerifiedPeerIdentity — идентификатор пира HTTP-слушателя: URI SAN нашего домена доверия с ПРОВЕРЕННОГО слушателем листа. Пусто — сертификата нет, цепочка не проверена либо лист не несёт идентификатора нашего домена.

Одно место, где решается «чей это пир» по сертификату: признак края выше и доверенный пересыльщик поверхности выдачи (`issuingsource`, kaname#930) спрашивают его, а не разбирают состояние рукопожатия каждый сам.

Types

type ACRFloor

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

ACRFloor enforces the gateway-fronted `required_acr_min` step-up floor on the internal listener. Construct via NewACRFloor.

func NewACRFloor

func NewACRFloor(catalog ACRRequirementLookup, gatewayFrontedRPCs []string) *ACRFloor

NewACRFloor builds the floor over the gateway-fronted RPC set. Defaults to dev-mode (no-op); use WithProductionMode to enable strict enforcement.

func (*ACRFloor) Stream

Stream returns the stream interceptor enforcing the acr-floor.

func (*ACRFloor) Unary

Unary returns the unary interceptor enforcing the acr-floor.

func (*ACRFloor) WithProductionMode

func (f *ACRFloor) WithProductionMode(prod bool) *ACRFloor

WithProductionMode toggles strict fail-closed enforcement (production AuthN).

type ACRRequirementLookup

type ACRRequirementLookup interface {
	RequiredACRMin(fqn string) string
}

ACRRequirementLookup — narrow port resolving an RPC's `required_acr_min` from the permission catalog. The key is the catalog FQN (NO leading slash, e.g. "kaname.cloud.iam.v1.InternalClusterService/GrantAdmin"); an unknown FQN or an RPC without an acr requirement returns "". Satisfied by an adapter over seed.PermissionRegistry in the composition root; a fake in tests.

type AddressGate added in v0.5.0

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

AddressGate — рубеж; ОДНО значение на оба слушателя. Метод, объявленный таблицей Р4в (методы внутренних служб контракта), судится её строкой; всякий прочий смонтированный метод — публичных служб и служб фундамента — судится как на публичном слушателе: отказ Р3 неподтверждённому человеку (Р4б).

func NewAddressGate added in v0.5.0

func NewAddressGate(marks admission.Marks) *AddressGate

NewAddressGate — рубеж над читателем отметок; домен отказа — у объявления (`refusaldomain`).

func (*AddressGate) Stream added in v0.5.0

Stream — потоковая полоса.

func (*AddressGate) Unary added in v0.5.0

Unary — однократная полоса.

func (*AddressGate) WithRefusalObserver added in v0.5.0

func (g *AddressGate) WithRefusalObserver(on func()) *AddressGate

WithRefusalObserver — приёмник отказа положения (клетка счётчика): отказ, невидимый снаружи иначе чем ответом вызывающему, считается.

type CallerPolicy

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

CallerPolicy enforces the per-RPC caller policy on the internal listener. Construct via NewCallerPolicy.

func NewCallerPolicy

func NewCallerPolicy(prodMode bool, gatewayOnlyRPCs []string) *CallerPolicy

NewCallerPolicy builds the caller policy. gatewayOnlyRPCs is the set of full-method names restricted to the api-gateway SA (see GatewayFrontedInternalRPCs). prodMode comes from cfg.AuthN.Mode.IsProduction().

The arm-3 METHOD set is static (SANRestrictedInternalRPCs); WithSANAllowlist only supplies WHICH certificate identities may call them. A policy built without WithSANAllowlist therefore denies every caller of those methods.

func (*CallerPolicy) Stream

Stream returns the stream interceptor enforcing the caller policy.

func (*CallerPolicy) Unary

Unary returns the unary interceptor enforcing the caller policy.

func (*CallerPolicy) WithModuleRoster added in v0.7.0

func (p *CallerPolicy) WithModuleRoster(r moduleidentity.Roster) *CallerPolicy

WithModuleRoster подставляет перечень личностей модулей установки (kaname#984): модулем признаётся только объявленная пара (пространство, учётка), и звать она вправе только методы своего профиля (ProfileMethods).

Политика, собранная без перечня, не признаёт модулем НИКОГО: пропущенная подстановка обязана дать отказ, а не прежнее «любой лист с приставкой».

func (*CallerPolicy) WithOwnFrontHop

func (p *CallerPolicy) WithOwnFrontHop(san string) *CallerPolicy

WithOwnFrontHop объявляет имя клиентского листа, которым СОБСТВЕННЫЙ REST-фронт службы приходит к этому же слушателю, и тем самым признаёт его ХОПОМ, а не вызывающим.

Предмет: сертификат на этом хопе называет НЕ ЗВОНЯЩЕГО

Фронт — обычный клиент своего же слушателя, и представляется он листом самой службы. Учётная запись у службы своя, приставки платформенных модулей она не несёт и в перечне личностей модулей не стоит, поэтому признание модуля (moduleidentity.Roster) на такой строке не срабатывает — и пол (рукав 1) отвергал КАЖДЫЙ запрос, пришедший через фронт, в боевой посадке. На стенде рукав вырождается целиком, поэтому расхождение было невидимо ровно там, где его ищут.

Что этот хоп получает — РОВНО ПОЛ, и ни одним методом больше

Вопрос пола — «стоит ли за запросом проверенный лист нашего внутреннего центра». На хопе фронта ответ ДА, и он получен на хоп раньше: в боевой посадке фронт поднимается только с требованием проверенного клиентского сертификата, а страж посадки отказывает в старте, когда это не так (`requireInternalRESTMutualClientAuth`). Тот же класс удостоверения, тот же внутренний центр — просто предыдущее звено.

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

Третий рукав хоп тоже не смягчает: он стоит выше и терминален, а чеканка маршрута HTTP не имеет вовсе.

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

С перечнем личностей (kaname#984) «любой проверенный модуль» читается буквально: у личностей разные профили, и хопу позволено ПЕРЕСЕЧЕНИЕ их перечней ([hopMethodSet]) — то, что позволено каждой. Фронт личности внутрь не переносит, поэтому всё сверх пересечения было бы шире gRPC-полосы для личности самого узкого профиля.

Почему величина приходит извне, а не выводится разбором

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

func (*CallerPolicy) WithSANAllowlist

func (p *CallerPolicy) WithSANAllowlist(perRPC map[string][]string) *CallerPolicy

WithSANAllowlist supplies the arm-3 allow-list: fullMethod → the exact client-certificate SPIFFE SAN URIs permitted to call it. Callers whose verified SAN is not listed are denied — in EVERY mode; a restricted method with no entry (or an empty one) is denied to everyone (fail-closed: the mint has no default caller). Exact URI match, not a service short-name — the ns AND the sa are part of the identity.

Blank entries are dropped so an accidentally empty config value (`KANAME_AUTHN__BOOTSTRAP_MINT__ALLOWED_CLIENT_SANS=""` → [""]) can never match a caller whose SAN failed to parse.

type ContextRelationChecker

type ContextRelationChecker interface {
	CheckWithContext(ctx context.Context, subject, relation, object string, condCtx map[string]any) (allowed bool, err error)
}

ContextRelationChecker — narrow port: ONE relation question, carrying a condition context. Satisfied by clients.RelationQueries.

type CredentialPresence

type CredentialPresence func(ctx context.Context) bool

CredentialPresence — приложил ли вызывающий удостоверение к ЭТОМУ запросу.

Предикат отвечает о ЗАПРОСЕ, а не о его годности: «предъявил и не сошлось» сюда не доходит — читатель отвергает такое выше по цепочке. Различие нужно ровно затем, чтобы не советовать назваться тому, кто уже назвался.

type DenyActionLookup

type DenyActionLookup interface {
	ActionForMethod(fqn string) string
	ScopeForMethod(fqn string) string
}

DenyActionLookup — port: full method name (no leading slash) → what the catalog says about that method.

ActionForMethod — the permission name, or "" when the catalog has no row for it or the row is exempt.

ScopeForMethod — the OBJECT TYPE on which that permission is granted (project / account / cluster), or "" when the row names none. It is the other half of the answer a refusal owes its caller: the action says WHAT is missing, the scope says WHERE it is granted — and therefore whom to ask.

Both are functions of the METHOD alone. That is not an implementation detail but the property that keeps the enriched refusal free of an existence oracle: nothing from the request, the object id or the subject enters the answer, so a refusal on an existing object stays byte-identical to a refusal on one that does not exist. Asserted by TestDenyNextStep_IsAFunctionOfTheMethodOnly.

Implemented by *seed.PermissionRegistry.

type InternalAddressGateRow added in v0.5.0

type InternalAddressGateRow struct {
	Row    InternalGateRow
	Reason string
}

InternalAddressGateRow — строка таблицы с доводом.

type InternalGateRow added in v0.5.0

type InternalGateRow int

InternalGateRow — строка таблицы Р4в: что рубеж делает на методе внутреннего слушателя.

const (
	// GateRefuses — метод круга края: отказ Р3 неподтверждённому человеку.
	GateRefuses InternalGateRow = iota + 1
	// GateAllowsSelfRevoke — снятие собственного удостоверения доступно
	// (`user_id` — сам принципал); о другом человеке — отказ Р3.
	GateAllowsSelfRevoke
	// GateOutsideCircle — метод вне круга края: рубежа нет, довод — у строки.
	GateOutsideCircle
)

type OwnDoor

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

OwnDoor — собственная дверь iam вместе с поправкой на вопросы о правах.

Тип существует затем, чтобы у двери был ОДИН конструктор: композиционный корень и пробы обязаны собирать её одинаково, иначе проба судила бы вторую проводку, которой в проде нет.

func NewOwnDoor

func NewOwnDoor(opts OwnDoorOptions) (*OwnDoor, error)

NewOwnDoor собирает звено решения о доступе для собственных слушателей iam.

Отказ возвращается там, где дверь получилась бы НЕДЕЙСТВУЮЩЕЙ: нет решателя, не выводится карта. Оба случая обязаны ронять старт, а не тихо давать пропускающее звено, — иначе провязка выглядит исполненной, и отличить её от исправной нельзя ничем.

func (*OwnDoor) Stream

func (d *OwnDoor) Stream() grpc.StreamServerInterceptor

Stream — то же на второй полосе.

Стримовых RPC у iam сегодня НОЛЬ, поэтому поправка здесь ничего не решает СЕЙЧАС и стоит ради того, чтобы решать, когда предмет появится: полоса без поправки при полосе с поправкой — различие, которого никто не принимал.

func (*OwnDoor) Unary

func (d *OwnDoor) Unary() grpc.UnaryServerInterceptor

Unary — звено публичного слушателя.

type OwnDoorOptions

type OwnDoorOptions struct {
	// SelfCheck — решатель ВЛАДЕЛЬЦА модели: собственный порт отношений iam.
	// Обязателен. nil → конструктор отказывает, а не заводит дверь, которая
	// никого не спрашивает: дверь без решателя пропускала бы всех, оставаясь на
	// вид провязанной.
	SelfCheck RelationChecker

	// Logger — журнал звена. nil → slog.Default().
	Logger *slog.Logger

	// CheckTimeout — предел одного вопроса к модели. ≤0 → умолчание звена.
	CheckTimeout time.Duration

	// DenyRateLimitPerSec — потолок темпа отказов на принципала. 0 → выключен.
	DenyRateLimitPerSec float64

	// PositiveTTL — окно вердикта: срок жизни ПОЛОЖИТЕЛЬНОГО ответа, то есть
	// время, в течение которого субъект с уже отобранным правом продолжает
	// проходить. Это параметр безопасности, а не производительности.
	//
	// Неположительной сюда НЕ ПРИХОДИТ: величину отвергает страж старта
	// (`AuthZConfig.Validate`, задача kacho#2307). Здесь стояло «0 — ЯВНОЕ „беру
	// умолчание политики“, как того требует конструктор кеша»: конструктор и
	// правда подставит умолчание, но умолчание это живёт в фундаменте, который
	// служба резолвит ПИНОМ, — то есть величина уезжала бы в чужую ревизию, а
	// оператор посадки не мог бы её ни сузить, ни прочитать.
	PositiveTTL time.Duration
}

OwnDoorOptions — то, что композиционный корень приносит двери.

type PublicCallerPolicy

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

PublicCallerPolicy держит политику вызывающего по глаголам на публичном слушателе. Строится через NewPublicCallerPolicy.

func NewPublicCallerPolicy

func NewPublicCallerPolicy(
	prodMode bool,
	peerCallable map[string][]string,
	acrCatalog ACRRequirementLookup,
	presented CredentialPresence,
) *PublicCallerPolicy

NewPublicCallerPolicy строит политику. peerCallable — таблица вызывающих глагол, не являющихся краем (см. PublicPeerCallableRPCs); prodMode берётся из cfg.AuthN.Mode.IsProduction().

presented — реализация порта CredentialPresence. Параметр ОБЯЗАТЕЛЕН и позиционен: сборка, забывшая его подставить, не соберётся вовсе — тогда как умолчание сделало бы её молча отвечающей «назовись» тому, кто назвался.

func (*PublicCallerPolicy) Stream

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

func (*PublicCallerPolicy) Unary

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

Хвост с вызовом ставится ДО возврата отказа. Отказ его установки (контекст без потока — так бывает лишь вне настоящего слушателя) вердикта не меняет: указание уходит тем же статусом, а REST-фронт без вызова поставит голую подсказку — ответ беднее, но не ложен и не шире.

func (*PublicCallerPolicy) WithModuleRoster added in v0.7.0

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

type RelationChecker

type RelationChecker interface {
	Check(ctx context.Context, subject, relation, object string) (bool, error)
}

RelationChecker — narrow port for a relation check. Satisfied by clients.RelationStore (same Check signature). Use-cases depend on this narrow interface, not on the whole decision door (Interface Segregation).

type RelationWriteGate

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

RelationWriteGate authorizes RegisterResource / UnregisterResource via ReBAC. It reuses the package RelationChecker port (scope.go) — the same FGA `Check(subject, relation, object)` surface used by the scope guard, satisfied directly by clients.RelationStore (no extra adapter at the composition root).

func NewRelationWriteGate

func NewRelationWriteGate(checker RelationChecker) *RelationWriteGate

NewRelationWriteGate — constructor. Defaults to dev-mode (backward-compat); use WithProductionMode to enable strict fail-closed enforcement.

func (*RelationWriteGate) Authorize

func (g *RelationWriteGate) Authorize(ctx context.Context) (string, error)

Authorize returns nil iff the verified mTLS client-cert resolves to a module ServiceAccount holding `fga_writer` on `cluster:cluster_root`. Every other outcome is PermissionDenied (fail-closed). Message text is the fixed, non-leaking `"permission denied"`. Authorize проверяет, что caller — модульная SA с `fga_writer@cluster:cluster_root`, и возвращает ее домен (vpc/compute/nlb) для object-type binding на write-path. Dev-mode без cert → ("", nil): домен неизвестен, domain-binding в ValidateProxyTuple отключается, но relation-allowlist и forbidden-object-type там действуют всегда.

func (*RelationWriteGate) WithModuleRoster added in v0.7.0

func (g *RelationWriteGate) WithModuleRoster(r moduleidentity.Roster) *RelationWriteGate

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

func (*RelationWriteGate) WithProductionMode

func (g *RelationWriteGate) WithProductionMode(prod bool) *RelationWriteGate

WithProductionMode toggles strict fail-closed enforcement (production AuthN).

type SystemViewerFloor

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

SystemViewerFloor enforces the `system_viewer@cluster` floor on the READ-RPC set. Construct via NewSystemViewerFloor.

func NewSystemViewerFloor

func NewSystemViewerFloor(checker RelationChecker, readFloorRPCs []string) *SystemViewerFloor

NewSystemViewerFloor builds the floor over the given READ-RPC set. Defaults to dev-mode (no-op); use WithProductionMode to enable strict enforcement.

func (*SystemViewerFloor) Stream

Stream returns the stream interceptor enforcing the floor.

func (*SystemViewerFloor) Unary

Unary returns the unary interceptor enforcing the floor.

func (*SystemViewerFloor) WithModuleRoster added in v0.7.0

func (f *SystemViewerFloor) WithModuleRoster(r moduleidentity.Roster) *SystemViewerFloor

WithModuleRoster подставляет перечень личностей модулей установки: пол спрашивает отношение у служебной учётки ОБЪЯВЛЕННОЙ пары.

func (*SystemViewerFloor) WithProductionMode

func (f *SystemViewerFloor) WithProductionMode(prod bool) *SystemViewerFloor

WithProductionMode toggles strict fail-closed enforcement (production AuthN).

Jump to

Keyboard shortcuts

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