access_binding

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2026 License: AGPL-3.0 Imports: 32 Imported by: 0

Documentation

Overview

Package access_binding — AccessBindingService.

Особенность: Create strict — дубль активного grant'а (5-tuple WHERE revoked_at IS NULL) поднимает 23505 из partial UNIQUE access_bindings_active_grant_uniq (migration 0003) → gRPC AlreadyExists с фиксированным текстом «these permissions are already granted to <subject_id> on <res_type>:<res_id>». Идемпотентный ON CONFLICT-upsert удален. Each Create/Delete emits outbox-event to invalidate authz-cache.

Index

Constants

View Source
const MinExpiresIn = 5 * time.Minute

MinExpiresIn — the shortest lifetime AccessBinding.Create accepts. A binding's access is materialised asynchronously within a bounded window, so anything shorter risks being revoked before it is ever usable; such a request is refused by name rather than issued as a grant that cannot be delivered.

Variables

This section is empty.

Functions

This section is empty.

Types

type CreateAccessBindingUseCase

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

func NewCreateAccessBindingUseCase

func NewCreateAccessBindingUseCase(r Repo, opsRepo operations.Repo) *CreateAccessBindingUseCase

func (*CreateAccessBindingUseCase) Execute

func (*CreateAccessBindingUseCase) WithReconciler

WithReconciler wires the γ selector reconciler (post-commit membership materialization). nil-safe.

func (*CreateAccessBindingUseCase) WithRelationStore

func (u *CreateAccessBindingUseCase) WithRelationStore(relations clients.RelationStore, logger *slog.Logger) *CreateAccessBindingUseCase

WithRelationStore wires the door to the rights decision. Grant tuples are emitted via fga_outbox (not through this client), but the door is required on the READ side by requireGrantAuthority to resolve delegated-admin grant authority (FGA `admin`/`system_admin` on the scope object). Logger is used for failure diagnostics.

type DeleteAccessBindingUseCase

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

func NewDeleteAccessBindingUseCase

func NewDeleteAccessBindingUseCase(r Repo, opsRepo operations.Repo) *DeleteAccessBindingUseCase

func (*DeleteAccessBindingUseCase) Execute

func (*DeleteAccessBindingUseCase) WithRelationStore

func (u *DeleteAccessBindingUseCase) WithRelationStore(relations clients.RelationStore, logger *slog.Logger) *DeleteAccessBindingUseCase

WithRelationStore подключает ДВЕРЬ РЕШЕНИЯ. Она нужна на READ-стороне: `requireGrantAuthority` спрашивает, вправе ли вызывающий снимать эту выдачу. Синхронного applier'а снятия за ней больше нет — см. разбор в шапке файла. Logger остаётся для диагностики самой проверки прав.

type ExpandAccessUseCase

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

func NewExpandAccessUseCase

func NewExpandAccessUseCase(l PrincipalLister) *ExpandAccessUseCase

func (*ExpandAccessUseCase) Execute

func (u *ExpandAccessUseCase) Execute(ctx context.Context, objectType, objectID, relation string, maxResults int) ([]Principal, bool, error)

Execute resolves <relation> on <objectType>:<objectID> into concrete principals. maxResults<=0 → default (1000); capped at 10000.

truncated=true means the answer is a LOWER BOUND, for either reason: the grant store cut its own enumeration at its server-side ceiling (no continuation token exists, so the rest is unreachable — narrow the query), or the resolved set exceeded maxResults and was trimmed here.

func (*ExpandAccessUseCase) WithGrantAuthority

func (u *ExpandAccessUseCase) WithGrantAuthority(repo Repo, relations clients.RelationStore, logger *slog.Logger) *ExpandAccessUseCase

WithGrantAuthority wires the per-object authority gate. repo resolves the owner-path for hierarchy scopes (account/project); relations resolves the delegated-admin FGA path for every scope. Mirrors the WithRelationStore wiring on Create/Delete/ListByScope. Logger is used for failure diagnostics.

type GetAccessBindingUseCase

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

func NewGetAccessBindingUseCase

func NewGetAccessBindingUseCase(r Repo) *GetAccessBindingUseCase

func (*GetAccessBindingUseCase) Execute

func (*GetAccessBindingUseCase) WithRelationQueries

WithRelationQueries wires the FGA ListObjects port so a label-selector grant (viewer ∪ v_list on iam_access_binding) lets its grantee Get the labeled binding — the D-6 additive path, consistent with the gateway already Checking v_get on AB.Get. nil → self/granted-floor only.

func (*GetAccessBindingUseCase) WithRelationStore

func (u *GetAccessBindingUseCase) WithRelationStore(relations clients.RelationStore, logger *slog.Logger) *GetAccessBindingUseCase

WithRelationStore wires the FGA client so the resource-scope grant-authority check (requireGrantAuthority) can resolve delegated admins — notably cluster-scope bindings, whose authority is `system_admin@cluster` in FGA (no DB owner).

type Handler

type Handler struct {
	iamv1.UnimplementedAccessBindingServiceServer
	// contains filtered or unexported fields
}

func (*Handler) Create

func (*Handler) Delete

func (*Handler) ExpandAccess

ExpandAccess — sync effective-principal audit "who can do <relation> on <object>". Resolves group usersets to concrete principals.

func (*Handler) Get

func (*Handler) List

List — the unified read (redesign-2026 F11). Page format is validated FIRST (page_size + page_token), BEFORE the use-case's listauthz visibility short-circuit, so a garbage token / page_size>1000 is INVALID_ARGUMENT regardless of grant state. Then the optional whitelist filter is parsed (unknown key → INVALID_ARGUMENT).

func (*Handler) ListAssignableRoles

ListAssignableRoles — sync read of the roles valid for binding on (resource_type, resource_id), each annotated with a server-computed scope_group. Thin transport: parse → use-case → format. resource_type/id validation, existence + grant-authority, and the isRoleAssignable filter all live in the use-case.

func (*Handler) ListByRole

ListByRole — sync audit "who holds role R". Each row carries the dual subjects[]/legacy projection; the use-case enforces the per-row grant-authority scope-filter.

func (*Handler) ListOperations

ListOperations — sync read of the operations recorded for the access binding (resource_id=acb-…: Create + Delete ops). Malformed id → InvalidArgument (first statement); well-formed-but-no-ops → empty list, not NotFound (parity). Viewer-tier authz is enforced by the api-gateway permission-catalog.

func (*Handler) ListSubjectPrivileges

ListSubjectPrivileges — sync, enriched read of a subject's DIRECT privileges role_name is resolved server-side via the repo JOIN; authz is "self OR account-admin of the subject's home Account" (use-case).

func (*Handler) Revoke

Revoke — soft-revoke of the binding (F10 IAM-1-28): the row is retained with status ACTIVE→REVOKED (audit-retention), the emitted FGA-tuple set is removed. Contrast with Delete (hard row-removal). Thin transport: parse → use-case → format.

func (*Handler) Update

Update — mutate the {deletion_protection, labels} set on a binding (T3.3-IMM-01): clear/toggle deletion_protection so a protected owner-binding can subsequently be deleted (C-03), and set own-resource labels (D-6 label-selectability). Any other mask path → INVALID_ARGUMENT (update_mask discipline enforced in the use-case).

func (*Handler) WithExpandAccess

func (h *Handler) WithExpandAccess(uc *ExpandAccessUseCase) *Handler

WithExpandAccess wires the effective-principal audit "who can do X".

func (*Handler) WithList

func (h *Handler) WithList(uc *ListUseCase) *Handler

WithListOperations wires the per-resource operation-listing use-case. Mirrors the core resources. WithList wires the unified List use-case (redesign-2026 F11).

func (*Handler) WithListAssignableRoles

func (h *Handler) WithListAssignableRoles(uc *ListAssignableRolesUseCase) *Handler

WithListAssignableRoles wires the assignable-roles read use-case.

func (*Handler) WithListByRole

func (h *Handler) WithListByRole(uc *ListByRoleUseCase) *Handler

WithListByRole wires the audit read "who holds role R".

func (*Handler) WithListOperations

func (h *Handler) WithListOperations(uc *shared.ListOperationsUseCase) *Handler

func (*Handler) WithRevoke

func (h *Handler) WithRevoke(uc *RevokeAccessBindingUseCase) *Handler

WithRevoke wires the soft-revoke use-case (F10 IAM-1-28).

func (*Handler) WithUpdate

func (h *Handler) WithUpdate(uc *UpdateAccessBindingUseCase) *Handler

WithUpdate wires the P6 deletion_protection Update use-case (C-03).

type ListAssignableRolesUseCase

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

func NewListAssignableRolesUseCase

func NewListAssignableRolesUseCase(r Repo) *ListAssignableRolesUseCase

func (*ListAssignableRolesUseCase) Execute

func (u *ListAssignableRolesUseCase) Execute(ctx context.Context, resourceType, resourceID string, f reporole.ListFilter) ([]domain.AssignableRole, string, error)

func (*ListAssignableRolesUseCase) WithRelationStore

func (u *ListAssignableRolesUseCase) WithRelationStore(relations clients.RelationStore, logger *slog.Logger) *ListAssignableRolesUseCase

WithRelationStore wires the FGA client so the delegated-admin grant-authority path (and the cluster-scope authority, which has no DB owner) resolves. When unset (nil) the use-case falls back to owner-only authority and denies the FGA path — same contract as ListByScope.

type ListByAccountUseCase

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

func NewListByAccountUseCase

func NewListByAccountUseCase(r Repo) *ListByAccountUseCase

func (*ListByAccountUseCase) Execute

Execute — D-6 visibility on the account-audit list: an account-admin (owner / FGA-admin / cluster-admin) sees EVERY binding in the account (the existing admin floor, NOT shrunk). A non-admin caller sees only the bindings made visible by a label-selector grant (viewer ∪ v_list on iam_access_binding) — the additive union floor, resolved PER-OBJECT over the page rather than by the server-capped ListObjects enumeration it replaces (internal/authzfilter). Anonymous → rejected. FGA error → UNAVAILABLE.

func (*ListByAccountUseCase) WithRelationQueries

WithRelationQueries wires the FGA ListObjects port for the D-6 viewer ∪ v_list union floor. nil → account-admin floor only.

func (*ListByAccountUseCase) WithRelationStore

func (u *ListByAccountUseCase) WithRelationStore(relations clients.RelationStore, logger *slog.Logger) *ListByAccountUseCase

WithRelationStore wires the FGA client for delegated-admin authority check. When unset (unit tests / degraded mode) only owner-based access is allowed.

type ListByRoleUseCase

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

func NewListByRoleUseCase

func NewListByRoleUseCase(r Repo) *ListByRoleUseCase

func (*ListByRoleUseCase) Execute

func (*ListByRoleUseCase) WithRelationStore

func (u *ListByRoleUseCase) WithRelationStore(relations clients.RelationStore, logger *slog.Logger) *ListByRoleUseCase

WithRelationStore wires the FGA client for the per-row delegated-admin scope filter. When unset (unit tests / degraded mode) only owner-based access passes.

type ListByScopeUseCase

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

func NewListByScopeUseCase

func NewListByScopeUseCase(r Repo) *ListByScopeUseCase

func (*ListByScopeUseCase) Execute

func (u *ListByScopeUseCase) Execute(ctx context.Context, resourceType domain.ResourceType, resourceID string, f repoab.PageFilter) ([]domain.AccessBinding, string, error)

Execute — D-6 visibility: a grant-authority on the scope (owner / FGA-admin / cluster-admin) enumerates ALL bindings on the scope (the existing granted-floor, NOT shrunk). A non-authority caller still sees the bindings on the scope made visible to them by a label-selector grant (viewer ∪ v_list on iam_access_binding) — the additive union floor, resolved PER-OBJECT over the page (the previous ListObjects enumeration was silently capped at 1000 objects of the type in the store, which denied a legitimate grantee outright — internal/authzfilter). Anonymous → rejected. FGA error → UNAVAILABLE.

func (*ListByScopeUseCase) WithRelationQueries

func (u *ListByScopeUseCase) WithRelationQueries(q clients.RelationQueries) *ListByScopeUseCase

WithRelationQueries wires the FGA ListObjects port for the D-6 viewer ∪ v_list union floor (label-selectable binding visibility). nil → granted-floor only.

func (*ListByScopeUseCase) WithRelationStore

func (u *ListByScopeUseCase) WithRelationStore(relations clients.RelationStore, logger *slog.Logger) *ListByScopeUseCase

WithRelationStore wires the FGA client for the scope-authority check.

type ListBySubjectUseCase

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

func NewListBySubjectUseCase

func NewListBySubjectUseCase(r Repo) *ListBySubjectUseCase

func (*ListBySubjectUseCase) Execute

func (*ListBySubjectUseCase) WithRelationQueries

WithRelationQueries wires the per-object question the PAGE is narrowed with.

func (*ListBySubjectUseCase) WithRelationStore

func (u *ListBySubjectUseCase) WithRelationStore(relations clients.RelationStore, logger *slog.Logger) *ListBySubjectUseCase

WithRelationStore wires the rights model for the cluster-admin and delegated account-admin admission lanes.

type ListPage

type ListPage struct {
	// Bindings — выдачи этой страницы, уже суженные вердиктом.
	Bindings []domain.AccessBinding
	// NextPageToken — продолжение обхода ВЫДАЧ. Соседние виды сопровождают
	// страницу и по страницам не режутся: их объём ограничен её содержимым
	// (членство) либо мал по построению (кластерные администраторы).
	NextPageToken string
	// Memberships — состав ТЕХ групп, что названы субъектами выдач на этой
	// странице. Членство в группе без выдачи доступа не даёт и к вопросу «кто
	// имеет доступ» не относится; видимость наследуется от выдачи, которую
	// вызывающий и так видит.
	Memberships []domain.GroupMember
	// ClusterAdmins — все действующие кластерные администраторы, и ТОЛЬКО тому,
	// кто сам кластерный администратор. Верхний ярус супер-доступа обязан быть
	// виден целиком тому, кто им распоряжается, и не обязан — арендатору:
	// перечисление имён администраторов облака арендатору не адресовано.
	ClusterAdmins []domain.ClusterAdminEntry
	// IncompleteMembershipGroups — группы, чей состав в этом ответе НЕПОЛОН.
	//
	// Состав ограничен сверху (членство неограниченно by construction — у него
	// для того и есть свой пагинированный глагол), и упереться в предел законно.
	// Незаконно — промолчать об этом: усечённое членство читается вызывающим как
	// факт о доступе, а «в группе больше никого» и «дальше мы не читали» — разные
	// утверждения.
	IncompleteMembershipGroups []domain.GroupID
}

ListPage — одна страница ПОЛНОГО перечисления поверхности выдач.

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

type ListSubjectPrivilegesUseCase

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

func NewListSubjectPrivilegesUseCase

func NewListSubjectPrivilegesUseCase(r Repo) *ListSubjectPrivilegesUseCase

func (*ListSubjectPrivilegesUseCase) Execute

func (*ListSubjectPrivilegesUseCase) WithRelationQueries

WithRelationQueries wires the per-object question the PAGE is narrowed with.

func (*ListSubjectPrivilegesUseCase) WithRelationStore

WithRelationStore wires the FGA client so the account-admin authz path (FGA `admin` on the subject's home Account) can resolve delegated admins who are not the account owner. When unset (nil) the use-case falls back to owner-only authority and denies delegated admins.

type ListUseCase

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

func NewListUseCase

func NewListUseCase(r Repo) *ListUseCase

func (*ListUseCase) Execute

func (u *ListUseCase) Execute(ctx context.Context, f repoab.ListFilter) (ListPage, error)

Execute reads the page from the iam database by cursor and returns the subset of it visible to the caller. The predicate fields on f (subject/role/scope/ scopeId) narrow the page at the SQL layer; visibility is applied to the rows that page yields.

func (*ListUseCase) WithClusterAdmins

func (u *ListUseCase) WithClusterAdmins(r clusterAdminReader) *ListUseCase

WithClusterAdmins провязывает чтение соседней поверхности кластерных администраторов (#914, решение 2). nil-safe: непровязанный порт вида не даёт.

func (*ListUseCase) WithListScanRecorder

func (u *ListUseCase) WithListScanRecorder(rec shared.ListScanRecorder) *ListUseCase

WithRelationQueries wires the FGA ListObjects port (viewer ∪ v_list floor). WithListScanRecorder провязывает съём стоимости страницы (#653).

func (*ListUseCase) WithRelationQueries

func (u *ListUseCase) WithRelationQueries(q clients.RelationQueries) *ListUseCase

func (*ListUseCase) WithRelationStore

func (u *ListUseCase) WithRelationStore(relations clients.RelationStore) *ListUseCase

WithRelationStore wires the FGA Check port for the D-9 cluster-admin super-gate. After the access-cascade contraction a cluster super-admin holds NO per-object viewer/v_list tuple on iam_access_binding (helpers.go requireGrantAuthority Path 0), so the per-object push-down alone would hand them an empty page — diverging from Get / ListByScope / ListByAccount / ListByRole, which all run requireGrantAuthority. nil-safe: an unwired gate does not fire.

No logger parameter (unlike the sibling builders). This used to be justified by "IsClusterAdmin swallows the FGA error, so there is nothing to diagnose" — that is no longer true: the failure of this question now refuses the request (UNAVAILABLE), so it is reported to the caller rather than buried, which is a stronger outcome than a log line and still needs no field here.

type ObjectForwardReconciler

type ObjectForwardReconciler interface {
	// ReconcileObjectForward is the ADDITIVE forward fast-path for one object: it
	// materializes ONLY that object's per-object tuples across the matching bindings
	// while holding NO advisory lock at all (neither EXCLUSIVE nor SHARE, no O(scope)
	// recompute). It transparently
	// delegates to the FULL ReconcileObject when the object already has members
	// (delete-stale guard) — which is the branch a REVOCATION takes.
	ReconcileObjectForward(ctx context.Context, objectType, objectID string) error
}

ObjectForwardReconciler — narrow post-commit port: re-materialize the per-object access of ONE iam-native object across the bindings whose selectors match it.

Deliberately NOT the package's SelectorReconciler (create.go): that port carries the binding-membership passes and the nothing-stale object entry point, none of which this path may use — a label-clear on an EXISTING binding must go through the guard-bearing forward (see doUpdate). It declares only the one method this path calls; the FULL ReconcileObject is driven by the reconcile worker off the co-committed event, never through here. Implemented by reconcile.Reconciler (the SAME single materialization path the reconcile worker and the cross-service RegisterResource drive). nil-safe: when unwired, the co-committed reconcile event + the periodic sweep remain the at-least-once backstop.

type Principal

type Principal struct {
	Type domain.SubjectType
	ID   domain.SubjectID
}

Principal — a concrete grantee resolved by ExpandAccess (USER or SERVICE_ACCOUNT; never a GROUP).

type PrincipalLister

type PrincipalLister interface {
	ListUsers(ctx context.Context, objectType, objectID, relation string, userTypes []string) (principals []string, storeTruncated bool, err error)
}

PrincipalLister — narrow port: resolve the CONCRETE principals (FGA-prefixed "user:…" / "service_account:…") that hold object+relation, traversing the full authorization graph (computed usersets + scope_grant indirection + group memberships). Implemented by the decision door over the relational form.

The second result is the SOURCE's truncation signal — whether the answer is a prefix is a fact only the source's reply can carry, because measuring the returned length against anything the use-case itself chose cannot detect a cut made upstream. It USED TO BE produced: the external engine bounded its own answer and offered no continuation. The form does not produce it — its enumeration is paged and continuable, so an incomplete answer that cannot be asked further does not arise — and it reads false. That false is HONEST, not a stub, and the field stays because the caller reads it and because the next source may truncate again.

type Reader

type Reader = kanamerepo.Reader

type Repo

type Repo = kanamerepo.Repository

type RevokeAccessBindingUseCase

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

func NewRevokeAccessBindingUseCase

func NewRevokeAccessBindingUseCase(r Repo, opsRepo operations.Repo) *RevokeAccessBindingUseCase

func (*RevokeAccessBindingUseCase) Execute

func (*RevokeAccessBindingUseCase) WithRelationStore

func (u *RevokeAccessBindingUseCase) WithRelationStore(relations clients.RelationStore, logger *slog.Logger) *RevokeAccessBindingUseCase

WithRelationStore wires the decision door (read-side grant-authority; synchronous revoke applier). Logger diagnoses sync-removal failures.

type RoleMembershipFanout

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

RoleMembershipFanout — the role.RulesMembershipFanout implementation.

func NewRoleMembershipFanout

func NewRoleMembershipFanout(repo kanamerepo.Repository, reconciler membershipReconciler) *RoleMembershipFanout

NewRoleMembershipFanout constructs the fan-out over the repo + the reconciler.

func (*RoleMembershipFanout) CountActiveBindings

func (f *RoleMembershipFanout) CountActiveBindings(ctx context.Context, roleID domain.RoleID) (int, error)

CountActiveBindings returns the number of ACTIVE/PENDING bindings of a role (the bound-check). Read-only, pool-scoped.

func (*RoleMembershipFanout) ReconcileActiveBindings

func (f *RoleMembershipFanout) ReconcileActiveBindings(ctx context.Context, roleID domain.RoleID) error

ReconcileActiveBindings re-materializes the role.rules membership of every ACTIVE binding of the role. Each binding is reconciled in its OWN writer-tx (the reconciler opens it), so a slow fan-out does not hold one long lock; the per- binding diff is idempotent (a re-run converges). The list is read once (pool-scoped); a binding revoked mid-fan-out is a no-op in ReconcileBinding (LoadBinding sees !Active). Bounded by the sync count-check upstream.

A failing binding does NOT abort the rest — see the loop below.

type RoleTupleReconciler

type RoleTupleReconciler struct{}

RoleTupleReconciler — the Role.Update reconcile fan-out.

func NewRoleTupleReconciler

func NewRoleTupleReconciler() *RoleTupleReconciler

NewRoleTupleReconciler constructs the reconciler (stateless — all state lives in the writer-tx passed to ReconcileRoleTuples).

func (*RoleTupleReconciler) ReconcileRoleTuples

func (r *RoleTupleReconciler) ReconcileRoleTuples(ctx context.Context, w kanamerepo.Writer, roleID domain.RoleID, newRole domain.Role) error

ReconcileRoleTuples reconciles every ACTIVE binding of roleID from its stored emitted-set (old) to the tuples derived from newRole (new), inside the caller's writer-tx (the same tx as the role UPDATE — atomic, ban #10).

Per binding:

  1. derive newTuples = buildBindingTuples(binding, newRole) — the thin-binding scope-anchor / per-rule scope_grant projection,
  2. read oldTuples = SelectEmittedTuples(binding),
  3. diff: removed = old\new, added = new\old,
  4. EmitRelationDelete(removed) + EmitRelationWrite(added) + ReplaceEmittedTuples(binding, newTuples).

Bounded: the fan-out iterates only the ACTIVE bindings of the SINGLE mutated role (ListActiveByRole), not all bindings. Idempotent: an unchanged tier yields removed=added=∅ (no fga_outbox rows, the ledger replace is a no-op swap) — so a re-run / replayed Operation does not double-emit.

type SelectorReconciler

type SelectorReconciler interface {
	// ReconcileBindingForward is the ADDITIVE create-path fast-path (sub-phase IAM-FMB):
	// it materializes THIS freshly-created binding's per-object membership additively while
	// holding NO advisory lock at all (neither EXCLUSIVE nor SHARE, no FOR UPDATE, no
	// delete-stale); the throughput fix
	// for a mass-binding-create burst. It transparently delegates to the FULL
	// ReconcileBinding if the binding already has members (delete-stale guard).
	ReconcileBindingForward(ctx context.Context, bindingID domain.AccessBindingID) error
	// ReconcileBinding is the FULL EXCLUSIVE path (Role.Update fan-out + sweep backstop);
	// still consumed by the reconcile machinery, not by the create hot-path.
	ReconcileBinding(ctx context.Context, bindingID domain.AccessBindingID) error
	// ReconcileObjectForwardNoStale is the ADDITIVE forward fast-path for the freshly-created
	// binding-AS-OBJECT (iam.accessBinding): it materializes ONLY that new object's
	// per-object owner/admin tuples across the matching bindings while holding NO
	// advisory lock at all (neither EXCLUSIVE nor SHARE, no O(scope) recompute); the throughput fix for the owner-tuple
	// materialization lag under a parallel create burst.
	//
	// The …NoStale variant asserts what only this call site knows: the object id was minted
	// in the writer-tx that has just committed, so it never existed before and carries
	// nothing stale. That matters because the SIBLING pass above
	// (ReconcileBindingForward) may already have written a member row ON THIS VERY
	// OBJECT — an anchor/`*.*` role in scope covers iam.accessBinding, i.e. the row just
	// created — and the plain ReconcileObjectForward reads such rows as "this object
	// existed before" and falls back to the FULL EXCLUSIVE ReconcileObject, the very
	// serialization this fast-path exists to avoid (measured: account-admin verbs ~67s
	// after create). The delete-stale guard is untouched for every other caller.
	ReconcileObjectForwardNoStale(ctx context.Context, objectType, objectID string) error
	// ReconcileObject is the FULL EXCLUSIVE object-fan-out (async at-least-once backstop —
	// delete-stale / audit / sweep); still driven by the reconcile worker off the
	// co-committed reconcile-outbox event, not by the create hot-path.
	ReconcileObject(ctx context.Context, objectType, objectID string) error
}

SelectorReconciler — narrow port (γ): materialize a selector binding's membership right after Create commits, so the membership + per-object FGA tuples are observable when the Operation reports done (γ-01). Implemented by the reconcile.Reconciler. Optional — nil-safe: when unwired the periodic sweep (D12) still materializes it, just not synchronously.

rbac-contract-a-fix (C-01b): the port ALSO exposes ReconcileObject — the freshly-created access_binding is itself an iam-native OBJECT (iam.accessBinding) whose per-object access (owner / account-admin admin/v_*) must be materialized under the flat model (the `from <scope>` ACCESS cascade on iam_access_binding is gone). ReconcileBinding materializes THIS binding's own grant membership; ReconcileObject materializes every OTHER binding's per-object access ON this new binding-object. Both run synchronously post-commit so a GET right after the Operation reports done does not race the async drain.

type UpdateAccessBindingUseCase

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

func NewUpdateAccessBindingUseCase

func NewUpdateAccessBindingUseCase(r Repo, opsRepo operations.Repo) *UpdateAccessBindingUseCase

func (*UpdateAccessBindingUseCase) Execute

func (u *UpdateAccessBindingUseCase) Execute(ctx context.Context, id domain.AccessBindingID, mask []string, deletionProtection bool, labels domain.Labels) (*operations.Operation, error)

Execute — sync validate (id + update_mask discipline + grant-authority) → Operation → worker (deletion_protection + labels в одной writer-tx). AB mutable set = {deletion_protection, labels} (T3.3-IMM-01).

func (*UpdateAccessBindingUseCase) WithObjectReconciler

WithObjectReconciler wires the post-commit per-object materializer used on a LABEL change (parity with the cross-service RegisterResource re-register path — see doUpdate). Optional; nil keeps the queue-only behaviour. It takes no logger on purpose: WithRelationStore already supplies one, and a second setter carrying it would silently nil it whenever the two are wired in the other order.

func (*UpdateAccessBindingUseCase) WithRelationStore

func (u *UpdateAccessBindingUseCase) WithRelationStore(relations clients.RelationStore, logger *slog.Logger) *UpdateAccessBindingUseCase

WithRelationStore wires the door to the rights decision used by requireGrantAuthority (delegated-admin authority path), mirroring Create/Delete.

type Writer

type Writer = kanamerepo.Writer

Directories

Path Synopsis
Package reconcile — selector/containment reconciler.
Package reconcile — selector/containment reconciler.

Jump to

Keyboard shortcuts

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