Skip to content

Latest commit

 

History

History
1428 lines (1159 loc) · 129 KB

AUTHORIZATION.md

File metadata and controls

1428 lines (1159 loc) · 129 KB

Authorization

This document consolidates all authorization-related knowledge for the platform.

Tenant Directory-Operator Identity Resource

GET /api/tenant/settings/documents/directory-operator-identity and PATCH /api/tenant/settings/documents/directory-operator-identity are authenticated tenant-admin resources. Server authorization, exact tenant binding, CQRS validation, and optimistic concurrency remain authoritative. Cross-tenant body IDs are not accepted because tenant scope comes from the trusted request context.

The resource assembler emits _links.edit only when the caller may patch the document. Blazor treats that exact relation and its PATCH method as the sole Save affordance authority; DTO booleans, roles, claims, source type, and case-insensitive relation guesses do not authorize UI actions. Anonymous public settings expose only normalized public facts and never the edit resource, concurrency stamp, audit actor, or readiness blocker payload.

Discovery Identity Authority

Discovery identity status is handler-authorized: public source eligibility is independent of management access, and public relationship targets must pass their own current eligibility check. Candidate-specific review and reverse HAL relations require current membership, management and explicit action grants across both identity groups, without owner/contributor conflicts. The browser never derives these actions from claims. These read-side capabilities do not replace the command's fresh fenced authority or expected tenant identity epoch. RoleEnum.EventDiscoveryReviewer (45) is an explicitly assignable event preset with event:view, event:update and the two discovery decision permissions. EventRoleAuthorityCeilingService retains its same-event subset ceiling; owner seed permissions include review/reversal so owners can delegate that preset. The typed conflict facts still deny owner, creator and publisher decisions even with these permissions. Existing EventManager permissions remain unchanged, and lookup seeding creates no reviewer assignment. The normal Team assignment and revocation commands own the user grants; HAL remains the UI authority. The command protects the undeleted reviewer, membership, actor/event ownership, event-role assignments, roles, active permissions and role-permission bindings with native row fences. Authority locks precede sorted identity locks and the terminal tenant epoch; ordinary updates and deletes therefore participate without adopting advisory lock names. PostgreSQL snapshot conflicts replay the complete serializable attempt, including authority and provider evaluation, rather than retrying only the final save. Grant validity is evaluated again after provider awaits. See identity correction surfaces.

Table of Contents

  1. Overview
  2. Authentication vs. Authorization
  3. Core Authorization Components
  4. Authorization Providers
  5. Roles and Permissions
  6. Implementation Patterns
  7. Related Documentation

1. Overview

The platform employs a multi-layered authorization strategy to ensure robust and flexible access control. It combines endpoint-level checks, fine-grained resource-level authorization within the application's business logic, and a runtime-pluggable provider model that supports both a sophisticated policy engine (Cerbos) and a local role-based access control (RBAC) fallback. This ensures security is enforced at multiple depths, from the web request down to individual data access.

2. Authentication vs. Authorization

Per ADR-027 and ADR-001, the platform enforces a strict separation of concerns between authentication and authorization:

  • Authentication (Local Identity / Keycloak / AT Protocol): The process of verifying who a user is. Exactly one provider is primary for new sign-ins. The Blazor BFF owns browser challenges and HttpOnly cookies; raw provider and platform tokens remain server-side.
  • Authorization (Cerbos / Fallback): The process of determining whether an authenticated user has the permission to perform a specific action on a specific resource. Cerbos acts as the policy decision point (PDP), evaluating fine-grained business resource policies against principal, resource, action, and tenant context.

ATProto does not introduce a second authorization model. The API independently restores the submitted CarpaNet OAuth session and verifies com.atproto.server.getSession against the expected DID and PDS. When AT Protocol is primary, the verified DID may JIT-create one passwordless platform User, personal Actor, and global UserExternalLogin; the subsequent purpose-separated ES256 session JWT uses that internal user Guid as sub. Existing operation authorization, tenant isolation, Cerbos/local fallback policies, and HAL affordance filtering operate unchanged. A DID, handle, or PDS response never authorizes a resource or grants a role by itself.

ATProto credential operations remain server-private. The bootstrap/current-session/delete bridge is excluded from API discovery and generated browser contracts; generic raw-token, UserExternalLogin, and IndexedDid CRUD routes do not exist. Public session reads return only the current user's safe metadata (id, provider, pdsHost, expiresAt), and deletion is authorized, self-scoped, and idempotent. Direct AtprotoRecord mutations are likewise absent: verified authentication, lifecycle-owned outboxes, and authoritative Jetstream ingress are the only identity and record-write authorities. If a client renders a resource action, it must use the returned HAL relation rather than infer authority from the ATProto provider, DID, roles, or claims.

3. Core Authorization Components

Provider Account Authority

PlatformIdentityPrincipalExtensions derives both the provider code and the persisted OIDC account-key kind from the same normalized issuer authority. Direct https://accounts.google.com identities use Google; Keycloak-brokered Google identities retain the Keycloak issuer and subject namespace. An idp hint or a hostname containing the Google hostname cannot change that authority. Trusted first-party auth_provider claims retain their explicit precedence. Issuer normalization does not change subject case or bytes, and missing or invalid issuers cannot reconstruct an OIDC account key.

For authentication, JWT validation, and security-header behavior, see SECURITY_OVERVIEW.md.

3.1. Endpoint-Level Authorization

  • Mechanism: Standard ASP.NET Core [Authorize] and [AllowAnonymous] attributes on API controllers.
  • Convention:
    • GET requests are generally [AllowAnonymous] to support public discovery.
    • POST, PUT, DELETE, and PATCH requests are [Authorize] by default, requiring an authenticated user.
  • Purpose: A coarse, first-line defense at the entry point of the API.

3.2. Resource-Level Authorization (Native Operations)

Resource authority protects native CQS operations through authorization decorators.

  • Enforcement point: RequestAuthorization<TRequest> is utilized by native authorization decorators (AuthorizationCommandHandlerDecorator and AuthorizationQueryHandlerDecorator). Native void commands, result commands and queries are always wrapped, with authorization outside timing.
  • Trust ordering: [AuthorizeResource] selects the catalog capability; ISecureRequest supplies optional typed facts; a typed enricher may refine context; the persisted-resource resolver has final authority before the provider call.
  • Failure behavior: ordinary denial throws AuthorizationException (HTTP 403); provider-unavailable decisions throw AuthorizationProviderUnavailableException with the existing distinct unavailable response. Neither reaches the business operation.
  • Composition: final descriptor validation rejects raw native replacements; runtime startup checks cached parameter metadata, while CI deeply resolves the actual provider in disposable scopes. See Protected Native Operations.

Event Category Assignment Authority

The seven EventCategories operations use protected native command/query ports. DeleteEventCategoriesAuthorizationContextEnricher, registered in ApplicationServicesRegistration, loads the tenant-visible assignment and supplies its persisted EventId to RequestAuthorization. The shared resource resolver then loads event facts for the existing Event:update capability; an assignment ID is not an event authority. Missing or tenant-invisible assignments throw AuthorizationException before provider evaluation, even with an allowing provider.

Update retains source-event enrichment. When the event changes, UpdateEventCategoriesCommandHandler resolves persisted destination event facts and asks IAuthorizationProvider for the same Event:update capability before assigning any entity field. Source authority never transfers to the destination; both event checks must allow. Denial throws AuthorizationException; a provider-unavailable decision throws AuthorizationProviderUnavailableException. Neither changes assignment fields, concurrency stamps, or persisted readback. Same-tenant validation, duplicate checks, optimistic concurrency, repositories and cache invalidation remain in their existing handler-owned order. These checks do not introduce atomic authorization-revocation/mutation fencing or cancellation support inside tokenless repositories.

NativeEventCategoriesOperationTests exercises production-registered ports in a real SQLite host, including owner deletion, missing/foreign assignments, unauthorized same-tenant relocation, an explicit persisted destination grant, and source/destination denial versus provider outage with unchanged readback. This changes neither category-definition administration nor EventTags policy, and adds no HTTP endpoint, schema migration, policy grant, or configuration. See the operator guide.

Event Tag Assignment Authority

All seven EventTags operations use protected scoped native command/query ports. The scoped DeleteEventTagsAuthorizationContextEnricher resolves the tenant-visible assignment's persisted parent before the shared resolver evaluates Event:update. An assignment ID is not an event ID. Missing or tenant-invisible assignments fail closed before the provider, just as they do for the existing update enricher. Request-supplied EventId and TenantId do not replace persisted source authority.

For relocation, UpdateEventTagsCommandHandler resolves persisted destination facts and requires the same Event:update capability on the destination before changing either the event or tag field. Source permission alone is insufficient, even within one tenant. Denial and provider unavailability retain distinct AuthorizationException and AuthorizationProviderUnavailableException outcomes; neither changes tracked or fresh assignment state, concurrency stamps, or caches. Successful updates retain source/destination detail and tenant-list cache eviction. Manual validators, entity repositories, grouped update semantics, immutable mapping snapshots, tenant filters, composite foreign keys and uniqueness remain unchanged.

NativeEventTagsOperationTests reproduces the legacy owner-delete denial and source-only relocation defect, then exercises production-registered native ports with real Local RBAC and a controlled external-provider boundary. In-memory and SQLite lanes cover both-authorized relocation, missing/foreign assignments, source/destination denial and outage, unchanged tracked/fresh state and relational constraints. There is no new API, policy grant, schema or configuration. This does not add atomic revocation/mutation fencing or cancellation inside the existing tokenless repositories; validators, cache operations and the new provider check receive the supplied token. Live Cerbos transport is outside this cohort's tests. See the operator guide.

Reviewed Handler And Worker Authorities

Not every authority is a resource role or an ordinary login. The exact reasoned NamedOperationExceptions in AuthorizationSurfaceGuardrailTests recognize the following existing enforced boundaries; they do not add pipeline bypass metadata or classify arbitrary Local-named requests. The inventory retains conservative command/namespace/command-response discovery (including the preview Query), rejects unrelated synthetic requests, and checks that each disposition still resolves to a compiled request. Its entries cite native boundary test classes.

Namespace prefixes below are exact: LC = Explore.Application.Features.Authentication.Local.Requests.Commands, LH = Explore.Application.Features.Authentication.Local.Handlers.Commands, EDC = Explore.Application.Features.EmailDispatch.Requests.Commands, EDQ = Explore.Application.Features.EmailDispatch.Requests.Queries, IOC = Explore.Application.Features.InstanceOnboarding.Requests.Commands, RC = Explore.Application.Features.RegistrationOrders.Commands.

Exact request Enforced authority
LC.ChangeLocalPasswordCommand Fresh ordinary Local session, Ready binding, current password and native stamp CAS.
LC.CompleteLocalPasswordRecoveryCommand Exact recovery-purpose token/receipt, one-use password mutation, original-token mirror retry only.
LC.ConfirmLocalEmailCommand Exact verification/change token and operation/subject/actor/link/purpose/generation, native receipt CAS.
LH.ReconcileLocalIdentityLifecycleMirrorCommand Trusted delivery worker only; current consumed native receipt, result stamp and exact binding. No API dependency or pointer-only public dispatch.
LC.RequestLocalEmailVerificationCommand Bounded current-address public admission, or fresh ordinary Local authority for proposed address; no alternate account selection.
LC.RequestLocalPasswordRecoveryCommand Bounded non-enumerating native recovery admission for verified-email Ready Local bindings.
LC.CompleteLocalCredentialReplacementCommand Dedicated restricted replacement proof validated against the current first-use operation; not ordinary authentication.
LC.CreateLocalIdentityCommand Authoritative user and fresh persisted instance-admin membership before mutation and one-time disclosure.
LC.ReconcileLocalCredentialOperationCommand Fresh instance-admin membership and exact provisioning receipt graph; conflicting partial bindings denied.
LC.ResetLocalCredentialCommand Fresh instance-admin membership, exact predecessor operation/stamp and current handover receipt.
EDC.DisableEmailDeliveryCommand Current platform admin or exact current tenant/admin grant rechecked under ordered SMTP lease/Serializable transaction; actor/target/revision/token/acknowledgement binding.
EDQ.PreviewEmailDeliveryDisableQuery Same fresh scoped admin checks and lease-owned snapshot before issuing an actionable confirmation.
IOC.CompleteLocalInstanceOnboardingCommand Authenticated SetupSecret identity and durable setup mode, active Local provider, server deployment mode, validation and preflight before native convergence.
RC.CancelConfirmedGuestRegistrationCommand Fresh exact limited guest capability and live deadline under fences, then native transactional cancellation eligibility and effects.
RC.ConsumeAnonymousRegistrationChallengeCommand Trusted normalizing adapter and native tenant/event/digest/key/envelope/nonce proof validation; intended typed selection is snapshotted before allocation or disclosure.
RC.IssueAnonymousRegistrationChallengeCommand Current public event/visitor/participation eligibility, live finite end promise, bounded difficulty and transactional tenant/event quota; no allocation.

The five Local email/password lifecycle commands live in Requests/Commands; the shared consumption orchestrator remains in the Local feature. Worker repair is separate from administrator provisioning reconciliation and from public token consumption. Native API lifecycle/credential/onboarding/challenge/status tests, Persistence first-use/lifecycle/email-disable/bootstrap/quota/cancellation tests, and BFF purpose/antiforgery tests retain runtime rejection evidence. These architecture dispositions are not a substitute for running those tests.

See Endpoint Classification for the four precise anonymous Local lifecycle actions and capability cancellation's enforced no-key/no-generic-replay contract. Neither limited proof nor replayed HTTP output grants ordinary login or administrator authority.

3.3. Runtime Authorization Provider

The actual logic of "is this user allowed to do this?" is delegated to a runtime provider. This allows the authorization engine to be swappable.

  • Wrapper: RuntimeAuthorizationProvider is injected into the AuthorizationBehavior and decides which concrete provider to use.
  • Providers:
    • CerbosAuthorizationService: Offloads decision-making to an external Cerbos Policy Decision Point (PDP).
    • FallbackAuthorizationService: Uses a local, database-backed RBAC implementation when local authorization is selected.

Persisted targets and public-action mutations

Human CreateTenantCommand authorization is evaluated by HumanTenantCreationPort after the mutation decorator selects the dedicated tenant-creation Global owner. The outer RequestAuthorization delegates only this exact human command; projecting a human principal earlier would bind an ordinary owner and correctly prevent later purpose retargeting. The creator still evaluates the unchanged Tenant/create question against original native principal sources and the selected Local or instance Cerbos provider. A denied or unavailable provider cannot fall back to administrator claims. Original credential, purpose, source, transaction and final lifetime checks remain in force through commit; machine callers retain outer evaluation and are rejected by the human creation scope. This is ordering, not a new authorization exemption for other requests or a relaxation of immutable owner selection. Authored API regressions remain locally unbuilt/unrun; remote verification is required.

The native creator acquires original Human security-source custody before Discovery enrollment, before beginning its serializable transaction. Both gates remain held through source revalidation, graph persistence and commit settlement; cleanup releases Discovery before Human custody. An already-retained Discovery gate is rejected rather than acquiring Human custody in reverse order. Failed Discovery acquisition releases the newly acquired Human gate without creating a tenant. Transaction-outcome evidence and cleanup failures must be preserved together, so failed release cannot turn an uncertain commit into an apparently safe retry. The existing missing-custody checks remain enforced; no permissions, schema, migration or configuration setting are added. The native HTTP regressions cover creation with and without administrator membership and failed enrollment acquisition; they remain locally unbuilt/unrun pending remote verification.

AuthorizationResourceContextResolver does not substitute request facts when an Event, Session, OrganizationMember, persisted StorageObject or projection parent cannot be loaded. Persisted targets must match the nonempty server tenant. Session and projection decisions also resolve their current event parent; member decisions require the persisted organization participation and parent. Pre-create facts apply only to creation, and an event parent is still resolved. Organization creation remains a global resource operation. Tenant-wide projection/status and storage-list scopes remain collection operations, not evidence about an individual object.

The decorator's decision is not a transaction receipt. Event public-action create, update and delete handlers call the selected provider themselves and then acquire current native authority in their own Serializable transaction. EventPublicActionRepository.GetAuthorizedEventForMutationAsync holds the persisted User, tenant membership, Actor/Event ownership, relevant assignments, roles and permissions through commit. Tenant administration, the publishing user's ownership, organization administration, and explicit event:manage-public-actions permission retain their distinct predicates. Platform administration alone is not authority for this action. An expired or revoked event grant cannot be replaced by an authentication claim or an earlier allow. Actual action id, parent id, tenant and concurrency stamp are checked again, including when an object is already tracked.

Provider evaluation precedes native locks deliberately: administrative discovery uses an independent Global connection, which must not wait on a row locked by the same logical operation. Native authority is reacquired after the provider response and on each Serializable retry. Cancellation and failures retain the existing unit-of-work rollback and tracking cleanup; selected-provider unavailability remains a denial rather than a local grant.

These mechanisms are not a blanket authorization exemption for other handlers, workers or raw SQL owners. Their action-specific persisted predicates and transaction contracts remain required. The Phase 21 task ledger distinguishes authored source from the unrun programme-final native assurance gates.

API-key management authority

Key detail, policy-update and revocation handlers resolve the canonical subject and own a Serializable transaction. ExternalApiKeyManagementAuthority reuses IExternalApiKeyIssuanceAuthority for tenant-bound ownership and IInstanceAdministratorCommitAuthority for instance ownership. The latter does not require the acting administrator to be the key's original owner. These ports fence actual identity, membership, role and permission rows; a role claim or an earlier administrative lookup does not substitute for them.

ExternalApiKeyRepository loads no-tracking candidates from the current tenant. Null-tenant instance keys use separate purpose-bound execution scopes. After owner authorization, GetForManagementAsync fences and rereads the exact key in the same transaction. The handlers compare persisted tenant, owner type and owner id before returning details or applying changes. Revocation remains idempotent, and mutation metrics are recorded only after commit.

The approved D03 capability amendment adds four independent read/write/issuance/ report pools. Fixed execute-only PostgreSQL functions enforce current user, provider binding, platform-admin grants and exact null-tenant instance-key ownership. List-mine stays self-owned; exact administration preserves cross-owner instance-admin access. The selected instance PDP evaluates fixed instance.api-keys.* resources through the existing InstanceSetting policy inside the transaction before native fences. Fixed writes cannot change owner, tenant, key identity or credential hash.

Issuance keeps first-only disclosure and digest-only nonreopening receipts. PostgreSQL derives the receipt selector from actor plus bounded operation key; recovery returns redacted metadata only. The existing privacy authority remains the outer fence, with co-located SQLite sharing the dedicated connection's actual transaction. Historical reporting returns existing aggregates across inactive tenants and instance keys; a requested tenant report binds a fresh Runtime lease and its own provider/admin/read transaction.

The credential, native-function and admission source is authored but unrun. Profile admission rejects missing grants/functions instead of inventing a Runtime bypass. Existing Global/authentication/scanner capabilities are unchanged; source authorship is not a completed provider assurance receipt.

Admission also rejects purpose contamination: actual inherited membership may reach only the selected executor, and other-purpose fixed-function execution is forbidden even when granted directly to the login. Exact other-key, authentication and scanner signatures are checked alongside a catalogue-based allow-list of executable application-schema SECURITY DEFINER functions. No existing Global, authentication or scanner grants are changed by admission.

Event agenda mutation authority

The three agenda mutation handlers evaluate the selected provider inside their Serializable transaction before native row fences. They then bind the current principal/provider identity, actual agenda permission and persisted event parent. Agenda administration retains its existing instance/tenant/org/owner/manager semantics; this does not expand ordinary event-update or public-action rights. Child rows are reread and fenced before changes, and relocation also checks the destination's separate event-update policy and current authority.

EventAgendaItemRepository.MoveToEventAsync checks the stored source tenant/parent/stamp, both current parents, and the stored destination placement and room before its parameterized update. Tracked parent substitution cannot authorize that operation. Creation reserves its aggregate identity and audit time before retries; entities and schedule projections are rebuilt per attempt. The native source cases are authored but unrun under the programme override.

Event deletion authority

DeleteEventCommandHandler owns selected-provider evaluation, current EventDelete authority, paid-evidence lookup, session enumeration/fences, cascading deletion and federation outbox planning in one Serializable transaction. It no longer treats a tenant grant with the legacy Admin code as system administration. Actor ownership, organization administration and current event-delete grants remain subject to the selected Event policy; platform or tenant administration alone does not grant deletion.

Missing, authority-denied and paid-evidence response categories remain stable. Provider unavailability stays distinct from ordinary denial, and cache work remains after commit. Diagnostics retain fixed outcomes and session count without actor/tenant/resource identifiers or titles. Native regression source is authored; executable assurance remains pending.

Event session mutation authority

Session create/update/delete use their own selected-provider decisions and current session-specific native authority inside Serializable transactions. The event-child resolver preserves the existing administrator and event-role rules; moving a session additionally requires the existing Session/create pre-create admission for its persisted destination event, not Event/update. The selected decision uses stored destination tenant/parent facts inside the Serializable transaction; the ordered native destination fence independently requires current session-create authority before effects. Persisted tenant, parent, stamp, placement and room are checked before the raw move. Published-session relocation and ticket-entitlement deletion restrictions remain enforced.

Creation now keeps Islamic metadata, template definitions/options/default values and projection creation in the session transaction rather than writing them after the session commit. Root identity/time survive rollback-known retries, and template audit fields use the canonical actor. Existing update fanout, refund and reminder effects retain their original transaction owner. Native scheduling, permission, tracked-state, provider-outage and shared-commit cases are authored but unrun.

Persisted machine mutation commit authority

Session mutations and footer link/group PATCH combine the selected provider decision with IMachineMutationCommitAuthority on the actual Runtime Serializable business connection. This does not widen Local's InstanceAdmin session allow-list or introduce Organization/Group session authority. Configured keys and internal machine execution have no persisted authentication proof and fail closed for these mutations, even if their KeyId matches a stored key.

Successful persisted API-key authentication attaches PersistedApiKeyAuthenticationProof as a server feature. Its row identity, KeyId, owner, tenant, status, deadline and copied scopes are immutable; the bound ClaimsIdentity is not. The accessor therefore requires exactly one authenticated identity, the original reference, unchanged key/owner/tenant/ scope fields, and purpose-specific User metadata. User-owned keys have exactly one matching internal_user_id; other keys have none. Subject, provider, nameidentifier or session-identity contamination invalidates that proof. An internal execution override cannot borrow it; clearing the override can restore only the original still-valid HTTP proof.

User-key delegation uses the validated proof owner and current owner/role fences. API-key identity remains excluded from ambient human identity: new placement creation, final-carrier placement detachment, template actor attribution, published material-change/refund paths, and the six human-only footer actions retain their real-human requirements. In particular, footer settings PATCH is not a machine operation.

Tenant-key fences use exact Runtime tenant access. Native checks retain Active/PendingRotation only, intersect required scopes in both the authentication snapshot and current row, verify exact identity/owner/tenant, and evaluate live expiry after target waits. Session guarded writes invoke the caller's final check after placement/room/overlap acquisition; a final check before commit also rolls back expiry during later transactional work. Footer PATCH holds the current link-group governance lease, then principal/key/tenant/group/optional-link fences, and rechecks immediately before mutation and before commit. Fresh repository reads reject tracked ownership substitution or parent drift.

Null-tenant InstanceAdmin footer PATCH alone uses PostgresMachineMutationAuthorityContract: its fixed boolean function accepts the exact group and optional link, never an operation name or arbitrary setting key. It rejects foreign and instance-default groups, holds actual owner, key, tenant, group and link locks, and reads clock_timestamp() after contention. The function cannot enumerate keys, read credentials/usage, change settings, or borrow Global/authentication/scanner/management credentials. The dedicated Runtime executor has only function execution; installer/admission verify ownership, membership, function identity/body, fixed search path, PUBLIC denial and cross-purpose isolation. Source and native cases are authored; execution and provisioning remain deferred to programme assurance.

Room-update transaction authority

UpdateLocationRoomCommandHandler owns a Serializable transaction for both ordinary edits and relocation. It repeats the selected room/update provider decision, fences current human administration or authentication-owned persisted-key authority, then holds the actual room and ordered source/ destination locations. Current tenant lifecycle, deleted rooms, erased locations, tracked parent substitution and the supplied concurrency stamp are checked before effects. Schedule references still prevent relocation. Machine expiry is checked after target acquisition and again before commit.

Cerbos's existing null-tenant InstanceAdmin room-update cell uses the distinct PostgresMachineRoomMutationAuthorityContract and machine_room_update_authority function. Local's machine allow-list is unchanged. The function derives the current source from the locked room, accepts only an exact optional destination, and holds key/owner/tenant/room/ location locks through the caller's commit. It has separate owner, migrator and Runtime executor roles, no credential pool, no enumeration and no secret/hash/usage access. It neither calls nor broadens the footer function. Native handler/provider and ordinary-Runtime cases are authored but unrun.

Native Local session validation

After native signature and audience validation, co-located PostgreSQL session validation uses human_principal_source_session(uuid,text,uuid) on the original Runtime context, connection, transaction and selected tenant, including a legitimate null-tenant instance request. It returns bounded revocation facts, not an Identity user or a human admission. Runtime receives execution only. Exact source, ownership, role and ACL attestation use the existing principal source contract and migrator installation.

LocalIdentityCredentialStateStore.ValidateReadySessionAsync requires the ordinal stamp match, email state, password presence, Ready metadata, replaced Create/Reset operation and exact live user/login/personal-actor binding. Instance email policy still distinguishes invalid sessions from unavailable validation. Login keeps its complete entity and response path; this projection does not fabricate a LocalIdentityUser. All fourteen native Runtime controls pass with both null and selected tenants, including invalid-session outcomes and denied direct access to private identity tokens. The selected-tenant fixture uses the production PostgresTenantSessionInterceptor; caller guards remain unchanged.

Original resource and visitor Settings ownership

Standalone selected human resource and visitor writes acquire their complete ordered policy key group before executing the prepared producer's Serializable attempt. Joined writes retain the acquired original producer and existing locks; an arbitrary caller-owned transaction is not sufficient. Values and lock changes remain separate storage effects: resource value upsert does not implicitly unlock a row. Fixture migrations exercise actual producers and rollback rather than authorizing a write by naming an import operation.

Current human Cerbos principal inputs

CerbosPrincipalBuilder obtains human policy inputs through IHumanCerbosPrincipalReader, rather than ordinary administrative or membership repository queries. HumanCerbosPrincipalReader prepares the original selected owner, retains the resolved subject, authentic credential lifetime, HTTP context, principal reference and ambient tenant, then observes IHumanPrincipalSourceFence while idle. It validates current account/session sources and caller continuity before projecting the existing Cerbos attributes, including ordered permission lists. It opens no new owner or transaction and grants no private-table access.

This is a current read, not commit admission. A mutation must prepare its decision while idle, then acquire and finalize its real sources under one original attempt. The reader deliberately cannot supply a cached observation to authorization inside an already-open transaction. Source diagnostics are clean; executable assurance for this integration remains pending.

Ordinary AI tool proposal custody

ProposeAiToolActionCommandHandler prepares ordinary proposals through IHumanAiToolProposalPreparation before entering one ISerializableAttemptOwner. It no longer calls the generic provider inside a replaying unit of work. HumanAiToolProposalPreparation retains the exact registry kind, required resource/action, strictly validated payload, command object and conversation owner, tenant, actor, active status and initial concurrency stamp. Its cancellable GetAiProposalAuthorizationTargetByIdAsync read loads only the tenant-filtered Event entity, without navigation joins into private identity tables. Missing foreign heavy-moderation targets still use their dedicated preparation below.

Preparation captures principal, route, support and policy source exclusion while the original human execution owner is idle. Actual Event targets use IHumanEventAssignmentSourceFactory in the selected tenant. Targetless draft and template questions do not invent assignment snapshots. Resource IDs and PreCreateAuthorizationFacts, EventScopedAuthorizationFacts or TenantScopedAuthorizationFacts retain the original proposal policy meaning; model-supplied organization, group and management-context flags grant nothing. Cerbos receives BuildPrincipalFromSources with actual assignment enrichment, at the selected instance or tenant endpoint and original tenant policy scope. Local evaluation uses the canonical fallback permission mapping and Event authority projector, including authenticated draft creation, tenant template administration and actual Event assignment permissions. Instance administration never replaces a selected Cerbos denial.

The provider finishes before locks are acquired. Acquisition borrows the same owner's exclusion, principal, route, optional assignments and support leases; no external call occurs under those leases. Full source comparisons and principal reprojection run at acquisition and terminal commit, including assignment windows and the original credential expiry after blocking work. The retained graph must still contain the exact proposed-only action and original owner/tenant/actor/payload. The repository advances the conversation stamp when saving; the original stamp is checked before that authorized save. A rejected attempt rolls back without replaying the prepared decision. Ordinary authorization rejections retain tool_authorization_denied; a changed conversation retains conversation_authority_changed. Provider outages keep their dedicated exception path.

HumanAiToolProposalPreparationTests contains credential-backed Local/Cerbos proposal, denial, source-revocation, request-replacement, reuse, expiry and retained-graph controls. These tests are authored but unrun in this source-only unit; the unchanged MCP assertions and serial programme assurance remain the execution gates. No additional action kind can be confirmed or executed, and no target Event is mutated or dispatched. See the operator guide.

Cross-tenant AI heavy-moderation proposals

ProposeAiToolActionCommandHandler retains ordinary filtered event reads. Only the registry's HeavyModerateEvent / Event / ModerateHeavy question with a missing ordinary target enters IHumanAiModerationProposalPreparation. The conversation's selected tenant S, owner H, actor, stamp, active status and exact command are retained. A current persisted platform-administrator grant and the original authenticated credential are required before discovering the exact event's tenant T. Tenant-admin roles, machine principals and model-provided management-context flags do not provide this authority.

HumanAiModerationProposalTargetSource never accepts T as an argument. Its PostgreSQL observe/acquire routines resolve one event identifier and read its actual assignment contributors under S's unchanged Runtime connection. The dedicated NOLOGIN owner has column-limited source access; Runtime receives only routine execution. Migrator installation is composed after the existing human assignment source, whose writer epoch also excludes negative-set changes. Existing selected-tenant assignment routines keep their equality guard. Exact routine bodies, ACLs, enrollment, source metadata and policies are attested. Separate current-catalog checks prevent an older Serializable snapshot from hiding native drift; no private data is read on that metadata connection.

Provider selection and policy scope remain S's, including S's BYO route. T does not select a different endpoint. The resource remains the existing EventScopedAuthorizationFacts(T, eventId), and Cerbos receives truthful T/event assignments. A selected provider denial or outage rejects the proposal. The prepared branch uses one ISerializableAttemptOwner, not replaying IUnitOfWork.ExecuteSerializableAsync. It acquires the existing policy exclusion, principal, route, target assignments and support sources before graph mutation. The terminal finalizer checks those original inputs, credential lifetime, authentic request references and the retained proposed-only graph before commit. The target event is neither mutated nor dispatched, and confirmation remains create-event-draft-only.

HumanAiModerationProposalTargetNativeTests and HumanAiModerationProposalPreparationTests contain focused native and credential-backed controls. They are authored but unrun pending serial programme assurance. See the operator guide.

AI retention transaction authority

Actor selection now uses IHumanActorContextReader before graph mutation. HumanActorContextReader explicitly prepares the canonical human and authentic credential lifetime, observes current principal contributors, then holds the policy source-set fence followed by the principal fence in one non-replaying Serializable attempt. Terminal admission checks the original request, account, tenant, credential lifetime and source contributors before releasing results. This is a self-read, not an administrator policy question. IAiActorContextRepository requires that held admission and returns detached Actor entities containing only display and owner names. PostgreSQL uses the execute-only ai_actor_context_read capability with column-limited NOLOGIN ownership; Runtime and Global receive no direct source-table access. Both native and portable predicates require live personal ownership or live approved organization/group placement, membership and an actual active event.create mapping in the correct role scope. A result exceeding 1,000 actors fails closed. Five authentic request controls pass: repeated detached reads, foreign-subject/ tenant rejection, committed membership revocation and both credential-expiry boundaries. Native display projection also passes. Full eligibility, drift and graph mutation assurance remains underway; these results do not establish successful conversation creation.

Ordinary AI graph writes require their exact nonbypassed Runtime tenant and a caller-owned Serializable transaction. AiConversationRepository fences the current actor anchors, owner user, tenant/status and root, then loads a fresh tracked graph. Admitted organization/group actors also fence the actual tenant link, membership, role, permission and composite RolePermission rows and recheck EventCreate, matching AiAssistantActorContextService; owner-only cancellation/rejection does not acquire a new universal membership requirement. The tracked root must retain its persisted ownership, and every child must retain its exact tenant/parent. EF persists both existing-child edits and new children, advances root concurrency even for child-only changes, and keeps unrelated tracking rather than replaying historical states or clearing the whole context.

Worker privacy checks use IAiConversationRepository.IsPrivacyFencedAsync on the freshly acquired tracked conversation, before provider handoff and again before settlement. The repository retains its original subject, actor, tenant, connection and Serializable transaction across the read. PostgreSQL reuses the native AI owner contract with ai_conversation_owner_privacy_fence(uuid,uuid), returning only whether a local saga exists. Its definer receives only SELECT(subject_id) on that source; Runtime gets execution, not saga or receipt access. Missing owner or drifted native authority rejects processing rather than treating the fence as absent. Installer body/role/ACL/source attestation covers both routines and rejects stale installations. The twelve native fence controls are now covered by the eleven passing original-owner/drift controls in AiPrivacy-CurrentInstallationAndOriginalOwner.trx and the corrected two-case present/absent check in AiPrivacy-InstalledSchemaOriginalSubject.trx (one overlapping absent case). The first run's present-case setup failure was a canonical migration model used against a custom execution schema, not a fence result; the corrected setup appends the genuine authority fact without granting Runtime access. Broader graph-owner assurance remains pending.

Installation metadata is checked before and after the Boolean read through a fresh non-enlisted Runtime connector. A graph owner's Serializable catalog snapshot can otherwise retain pre-DDL function bodies, ACLs and role flags. These checks read catalog metadata only; private fence data remains on the original graph transaction and connector, whose identity is checked again before the result is released. Late function-body, ACL and role drift are rejected in the verified native controls; signed HTTP and real worker checks also pass in AiApi-CurrentCatalogAndOriginalWorker.trx.

PostgreSQL owner fencing uses PostgresAiConversationOwnerFenceContract: a column-limited NOLOGIN owner holds those sources and returns only eligibility. This data fence serves human graph mutations and worker settlement; it does not issue human action admission. ExploreDbContext.TrackedAuthority consumes the repository's retained native owner keys only for the exact conversation graph's User, Actor and Tenant foreign keys on the original tenant, connection and physical transaction. Other foreign keys and persisted tenant checks still use their existing validation. Changed keys or transaction replacement cannot reuse that evidence. Runtime receives no private source-table grants. AiEventDraftRecoveryRepository passes deployment enrollment when constructing the graph repository and adds the native organizer-eligible placement predicate for event creation. That predicate only narrows owner eligibility; it is not an authorization claim. Portable recovery retains its existing placement checks.

Create, send, cancel, proposal and rejection handlers own their local mutation transactions. Model-catalog and primary tool admission precede Runtime user fences. Sending serializes user/tenant quota checks, reloads after stale recovery and commits the message/run and database idempotency receipt together. The controller's later in-memory queue enqueue remains a separate delivery boundary. Processing claims the queued run in one transaction, calls reference/model providers without a business transaction, then freshly settles the same eligible in-progress run in another. A late failure cannot replace a cancelled outcome.

Transcript hygiene owns a separate privacy-purpose transaction that remains available after ordinary owner authority is revoked, but permits only existing message-content/reference-summary redaction. Confirmation commits one local claim before independent storage/native command dispatch and commits the action outcome plus execution record together afterward. Already claimed actions cannot redispatch. These local boundaries do not make independent external/native commits atomic: Phase24/Phase27 still own exact payload/version/actor/expiry binding for every family, primary admission before image effects and unknown cross-commit recovery. Twenty native graph/action cases are authored and unrun; no end-to-end confirmation or runtime certification is claimed.

AiRetentionCleanupJob reaches AiRetentionCleanupService, which creates a fresh Runtime tenant scope and invokes the native cleanup handler. That handler holds the retention-policy setting lease, reads current system/tenant rows with the instance lock taking precedence, and owns one Serializable transaction for the six graph-redaction updates. The repository requires that exact nonbypassed tenant transaction, fences selected conversation roots, and rechecks their current age before redaction. Dry-run remains read-only. Partial child redaction cannot survive a later failure. Seven native cases are authored and unrun; aggregate graph updates and proposed-action execution remain separate caller-owner work.

Custom-property projection maintenance authority

Tenant-wide rebuild, dirty-scope drain and single-event/session rebuild handlers own their Serializable transaction. They require the exact current tenant and canonical authenticated user, evaluate the selected provider, then fence current instance or tenant administration before calling an updater. Posted tenant IDs cannot redirect maintenance into another tenant. The single-resource requests continue to resolve their tenant from the stored event/session through the authorization resolver.

The two updaters no longer create or commit a separate rebuild/drain transaction. Their maintenance entrypoints require the caller's exact nonbypassed tenant lease and Serializable transaction; dirty-row marking repeats that boundary and an explicit tenant predicate. Projection rows, dirty markers and status changes therefore roll back with the caller's authority transaction. Existing native tests now create fresh exact-tenant maintenance contexts rather than relying on their privileged seed context. New native handler and rollback tests are authored, not executed.

Template mutation authority

Event-template creation uses the server-selected execution tenant. Session-template creation resolves its persisted event-template parent; update/deletion resolve the selected template to its persisted execution tenant before evaluating the declared tenant-update policy. A session template must retain a visible same-tenant parent. The handlers invoke the selected provider themselves, then use a Serializable transaction for the target reread and all child/root writes.

ITenantAdministratorCommitAuthority exposes the existing tenant-administrator identity, active membership and grant fences; its implementation reuses the native owner checks in ExternalApiKeyIssuanceAuthority. Instance administrators use the existing instance-administrator port instead. Neither port grants new database privileges. Current authority is reacquired on transaction retry, and cache invalidation follows commit.

Update repositories require that exact-tenant Serializable transaction, fence the template row and, for a session template, its visible same-tenant parent. A fresh persisted snapshot is compared with tracked scalar values before the patch is constructed. Audit timestamps are taken from storage, avoiding false mismatches caused by database timestamp precision; caller-supplied tracked business fields cannot become an unsubmitted patch. The owning handlers retain the expected concurrency stamp and existing validation/quota rules. Quota reads use the same scoped context and now propagate cancellation to their existing setting readers.

Creation snapshots the mutable outer DTO, validates its input shape, and allocates root/definition/option UUIDv7 ids plus creation time before transaction replay. Each attempt builds fresh entity graphs using that identity plan and the captured execution tenant. It reacquires current administrator authority; session-template creation also fences its parent before insertion. Quota, uniqueness and governance checks and every write stay in the transaction; cache invalidation follows commit.

Definition replacement also preallocates its new child identities and creation time outside replay. Both repositories bind definitions to the actual root and its tenant, then options to their persisted definition, before inserting them.

Six authored SQLite cases inject one pre-commit busy failure into the real transaction pipeline. They check stable root/child/default-option identities and creation timestamps for creation and replacement, and reject creation replay after a real administrator-grant removal. They do not substitute authority services or claim engine-contention coverage. These and the other native cases are unrun; source completion does not close the wider Phase 21 caller inventory.

Custom-property definition mutation authority

All sixteen shared/event/session definition and value write handlers invoke their declared tenant-update provider policy, then reacquire native current administration inside Serializable execution. Creation holds the actual parent; update/delete/value readers hold the definition, parent and runtime option rows. Tracked scalar comparisons reject unsubmitted tenant, parent or option changes. Database-owned audit timestamps are normalized from storage.

Updates retain concurrency-stamp, quota and governance checks. Definition, option and value ids/time are reserved before retries, with fresh graphs per attempt. Value validation rereads options and existing values under the fences, preserving duplicate-value rejection. Existing value writes compare persisted ownership, and replacements bind every row to the command's exact parent/scope. Purge explicitly fences its target before dependency reads, then commits deletion and a freshly constructed audit together. Its audit id/time remains stable on retry. Projection writes and cleanup remain atomic; metrics/cache follow commit.

Fifty-eight actual-handler SQLite authority cases are authored, plus seventeen conversion/quota/value-validation regressions migrated from fake-store fixtures to native storage. None has run. This family source packet does not close the remaining Phase 21 caller inventory or provider assurance.

Subscription state reads

ActorSubscriptionRepository self-service state/detail/list reads require the current nonbypassed tenant lease. They retain soft-delete filtering and use no-tracking rows, with persisted equality between the subscription recipient and its tenant membership's user. Deleted members/users and inconsistent recipient links cannot supply state or become fanout recipients.

The state lookup returns scalar entities for explicit mutation, not an identity-map instance or a graph containing mutable actor PII. Unsubscribe rejects a live administrative Blocked state, preventing unsubscribe/resubscribe from clearing the block. A retired row remains retired; the existing filtered unique index still permits a new subscription after retirement. Missing state retains the existing idempotent unsubscribe result; BaseCommandResponse permits its empty id and no mutation occurs.

This source packet does not close the subscription family's transaction owner. Commit-bound membership/target checks and the legacy publication fanout's persisted outbox-to-tenant binding remain open in Phase 21. Authored native cases have not been executed.

Event resource authority

RuntimeAuthorizationProvider partitions islamuevent_event_resource checks before generic cached provider selection, safe mode, or administrator shortcuts. EventResourceCapabilityAuthorizer binds the tenant, authenticated subject and machine status from server contexts. EventResourceTargetAuthorizationFacts contains identifiers only: mismatched identifiers or generic authority facts cannot supply permissions. Management decorators and HAL capability batches enter this boundary; audience disclosure and content transport retain their handler-owned response and preparation protocol.

EventResourceAuthorityOrchestrator owns separate serializable reads A and B. Repositories return fresh entities; Application freezes facts and evaluates the Domain policy using one UTC instant per read. The resource provider consumes only that frozen projection and its DomainAllowed ceiling. It performs no secondary membership lookup, clock sampling, secret resolution or grant caching. Native batches share category reads and grouped provider calls rather than repeating the single-resource decision for each link.

Provider I/O and private content preparation occur after A commits and before B starts. B compares the route, complete provider input, resource version, storage identity and attachment generation. A change permits at most one complete retry, under the original request cancellation/deadline. Final time checks reject grants that expire before capability return or response headers. Mutation handlers recheck inside their transaction; every execution-strategy replay samples fresh authority time rather than reusing the mutation's stable data timestamp.

B's start, not its completion or a later commit, is the revocation boundary. A concurrent revocation may overlap delivery. These decisions are not reusable capabilities. Provider deactivation and failures remain non-enumerating for restricted EligibleOnly reads. Generic Cerbos and local provider entry points reject this resource kind; only its frozen adapter may evaluate it.

Resource moderation has a paired parent prerequisite. Application freezes native platform/tenant administrator eligibility, parent event attributes, all native non-clock principal maps and permission lists, and timed event assignments. Local evaluation requires the exact native scoped administrator gate. Remote evaluation additionally requires a complete parent-event response permitting moderate-light or moderate-heavy, and a resource-policy allow for moderate. Neither decision grants content, export or republish authority.

Parent checks use native UsePolicyScope semantics and the default event policy version, not the resource binding's scope/version. They share its endpoint and deployment fence. Frozen collection equality is structural; assignment windows are projected again at the final clock gate. The adapter neither enriches a principal nor emits the legacy raw nowUtc field.

Resource-policy deployment activation

Remote resource authority additionally requires an Active deployment fence. EventResourceProviderActivation is global to an explicitly bound deployment UUIDv7, not scoped to a tenant alias. Its epoch is a local concurrency fence, not a remote policy-store revision.

The instance-owned cerbos.resource_deployment_bindings document binds normalized gRPC aliases to deployment identity, explicit scope and policy version. Empty scope means Cerbos root policy. Aliases cannot be removed or reassigned. Binding changes and announced policy writers start a new operation and close authority before Admin mutation. Generic settings writes and portable manifests cannot replace this coordinated document.

EventResourceProviderControlPlane retains the actual Bind, Begin and Activate commands through EventResourceProviderOriginal. The binding-key ordered lock is acquired before the existing single, non-replaying Serializable attempt. This is ordinary Global Instance authority, never MixedSettings authority. HumanSettingsMutationPreparation.EventResourceProvider acquires the original subject/principal, authorization route, support and credential horizon before EventResourceProviderOriginalStore privately compiles the complete binding and activation row effects. Row metadata, explicit absence, the full binding document and publication resolver inputs are retained on the same physical connection.

The store writes only that pair, with the original actor and one represented timestamp, operation ID, document revision and concurrency stamp. It does not flush unrelated tracked entities through SaveChanges. A stale activation is a real no-op and must still pass private action completion; omitted effects cannot borrow that result. Final source checks precede the owner's final horizon sample, and every exit expires the original. PostgresEventResourceProviderContract provides the enrolled Global execute-only backend, independent native effect comparison, compulsory source revisions and exclusion (including absence and stale Serializable snapshots), row-effect witnesses, deferred completion and strict role/routine/privilege/trigger attestation. The production database migrator installs it; Runtime never repairs it. Generic ordinary-setting definitions and the current-transaction upsert both reject the binding key. Only current instance administrators may bind, begin or activate. Activation requires the matching operation and epoch, matching scope/version, all previous writers stopped, a positive reachable-replica count equal to the count with the declared policy, and FrozenParentPolicyContractConfirmed. The latter is an explicit operator attestation covering the parent event policies and their derived-role dependencies, including migration away from raw-clock predicates to the frozen derived-time contract. It is not inferred from the replica counts. Upload success, elapsed time, failure or cancellation cannot activate a deployment. Recovery starts a new operation and attests convergence. All three write actions suppress generic idempotency response storage and replay; a cached receipt must not bypass a revoked administrator or a newer operation.

Publication retains the actual SyncAuthorizationPolicyPackageCommand through IPolicyPackageService and the same original producer. The package service issues and rechecks its own selected target from captured system/tenant resolver rows, including absences, deployment provider options, gRPC endpoint and validated Admin origins. It does not use a cached runtime resolver as write authority. The closure owner must acknowledge commit and release the ordered lock before schemas, policies or reload requests can be sent. Rejection and unknown settlement produce no remote permission; later remote failure never reactivates the deployment. Inspect-only remains observation. Setup-secret publication uses its genuine setup-ticket and pending-generation authority, rejects a bound deployment before remote writes, and preserves validated unbound publication without manufacturing a human activation proof. The submitted one-time Admin credential pair is never serialized into the original source or native authority payload.

This protocol depends on truthful endpoint associations and participation by every remote writer. It cannot detect unannounced external policy edits or infer fleet convergence from an Admin response. See the public operator procedure.

3.4. HATEOAS Link Authorization

The API uses a Hypermedia as the Engine of Application State (HATEOAS) model. HAL _links are the browser/client source of truth for action availability; Blazor and other clients must not recreate action gates from roles, claims, or cached local state.

Visitor participation adds a capability condition, not a new permission or an administrator bypass. VisitorAccessCapabilityResolver combines effective visitor mode with all enabled, tenant-usable account providers. Local credentials do not provide public signup. Configurable external providers contribute onboarding only with an explicit Allowed policy and a trusted configured HTTPS destination; unknown policy or a successful discovery probe cannot substitute.

Public settings, shell, provider configuration and event-detail responses project the same immutable visitorAccess facts. Provider HAL exposes signup:<provider> destinations separately from operator login. Event start and AccountRequired publication candidates consume those facts alongside existing permissions. Unavailable facts omit candidate actions. Metadata does not authorize a write: event and provider/settings mutations recheck current capability inside their shared lease-owned transaction. Listing, walk-in, external registration and existing registration access retain their own authority.

  • Mechanism: HateoasAuthorizationEvaluator is used by resource assemblers and manual sync controllers before links are materialized.
  • Behavior: It evaluates the permissions required to execute each potential link. If the current user is not authorized, the link is omitted from the response. Permission-bound links fail closed when authorization evaluation fails; non-permission navigation links may remain when they only require authentication or static conditions.
  • Metadata: Link permission metadata includes resource kind, resource id, action, optional AuthorizationScope, and resource attributes. Descriptor-based links propagate scope and attributes from ResourceDescriptors; API-only links use explicit AuthorizationActions + ResourceKinds constants.
  • Batching & Performance: To avoid $N+1$ performance issues, the evaluator implements a 4-Phase Capability Planning Pipeline (Candidate → Normalize → Batch Decision → Materialize). Deduplication includes resource kind/id/action, scope, and normalized attributes so scoped or attribute-sensitive links do not collapse into the wrong decision.
  • Provider Optimizations:
    • Cerbos: Uses the official gRPC SDK to send deduplicated checks in a single batch request (CheckResourcesAsync).
    • Fallback (Local): Resolves the user's Authority Profile (admin status, tenant membership) once for profiled batch checks. Organization evidence actions (submit-evidence, view-evidence, review-evidence) instead reuse the single-request evaluator with the original immutable facts and scope: OrgAdmin can submit/view, TenantAdmin can review/view, and combined authority permits both. Batch size and neighboring capabilities cannot confer submission on a tenant-only reviewer or review on an organization-only submitter. Other organization actions retain their existing batch evaluation.
  • Collection Support: For "Get All" endpoints, all link definitions for all items in the paginated result are flattened into a single massive batch, ensuring high-scale efficiency.

Committed administrator-authority freshness

AdminContext reads platform roles, tenant grants, organization memberships, and group memberships from their existing no-tracking repositories on every authority call. It does not cache positive or negative authority results across calls or requests. Local authorization and Cerbos principal construction therefore observe committed role changes without mutation hooks, cache invalidation, or revised claims. The optimized local batch still resolves one immutable AuthorityProfile for its checks; small batches, machine callers, and evidence actions retain their existing paths.

The guarantee applies when a new request's database read/snapshot starts after the mutation commits and every node reads the same authoritative primary database. It also covers out-of-process writes. An already-running read, batch, or older transaction snapshot may retain its earlier view; this does not promise retroactive cancellation or solve authorization/write races. Rollback creates no reusable cached grant or denial. Existing role predicates, tenant filters, and provider-identity binding resolution remain authoritative; missing provider bindings are re-read after same-scope onboarding. BFF display claims, permission-registry caching, and unrelated content caches are unchanged.

The obsolete IAdminCacheInvalidator contract and diagnostic POST /api/_internal/admin-cache/users/{userId}/invalidate action are removed. POST /api/_internal/admin-cache/current-user/snapshot remains available under its existing Development/Testing configuration gate; it diagnoses identity, not cached authority.

Registration-form authoring uses the scoped islamuevent_registration_form resource with view, create, update, delete, preflight, publish, and manage-requirements actions. Its trusted resource context is enriched from the persisted parent Event; request bodies cannot author tenant or organizer identity. The event-level manage-registration-workflow entry relation and all form-level actions share the same authority: a verified organizer controller or exact tenant/event event.registration_manager assignment carrying event_registration:manage. Contributors, listing submitters, tenant-only curators, instance administrators, machine principals, missing/ambiguous organizer state, and unrelated tenant/event assignments fail closed in both Cerbos and fallback authorization.

Paid-event commerce is an exact event authority, not administrative fallback authority. manage-paid-event-commerce is evaluated with the persisted event and organizer actor context for payment-connection, hosted onboarding, commercial disclosures, and paid publication. A current user must control that exact organizer actor in the ambient tenant; instance and tenant administrators, unrelated actor controllers, machines, historical recipients, and ambiguous organizer state do not substitute. The same decision controls the corresponding payment-connection, start-onboarding, commercial-disclosures, and paid publish HAL relations.

HateoasAuthorizationEvaluator performs that enrichment through one bounded persisted-Event lookup per batch, applies the ambient tenant filter, removes caller-supplied authority attributes, rebuilds trusted actor/organizer context, and denies before Cerbos or fallback evaluation for missing, cross-tenant, missing-ID, or over-bound inputs. The resulting HAL relation set—not local claims or role checks—is the client action boundary.

3.5. Address Governance And Tenant Approval

Address settings and actor authorization answer different questions. The hierarchical creation mode and organization grant determine whether a class of manual-address creation is enabled; they never grant the current caller authority. Named provider decisions then authorize the trusted current actor. Neither settings nor browser-visible claims may substitute for the other boundary.

Tenant-wide address reuse requires approve_tenant_address on the exact islamuevent_location. The promotion handler derives tenant and subject from ITenantContext and ICurrentUserService, supplies TenantScopedAuthorizationFacts, and accepts no tenant, actor, source, visibility, organization, provenance, or authority field from the command body. Instance and in-scope tenant administrators have this action. Organization-level create_custom_address authority does not imply approval, and regular authenticated users remain denied. Provider denial or failure is fail-closed.

The handler also rechecks persisted tenant, active PII, erasure, and Private Home state before mutation. An authorized same-target retry is a successful no-op. Future HTTP and UI exposure must use the normal authenticated write boundary and a HAL affordance backed by this same named action; clients must not infer it from roles.

4. Authorization Providers

Signed Publication Receipt Verification Primitive

CerbosPublicationReceiptVerifier verifies an operator statement over exact JSON bytes using RSA-PSS with SHA-256 and an independently configured public key of at least 3072 bits. The private signing key is not an Event input. Callers supply the expected revision, content hash, environment, PDP endpoint, and observation time; receipt fields never define their own expected deployment authority.

Verification rejects unsuccessful publication, future publication, expiry, a lifetime exceeding 24 hours, duplicate or unknown JSON properties, missing required constructor fields, and malformed signatures or keys. Documents are limited to 8192 bytes, signatures to 1024 bytes, and PEM public keys to 16384 characters. The result is a boolean without raw provider diagnostics. A valid signature proves the signed statement, not the live PDP state. This primitive does not itself wire startup readiness or replace the existing provider-administration callers.

4.1. Cerbos

  • Description: A powerful, open-source, stateless authorization service that allows policies to be defined in human-readable YAML files.
  • Layering: Application owns provider-neutral catalogs and checks (AuthorizationActions, ResourceKinds, AuthorizationCheck, ResourceDescriptors). Infrastructure owns Cerbos gRPC, Admin API, ZIP package export, client caching, and package publishing details.
  • Usage: When configured, the CerbosAuthorizationService translates the application's authorization request into a Cerbos CheckResources API call. Cerbos policy resource kinds are namespaced, for example islamuevent_custom_property_template and islamuevent_custom_property_projection.
  • Scoped checks: Resource descriptors always send tenant context as resource attributes. The shared instance PDP does not send a Cerbos resource scope by default, so bundled root policies work without tenant-scoped policy files. Set Cerbos:UsePolicyScope=true only when the target PDP has a complete scoped-policy chain and engine.lenientScopeSearch=true.
  • BYO (Bring Your Own) Cerbos: The platform supports a multi-tenant model where each tenant can optionally provide their own Cerbos PDP and Admin API configuration.

Event-session creation uses the islamuevent_event_session Cerbos resource even before a session row exists. The create check authorizes against the parent event id with tenantId, eventId, and authorizationPhase=pre_create attributes. Instance admins can always create sessions; tenant admins can create sessions within their tenant; event owners/managers can create sessions for their assigned event; authenticated users remain view-only. In the shared instance-provider path, pre-create session checks are evaluated through the local parity provider even when they appear inside mixed HATEOAS batches, while tenant BYO Cerbos remains authoritative when configured.

Event moderation uses explicit event actions rather than edit authority. moderate-light, moderate-heavy, and unmoderate are granted to instance administrators and tenant administrators in scope while update/delete remain denied to those admin roles unless a separate edit policy grants them. Organization administrators, owners, and event-role members can receive normal management/edit affordances for events they control, but they do not inherit admin moderation actions from that relationship. The active HAL/API surface emits moderate-light, moderate-heavy, and eligible unmoderate affordances independently from edit authority. Heavy redaction is irreversible and is advertised only when the backend can redact event-owned content, detach and delete event images through provider-backed retryable storage deletion, and send generic attendee notifications. Unmoderation is advertised only when the latest moderation record is reversible light moderation.

Global Actor and exact ATProto identity moderation use a separate instance-setting boundary. Both commands authorize AuthorizationActions.InstanceSettings.Update on ResourceKinds.InstanceSetting with resource id global-actor-moderation, then recheck the authenticated operator with IAdminContext.IsInstanceAdminAsync before loading the target. Tenant administrators have no authority over global Actor or identity state. Tenant moderation remains limited to TenantUser, OrganizationTenant, GroupTenant, and tenant federation or import policy. Event moderation remains content-local.

Moderated-event reads also use explicit management authorization. Public event detail and public discovery fail closed for moderated events. Authorized management callers use view-management through GET /api/event/{id}/management-detail, GET /api/event/management/by-actor/{actorId}, and GET /api/event/{id}/moderation/history. The moderation-history route returns safe audit metadata only; it must not expose original event text, slugs, URLs, image identifiers, storage object keys/paths, or raw provider errors.

Public Event eligibility is evaluated independently from management authorization. Anonymous event, program, session, group, assignment, and agenda reads inherit the parent Event eligibility gate. An ineligible Event remains available through authorized view-management reads, but its HAL representation uses the management self link and omits public report, claim, participation, external-action, and other public affordances. Clients must not infer public eligibility from management access.

Event reporting keeps reporter and moderator authority separate. GET /api/event-reports/events/{eventId}/options remains anonymous for published-event reportability discovery, while POST /api/event-reports, GET /api/event-reports/my, and GET /api/event-reports/my/{reportId} require the authenticated current user and return only reporter-owned, limited status metadata. Moderator queue/detail reads and triage/assign/decide/execute commands authorize against the parent islamuevent_event resource with the explicit event moderation action, then the handlers recheck tenant, report, event, case, assignment, and expected case concurrency stamp before mutating state. Moderation read projections are intentionally data-minimized: they do not expose stable reporter user/actor identifiers, evidence creator identifiers, decision moderator identifiers, raw provider case/signal identifiers, provider URLs, or provider correlation identifiers. UI affordances must come from HAL relations: report-event, moderation-reports, triage-report, assign-report, decide-report, and execute-report-decision.

Event provenance separates listing contribution from organizer authority. A submitter or importer does not gain registration, attendee-data, ticketing, or commercial authority; those capabilities require an assigned organizer actor and the relevant event authorization. Public-action management authorizes against islamuevent_event, while every event-bound organizer-claim list/detail/submit/withdraw/review check uses islamuevent_event_organizer_claim with parent-event metadata preserved server-side. Withdrawal uses the dedicated withdraw-organizer-claim action. Before provider evaluation, the authorization behavior loads the persisted claim and claimant actor, then supplies claimant user, organization, or group ownership as server-only resource attributes; route/body ownership is never trusted and the attributes are excluded from public JSON/OpenAPI. Only an authenticated non-machine principal controlling that claimant actor may withdraw: personal actors require the same user, while organization/group actors require PermissionCodes.EventCreate in that exact organization/group. Separate Cerbos principal attributes carry those permission-derived IDs without broadening admin memberships or derived roles. Unrelated users, curators without claimant control, and instance administrators are denied. Event policy does not accept claim actions, and both providers deny unsupported future registration, ticket, and attendee actions until their owning phases implement them. Authenticated humans may submit claims only for claimant actors they control in the current tenant; the actor must be globally active, with an active tenant user or approved organizer-eligible organization/group participation. Withdrawal intentionally checks persisted claimant ownership rather than current tenant eligibility, so a legitimate controller can revoke an active claim after the claimant becomes ineligible; approval still revalidates tenant eligibility inside the decision transaction. Machine principals and unsupported claim actions fail closed. Approval assigns Event.OrganizerActorId transactionally but never grants access to historical attendee data. HAL relations remain the client authority for claim-event, source/action links, correction suggestions, unsafe-link reports, withdrawal, and review.

Event ticket management is implemented through AuthorizationActions.Events.ManageTickets on the parent Event. The event HAL may expose manage-ticket-types and manage-capacity-pools independently. The ticket catalog HAL then applies the same parent-event permission metadata to create-draft, clone-draft, create-type, create-pool, publish, and item-level edit or delete. Community contributors and callers without exact ticket authority fail closed. Clients must not turn the two event relations into broader local authority.

Platform monetization uses ResourceKinds.InstanceSetting with setting key platform-monetization and InstanceSettings.View or InstanceSettings.Update. Endpoint classification, operation authorization metadata, and HAL filtering provide the normal server boundary. The query and command handlers add defense in depth by calling IAdminContext.IsInstanceAdminAsync before repository access. Tenant administrators, organizers, curators, and regular users cannot read or update this management resource, and the Blazor save controls appear only when the HAL document contains edit.

Paid-event authority follows ADR-022 through ADR-024 and is not implied by administration. AuthorizationActions.Events.ManagePaidEventCommerce (manage-paid-event-commerce) authorizes against ResourceKinds.Event using trusted persisted Event organizer attributes only: exactly one of organizerUserId, organizerOrganizationId, or organizerGroupId must accompany organizerActorId. The action allows only authenticated human principals who directly control that organizer actor: same authoritative userId for user organizers, or event:manage-finance in the exact organizer organization/group via eventFinanceOrganizations or eventFinanceGroups. Listing contributors, event owners/managers/ticket-role assignees, tenant administrators, instance administrators, machine callers, missing organizer context, and ambiguous organizer context receive no fallback merchant authority.

Phase 17 promotion list/detail/create/revise/publish/revoke/code-rotate requests use that same exact event decision. Promotion HAL carries persisted tenant, Event, actor, and organizer attributes as server-only metadata. The collection emits create-promotion; a draft emits publish; a published definition emits revise-promotion, revoke, and rotate-promotion-code, always only after the permission decision and lifecycle-state gate both pass. A platform-managed participation configuration and exact Event/catalog lineage are still rechecked by handlers; authorization alone cannot make an external-managed or cross-catalog promotion valid.

Order redemption is a separate purchaser boundary. Authenticated apply/remove first proves the current account owns the tenant/Event/order tuple, then delegates to the shared redemption command. Guest apply/remove requires the existing opaque X-Registration-Order-Capability for that exact tuple. Registration-order HAL emits only apply-promotion or remove-promotion while READY_FOR_CHECKOUT; authenticated links also require the registration-order continue decision. These links never grant organizer promotion-management authority, and organizer-management links never grant purchaser order access. Invalid authorization, capability, code, scope, or availability is deliberately mapped to the same non-enumerating 404 at the redemption API boundary.

Payment start/status/retry preserves the purchaser split: authenticated callers must own the exact tenant/Event/order tuple, while guests must present the existing opaque capability for that tuple before expiry. Missing, malformed, expired, wrong-tenant, wrong-event, wrong-order, and wrong-account authority collapse to the same not-found boundary. Studio payment status is separately authorized by manage-paid-event-commerce against the persisted Event organizer and does not widen registration-manager or administrator authority. HAL is the only action authority: start-payment, payment-status, checkout-redirect, retry-payment, and studio-payment-status are omitted whenever state or permission is uncertain. Refund, dispute, payment-attempt cancellation, ticket, check-in, transfer, waitlist, and add-on actions retain their later phase boundaries.

The BFF checkout-ticket endpoint does not create payment authority. It replays the same account or guest capability to the private API checkout-target read, binds the resulting one-time navigation to tenant, order, PathBase, host, and the dedicated checkout session, and exposes only the constant same-origin consume route. Provider destinations never enter HAL, browser-readable cookies, client state, or authorization decisions.

Managed reporting routing actions use settings resources rather than event resources. Tenant routing-state reads, tenant routing updates, tenant provider readiness tests, and tenant moderation-reporting dashboard reads authorize as AuthorizationActions.TenantSettings.View or .Update on ResourceKinds.TenantSetting with resource id <tenantId>:moderation-reporting and tenant scope. Instance reporting-provider lock updates authorize as AuthorizationActions.InstanceSettings.Update on ResourceKinds.InstanceSetting with resource id moderation-reporting-locks, then the handler rechecks instance-admin authority. UI affordances must come from HAL rels (routing-state, edit, test-osprey-provider, test-coop-provider); hidden links stay hidden and clients must not recreate them from role claims.

SingleTenant private management permits the existing control-plane detail and activation routes only for the fixed default tenant. The controller rejects arbitrary targets; the existing instance-setting authorization still decides who can read or activate. Machine principals, ordinary members and wrong-tenant administrators receive no new authority. Tenant identity/branding edits retain their separate exact tenant-admin authorization: the completed setup administrator holds both persisted platform and default-tenant grants. HAL is server-authored; the SingleTenant detail removes fleet-only links rather than exposing actions the mode forbids. No role/claim gating is added to Blazor.

Control-plane tenant fleet governance uses the instance-setting authorization boundary, not tenant membership authority. Fleet list and detail requests authorize with ResourceKinds.InstanceSetting and AuthorizationActions.InstanceSettings.View for resource id control-plane.tenants; activate, suspend, archive, reactivate, and schedule-purge commands use the same resource with AuthorizationActions.InstanceSettings.Update. Bundled Cerbos policy for islamuevent_instance_setting and the local fallback both allow these checks only for instance administrators, so regular users and tenant administrators are denied direct API reads as well as lifecycle writes. Tenant HAL resources evaluate the same kind, id, and action metadata before emitting lifecycle links; clients must use _links as the action source of truth and must not reconstruct controls from roles or claims.

Moderation provider callbacks are machine-authenticated, not browser-user authenticated. POST /api/integrations/moderation/osprey/callback uses the ModerationIntegration.OspreyCallback policy and POST /api/integrations/moderation/coop/callback uses the ModerationIntegration.CoopCallback policy. Both require authenticated API-key principals whose scopes pass MachineScopeMapping for event moderation authority; the Coop endpoint also verifies the configured HMAC-SHA256 webhook signature before retaining callback bytes and a unique effect pointer. Deferred execution runs under a tenant-bound internal principal limited to webhook:process-incoming. Effect status requires webhook:view-delivery; operator redrive requires the distinct webhook:redrive-incoming action, an authenticated actor, current processing generation, and a replayable retained callback. HAL exposes redrive only for dead-lettered state.

Registration provider callbacks are public-ingestion endpoints where provider proof is the authentication boundary. POST /api/integrations/registration/{provider}/{bindingId}/callback must not disclose tenant existence: binding resolution, verifier failure, duplicate delivery, stale evidence, malformed evidence, and parked outcomes acknowledge with 202 Accepted. The intake controller does not authorize or mutate registration aggregates. A later fenced worker validates the protected receipt and executes under the Application registration-submission path.

Registration provider management is event- and tenant-scoped. Connection CRUD and approved-origin replacement authorize against tenant update authority because connections are tenant-owned secret-binding metadata. Binding, mapping, channel, health queue, manual import, retry/resolve, launch descriptor, and reconciliation routes authorize against the parent Event using manage-registration-channels or view-registration-provider-health. Event HAL emits manage-registration-channels and view-registration-provider-health; item/collection HAL emits provider-create, origins, mappings, publish, manual-import, poll, retry, resolve, and launch-descriptor. Studio and clients must render only those relations and must not infer actions from roles, provider names, capability codes, drift class, channel mode, or local status.

Outgoing webhook management uses the islamuevent_webhook resource kind. The namespaced catalog includes webhook:view, webhook:create, webhook:update, webhook:delete, webhook:rotate-secret, webhook:test, webhook:retry, webhook:view-delivery, webhook:view-payload, webhook:pause, webhook:resume, webhook:reconcile-publication, webhook:abandon-publication, webhook:bulk-replay, webhook:manage-provider, and webhook:open-provider-portal. The Svix App Portal route uses webhook:open-provider-portal, sensitive retained bytes use webhook:view-payload, and bulk replay preview/list/detail/schedule/cancel requests use webhook:bulk-replay through the operation authorization pipeline with tenant and operation attributes supplied by ISecureRequest. Bundled Cerbos policy grants instance admins all webhook actions and tenant admins webhook actions within their tenant scope. Organization admins remain limited to delegated organization-owned endpoint actions and cannot view payloads, reconcile/abandon provider uncertainty, manage provider configuration, or bulk replay. The dedicated incoming webhook worker can only use webhook:process-incoming; a tenant-admin machine needs admin:tenant for bulk replay. Local fallback and machine-scope mapping mirror those boundaries. HAL exposes replay scheduling/preview only on the authorized collection and cancellation only on an authorized queued operation, so clients must not infer these controls from roles or claims.

EmailDispatch admin status and tenant delivery controls use the islamuevent_email_dispatch resource kind. Status reads require view, tenant pause/resume requires manage_tenant, row parking requires park, row replay requires replay, terminal abandonment requires resolve, and an explicit Unknown delivered/not-delivered decision requires reconcile. The API controller remains [Authorize], but operation request metadata is the enforcement boundary and supplies tenant/outbox context through ISecureRequest. Bundled Cerbos and local fallback deny regular users and bound tenant admins to their tenant. Global drain pause/resume and SMTP rate override instead use islamuevent_instance_setting:view|update for email-dispatch.processor; only instance administrators receive those permissions. HAL uses the same split actions so clients gate every affordance from _links.

Tenant role grant management uses the islamuevent_tenant_user_role_grant resource kind. The action catalog is view, create, and delete. Read projections intentionally include tenant-local user and grant identity metadata, so they are administrative APIs: list/detail requests are authenticated, carry the resolved tenantId through ISecureRequest, and require action view on islamuevent_tenant_user_role_grant. Grant and revoke commands also carry the resolved tenant id for create and delete checks. Bundled Cerbos and local fallback both deny regular authenticated users and allow tenant admins only for their resolved tenant, with instance administrators retaining cross-tenant administrative authority.

Footer management writes use the existing tenant-resource update convention rather than a separate footer resource family. Link-group, link, reorder, and footer-settings commands are authenticated, carry the resolved tenant id from ITenantContext through ISecureRequest, and require [AuthorizeResource(ResourceKinds.Tenant, AuthorizationActions.Update)] before handlers mutate tenant footer configuration. This keeps Cerbos and local fallback aligned with tenant administration authority; regular authenticated users are denied, tenant admins are limited to their resolved tenant, and a future distinct Footer.Manage permission must add resource kind/action metadata, Cerbos policy/schema, local fallback, HAL metadata, and tests in one slice.

Support-access session management uses the islamuevent_support_access_session resource kind. The foundational action catalog is view, list, start, stop, view_audit, and force_stop. Runtime support-access authorization preserves actor identity, validates persisted sessions per request when the BFF/server forwards a trusted support-access session id, matches the target tenant, and gates write behavior by explicit write-mode policy rather than by tenant role grants or browser-visible claims. RuntimeAuthorizationProvider applies this boundary before routing to local RBAC, instance Cerbos, tenant BYO Cerbos, or HAL batch evaluation: inactive forwarded sessions deny tenant-scoped resources, read-only sessions deny mutation actions, and write sessions deny mismatched tenant resources. Boundary denials emit warning logs, bounded Explore.Business metrics, and trace tags/events so HAL affordance filtering and operation command authorization remain observable through the same provider path. The provider forwards only bounded support metadata such as session id, actor id, target tenant/user id, mode, and supportAccessAllowsWrites; ticket and reason text stay out of policy attributes. Local fallback and bundled Cerbos policy/schema files both understand this resource kind; new support-access actions must update both paths plus the HAL link policy so clients continue to gate affordances from _links.

User profile updates use the islamuevent_user resource with the target domain user id as the resource id. The static Cerbos policy mirrors local fallback RBAC: instance admins can manage all users, tenant admins can manage users in their tenant, and an authenticated human user can update only the resource whose id matches either the Cerbos principal id or principal.attr.userId. That userId attribute carries the internal domain user id so self-service account settings still work when the OIDC subject is an external provider identifier. The actorId user-resource attribute is optional because self-service account settings updates authorize before the actor/profile-picture context has to be loaded. In the shared instance-provider path, islamuevent_user:update checks are evaluated through local parity so a stale shared PDP package cannot block the authoritative self-service user handler; tenant BYO Cerbos remains authoritative when configured.

4.1.1 Cerbos Package Upload

Admin HTTP calls carry the server-selected target classification into separate CerbosByoAdminClient and CerbosInstanceAdminClient transports. BYO targets require HTTPS, a root-only origin, and exclusively public DNS answers; a mixed public/private answer is rejected before connection. Instance targets may use a private operator service, but only at an exactly configured scheme/host/port. Both use the vetted-address connector, reject cloud metadata destinations, and disable redirects so administrator credentials cannot follow a redirected request. Endpoint paths, user information, query strings, and fragments are not admitted. The former AllowInsecureByoAdminEndpoints and AllowPrivateByoAdminEndpoints options are removed; neither can bypass the BYO boundary.

ZIP export reopens each artifact through the bounded reader and compares both its byte length and SHA-256 with the completed manifest before creating its ZIP entry. It writes the verified byte array rather than reopening the path again. Replacement after manifest construction fails with PolicyPackageUnavailableException, including replacement by different bytes of the same length. No inconsistent archive is returned; export does not perform a provider publication.

Publication and status diagnostics retain artifact counts, typed HTTP status, and closed failure categories rather than exception objects/types, package identity, content hashes, provider endpoints, or private transport details. Refit failures that arrive as failed response objects follow the same diagnostic boundary as thrown failures. Business issue codes and unknown status remain authoritative; ordinary logs are not a substitute for publication-state evidence. Unexpected failure warnings do not imply that external side effects were rolled back.

The Event package compiler admits at most 256 policy/schema artifacts, 2 MiB per artifact, and 16 MiB of aggregate artifact bytes before provider handoff. Reads reject file growth beyond the observed length. Every artifact is parsed before manifest hashing using strict UTF-8 and JSON depth 32. Duplicate JSON members, $type, and merge keys are rejected. YAML is preflighted before deserialization: one document, depth 32, at most 65,536 parser events, scalar length at most 65,536 characters, and aggregate scalar text at most 2 MiB. Anchors, aliases, explicit tags, duplicate mapping keys, and merge keys are not admitted. Converted JSON must also fit the per-artifact byte budget. Existing packages using these YAML features must be rewritten as explicit bounded mappings before Event publication.

In-App Onboarding & Admin Sync (POST /api/instance/settings/authz-provider/sync)

Operators can synchronize the bundled policy package directly through the instance administration UI or API:

  • Foreground credentials only: Every publication requires a fresh complete administrator pair. Missing and partial input are rejected; there is no configuration, cache, tenant binding or selected-secret-authority fallback. Setup/admin UI fields are cleared after each attempt, on navigation and on component disposal. Complete credential strings are passed unchanged.
  • Read-only inspection: The same authorized POST accepts inspectOnly: true with a fresh pair. It reads current store identifiers and revision metadata without uploading or reloading. Ordinary package status reads do not acquire administrator credentials and report live-store status as unknown. A store's revision is not the package content hash.
  • Credential security boundary: One-time values are never written to SystemSetting, database rows, response bodies, logs, traces, or background jobs.
  • Timeout and additive upload: Admin HTTP API calls enforce a strict 10-second timeout. Policy and schema uploads are additive; they do not perform destructive deletions of existing policies.
  • Runtime isolation: Sync and revision status depend on the Cerbos Admin HTTP API, but runtime authorization evaluation depends solely on the gRPC PDP, ensuring PDP decision checks remain isolated from Admin API availability and latency.

Hosted CI/CD Publishing

Cerbos Policy Validation retains its required check identity and package compilation/tests. The PDP's Admin API supports one Basic-auth administrator, not a scoped non-administrator publisher. Consequently CI does not receive a reusable Cerbos administrator pair. Unattended publication is unavailable; renaming an administrator account does not make its permissions least-privileged.

deploy-coolify.yml calls _cerbos-publication.yml as an actual prerequisite. An authorized operator publishes the candidate package with fresh input, verifies the target installation and signs an exact UTF-8 JSON publication statement. The independently configured public key in the protected production environment authenticates that operator statement. The submitter cannot provide the verifier's trust root. Restrict the production environment to reviewed protected branches/tags and require independent approval before running candidate workflow code. Environment maintainers, permitted signers and the provider administrator remain trusted deployment authorities; this is not a provider-issued attestation.

CerbosPublicationReceiptVerifier accepts only RSA-PSS/SHA-256 signatures with an RSA key of at least 3072 bits. The exact JSON properties are SchemaVersion (cerbos-publication.v1), Revision, ContentHash, Environment, PdpEndpoint, PublishedAt, ExpiresAt, and Succeeded. Revision, package hash, environment and PDP endpoint must equal independently supplied deployment expectations. Only successful statements with PublishedAt <= now < ExpiresAt and a lifetime no longer than 24 hours pass. Duplicate/unknown JSON fields, missing fields, invalid signatures, private-key input and missing inputs fail closed. Never sign success after a failed or uncertain provider operation.

The package hash is the existing manifest hash, calculated from the ordered artifact identities, kinds, byte hashes and sizes. It is not a Git tree hash or a Cerbos live-store hash. .ci/scripts/verify-cerbos-publication.cs computes it from the checked-out candidate. The reusable workflow verifies the receipt before retaining it; deploy verifies it again after image builds and environment approval, and again before the UI trigger if the API rollout consumed part of its lifetime. Pushes have no receipt and cannot deploy. Use workflow dispatch with the exact receipt JSON and base64 detached signature after operator publication.

For an operator signing tool backed by the approved external signing authority:

openssl dgst -sha256 -sign "$CERBOS_PUBLICATION_SIGNING_KEY_FILE" \
  -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:digest \
  -out receipt.sig receipt.json
base64 -w0 receipt.sig

Do not reformat the signed JSON. Supply its exact bytes to workflow dispatch; the signing key never goes to Event, the BFF, GitHub Actions or an artifact. GitHub environment variables are CERBOS_PUBLICATION_PUBLIC_KEY_PEM and CERBOS_PUBLICATION_PDP_ENDPOINT. Runtime deployment receives the public key, base64 receipt/signature and independently owned revision/environment through Cerbos:PolicyPackage:Publication. Compose/Aspire map the corresponding CERBOS_PUBLICATION_* inputs. The receipt's PDP endpoint must exactly match Cerbos:GrpcEndpoint.

Startup now checks PDP health and signed historical readiness without publishing. Role create/update/delete retain permission-cache invalidation without republishing the static package. Historical readiness does not establish current store equality, custom-role parity or event-resource deployment activation. Resource control-plane fences remain independent; uncertain live-store metadata cannot become authorization. Before out-of-band ZIP/cerbosctl publication, close affected resource deployments through their existing control-plane procedure and drain old writers. Verify every reachable PDP alias before the separately authorized coordinated activation. Signing or accepting a publication statement never reopens that fence.

If expiry blocks the UI after an API deployment, that API deployment is not rolled back. Reconcile the actual component versions and issue a fresh same-candidate statement before continuing the rollout.

Recovery is operator-owned: repair or reinstall the same validated package, inspect the outcome, then issue a fresh statement and dispatch deployment again. A restart after expiry requires a fresh verified statement. Rotate the configured public key to revoke a signer, then replace receipts and restart affected hosts. Drain old application replicas before revoking the retired runtime administrator credentials; do not restore fallback bindings. This source cutover does not delete historical database bindings, backups or external credentials.

Coolify PDP Deployment

For a Coolify-managed Cerbos PDP, use CERBOS_COOLIFY.md. That runbook covers the pinned Docker image, PostgreSQL policy-store schema, Admin API password hash, Traefik gRPC h2c label, and direct cerbosctl upload from this repository's cerbos/policies/ folder.

Direct CLI Upload (cerbosctl)

When CI/CD publishing or Admin API sync is unavailable, operators can still push local policy and schema files with cerbosctl directly:

docker run --rm -it -v "/home/{user}/ISLAMU/Github/Event/cerbos/policies/_schemas:/schemas:ro" ghcr.io/cerbos/cerbosctl:0.53.0 --server={cerbos.example.com:443} --username={username} --password={password} put schema -R /schemas

docker run --rm -it -v "/home/{user}/ISLAMU/Github/Event/cerbos/policies:/policies:ro" ghcr.io/cerbos/cerbosctl:0.53.0 --server={cerbos.example.com:443} --username={username} --password={password} put policy -R /policies

If the password contains spaces, wrap it in quotes:

--password="password with spaces"

4.2. Fallback RBAC Service (FallbackAuthorizationService)

  • Description: A local, in-database implementation of Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC). It serves as the primary authorization engine when running without an external Cerbos PDP container (e.g., local development, single-tenant deployments, ATProto/PDS standalone nodes). It is not used as an automatic fallback when the instance-level Cerbos provider is selected and unavailable.
  • Class Architecture: The service is decomposed into four partial classes in src/Explore.Infrastructure/Services/:
    • FallbackAuthorizationService.cs: Entry point, primary switch dispatcher, safe-mode latch, and Activity correlation logging.
    • FallbackAuthorizationService.Evaluators.cs: Granular evaluator logic across all 40 domain resource kinds (organization, event, storage_object, user, webhook, support_access_session, etc.).
    • FallbackAuthorizationService.Batch.cs: High-throughput batch evaluation engine utilizing single-pass AuthorityProfile pre-resolution and EventAuthoritySnapshotService batch querying.
    • FallbackAuthorizationService.MachineCaller.cs: Scope-ceiling and owner-type evaluation for API key machine principals.
  • Notable Evaluation Rules:
    • Instance Admin Bypass: Instance administrators bypass standard checks except for direct event authority requirements (e.g., event:manage-tickets requires explicit event authority).
    • Custom Property Governance Reports: The instance-admin local allowlist includes exactly islamuevent_custom_property_governance:view, matching the bundled Cerbos report permission. Single and optimized batch checks share that allowlist; safe mode retains instance-admin access and denies non-instance administrators. The native report handler still requires its target tenant to equal trusted ITenantContext, including for instance administrators. Machine callers retain their existing scope ceilings. This adds no governance write permission or authority for other resources.
    • Tenant Settings & Governance Locks: Updates check isLockedByInstance == true. If locked by infrastructure operators, non-instance admin update attempts are denied. tenant.branding document updates are explicitly exempted from instance locks.
    • Event Moderation: moderate-light, moderate-heavy, and unmoderate actions require Instance Admin or Tenant Admin authority in scope. Event managers, owners, and organization admins cannot moderate events.
    • User Profiles: Authenticated users can view/update their own profile (targetUserId == currentUserId); other targets require Tenant Admin or Instance Admin authority.
    • Storage Objects: Downloads are allowed if active and visibility is PublicImage or AuthenticatedTenant, or if private and createdBy == currentUserId.
    • Support Access Sessions: Active support sessions tag OpenTelemetry traces, enforce tenant isolation (support_access_target_tenant_mismatch), and block write actions when in read-only mode (support_access_read_only).

4.3. Provider Resolution Flow (RuntimeAuthorizationProvider)

The RuntimeAuthorizationProvider selects the authorization engine for a given check in the following order:

  1. Tenant BYO Cerbos: If the current tenant has a specific "Bring Your Own" Cerbos instance configured via ICerbosConfigResolver, all resource checks route to that endpoint.
  2. Handler-Owned Local Parity Bypasses: Specific requests (self-service user:update, pre-create event:create, organization:create, event_session:create, ai_conversation) are identified via GetHandlerOwnedLocalCheckIndexes() and routed directly to FallbackAuthorizationService. This guarantees that stale external PDP policy packages cannot block authoritative self-service or pre-create handlers.
  3. Instance-Level Setting: If not bypassed, the system checks the instance-wide AuthorizationProvider setting (SystemSetting key GovernanceSettingKeys.Security.AuthorizationProvider, cached for 1 minute):
    • If "cerbos", it routes to the shared instance CerbosAuthorizationService and fails closed if the PDP is unavailable.
    • If any other value (or null / "local"), it routes to FallbackAuthorizationService.

If reading the instance provider setting fails, runtime authorization uses the Cerbos fail-closed path and logs only safe FailureType metadata. It does not default open to local RBAC.

4.4. Failure Modes & The One-Way Safe-Mode Latch

The system is designed to fail safely — deny by default when the configured provider is unavailable.

  • Instance Cerbos Failure: If the connection to the instance-level Cerbos PDP fails (e.g., network error, timeout), all authorization checks are denied. The operator explicitly chose Cerbos; falling back to a potentially more permissive local RBAC would silently bypass intended policies. Restore Cerbos connectivity or explicitly switch the authorization provider setting to local RBAC through instance administration to recover without Cerbos.
  • BYO Cerbos Failure & The Safe-Mode Latch: Any tenant BYO PDP failure triggers FallbackAuthorizationService.ActivateSafeMode(). Once activated it logs a critical alert and denies all non-instance-admin requests, preventing a bypass of stricter tenant policies. The latch is scoped to the request, since FallbackAuthorizationService is registered scoped — it does not persist across requests and recovery needs no operator action. There is no fail-open setting: cerbos.failure_mode was deleted because it was parsed and then ignored at runtime, and a knob that appears to control fallback while controlling nothing is worse than none.
  • BYO Configuration Failure: If tenant BYO configuration cannot be resolved, runtime authorization activates provider-instance safe mode instead of silently using local RBAC.
  • Blank BYO PDP Endpoint: If a tenant explicitly sets cerbos.mode=custom_endpoint but leaves the custom PDP endpoint blank, the resolver preserves BYO mode, failure mode, and explicit BYO Admin API config. Runtime authorization activates safe mode; it does not fall back to the instance PDP or local RBAC.
  • Safe Logging: Runtime failure logs avoid raw endpoints, Admin API credentials, JWTs/tokens, response bodies, and exception objects/messages. They keep safe operational metadata such as failure type, action, mode, counts, request id, and correlation id.

4.5. Machine Principal (API Key) Security Architecture

API key machine callers evaluate authorization through EvaluateMachineCallerAccessAsync:

  1. Registration Workflow Prohibition: Machine callers are strictly barred from modifying registration forms, registration workflows, or managing event tickets.
  2. Scope Ceiling (MachineScopeMapping): External API key scopes (events:write, organizations:read, admin:tenant, mcp:propose, etc.) establish a maximum capability ceiling. A machine caller must satisfy this scope ceiling in addition to owner-type authority.
  3. Owner-Type Boundaries (ExternalApiKeyOwnerType):
    • InstanceAdmin: Phase 0 allows only narrow platform operations after admin:instance scope and the shared instance-admin fallback allowlist match. It does not grant tenant/content mutations, incoming webhook processing, registration workflow changes, ticket management, paid-commerce management, or ordinary event deletion.
    • Tenant: Bound to the key's TenantId. Cannot access instance settings, ATProto records, or platform namespaces.
    • Organization / Group: Bound strictly to resources owned by context.OwnerId.
    • User: Bound to user-owned resources or tenant resources where context.OwnerId has matching tenant, organization, or group admin membership. Ambient current-user admin checks do not authorize machine requests.

Machine callers are routed through ApiKeyPrincipalContext before human instance-admin shortcuts, including setting checks. Tenant and content access still requires explicit owner authority after the scope ceiling. Phase 1 Local/Cerbos parity follow-up: mirror this Phase 0 machine allowlist and owner-aware containment in bundled Cerbos policies and policy-contract tests before treating Cerbos as parity-complete for machine principals.

4.6. Batch Capability Planning Engine

To prevent $N+1$ database queries during HATEOAS link evaluation for paginated resource lists:

  1. Authority Profile Pre-Resolution: FallbackAuthorizationService.Batch.cs resolves an immutable AuthorityProfile (Instance Admin, Tenant Admin, Admin Org IDs, Admin Group IDs, Event Create Org/Group IDs) in a single pass at the start of a batch check.
  2. Batch Event Authority Snapshots: IEventAuthoritySnapshotService.GetForUserAndEventsAsync() extracts distinct event IDs from all event-scoped checks and loads active EventRoleAssignment records in a single SQL query.
  3. In-Memory Evaluation Loop: EvaluateWithProfile() evaluates all checks in CPU memory against the pre-resolved AuthorityProfile and event authority snapshot, executing batch checks in $O(1)$ database calls.

EventAuthoritySnapshotService keeps tenant, user, requested event IDs and effective assignment-window filtering in SQL. Its second query reads only role IDs and active permission codes for the roles found by the first query. Distinct permission sets are grouped in memory with ordinal comparison; no events, user profiles or navigation graphs are materialized for this grouping. This flat scalar projection avoids SQL APPLY, which SQLite cannot execute, without changing owner/manager flags, permission unions, caller-selected actions or machine-principal boundaries. Both reads retain their cancellation token and existing ambient transaction behavior; the service does not introduce a new transaction or promise a stronger cross-query snapshot. EventAuthoritySnapshotSqliteTests and NativeEventSessionLanguageHttpTests cover the portable projection and real authorization/HAL consumers. No schema, policy-package or configuration migration is required.

4.7. Policy Revision, Drift, and Convergence

Cerbos mode enforces whatever the PDP's policy store holds, which is not automatically the package this deployment published. Since the local carve-out was removed, no evaluator answers around a stale or unpublished store — so "which policy decided this?" has to be answerable rather than assumed.

How the revision is derived. Cerbos exposes no store-wide revision or content hash. It does return a content hash per policy on GET /admin/policy, and that hash changes when a policy body is edited even if its identifier does not. CerbosStoreRevision folds those hashes — sorted by store identifier, since the PDP does not preserve request order — into a 16-character token. Nothing new is published or distributed; this is a read plus a deterministic fold over values Cerbos already computes.

The token is comparable only against a previous observation of the same store on the same PDP version. It is never compared against the package ContentHash: different algorithms over different inputs, so they will never match. A PDP upgrade may shift the token with no policy change.

What each state means.

Observed state Meaning Effect on decisions
Revision observed The store's exact policy set is identified Operator status can compare it with a previous observation
Revision unknown Admin API unreachable, unlistable, or package unhealthy Runtime decisions are unaffected and continue through the gRPC PDP
Store empty Package was never published PDP denies everything; status reports PackageMismatch
Store incomplete Publish was partial Status reports PackageMismatch

Request-path isolation. The Cerbos gRPC PDP is the sole runtime decision dependency. Policy-store revision observation uses the Admin HTTP API only when an authorized operator explicitly requests package status. Admin API health, credentials, or latency cannot delay runtime authorization or HAL capability planning.

Readiness degradation. An unhealthy package no longer reads as healthy: PolicyPackageStatusResult.IsHealthy is true only for PolicyPackageIssueCode.None. PackageStatusUnknown was previously counted healthy, which was defensible while a local evaluator answered around an unreachable store; nothing answers around it now.

Operator visibility and recovery. GET api/instance/settings/authz-provider/package/status (instance-administrator gated) reports provider mode, package identity and hash, observed revision, whether it is certain, health, warnings, and a recovery action per issue code. It is deliberately separate from the anonymous authz-provider/status readiness probe, which must not disclose policy-store diagnostic state.

Recovery for the common cases: republish via POST api/instance/settings/authz-provider/sync; restore Admin API reachability if the store cannot be listed; grant the Admin API credentials policy-read permission if listing succeeds but hashes cannot be read. These are operational diagnostics and do not add an Admin API dependency to runtime decisions.

Decision telemetry. Every decision is counted on explore.authorization.decisions with duration on explore.authorization.decision.duration, dimensioned by resource kind, action, outcome, reason code, and deciding provider. Denials additionally raise an authorization.denied span event. Policy-store revision remains limited to the privileged package-status diagnostic.

5. Roles and Permissions

Admission Check-In Permissions

Admission uses public provider-neutral actions rather than role-name inspection:

Permission Authorized surface
event_check_in:view Exact-target summary, export-safe audit, bounded check-in detail, and scanner health.
event_check_in:manage Staff check-in/batch/undo plus target stop, restore, and reconcile controls.
events:manage_tickets One-time scanner capability issue/list/revoke for a PlatformManaged event.

Controllers and Application services authorize the exact tenant/event resource. Event HAL emits the same permission on each relation, and Blazor renders controls only from those relations. The dedicated AdmissionScanner principal is not a staff role: it carries only one capability UUID, tenant, event, target, and closed action claims, and cannot satisfy staff permissions.

5.1. Administrative Hierarchy

The platform defines a clear hierarchy of roles with distinct boundaries. See ADMIN_HIERARCHY.md for a detailed breakdown.

  • Instance Administrator: Operates the infrastructure. Can manage tenants but cannot access tenant business data.
  • Tenant Administrator: Manages a specific community (tenant). Can configure the tenant, manage users and content within it, but cannot override instance-locked policies.
  • Organization Administrator: A user with elevated privileges within a specific organization inside a tenant.
  • Standard User: A regular platform user.

5.2. Permission Boundaries

Strict boundaries are enforced to protect tenant autonomy and platform integrity. For example, an Instance Admin cannot read tenant business data, and a Tenant Admin cannot disable globally enforced security policies.

Tenant user participation is tenant-local. A global User authenticates the person or external identity, but tenant-admin-controlled lifecycle and moderation state lives in TenantUser/TenantUserProfile. Tenant role authority lives in TenantUserRoleGrant, an auditable child of TenantUser. Local membership checks require an active tenant-local user record plus an unrevoked tenant-scoped grant, so a suspension, ban, removal, or profile moderation action in one tenant does not affect the same external identity in another tenant.

AdminContext.GetAdminTenantIdsAsync applies the same active, non-deleted membership requirement when projecting distinct tenant-admin IDs from unrevoked grants. This authority projection feeds Cerbos human/user-owned-machine principals, Local user-owned-machine checks, and admin-authority UI responses. Generic grant inventory (GetByUserId and grant detail/list queries) retains its existing semantics; an inactive membership's unrevoked grants remain inventory, not effective authority. Tenant lifecycle status is not a new authority predicate. This eligibility correction does not change cache lifetime or establish immediate cross-request revocation freshness.

Managed-provider provisioning follows the same boundary. Provider/operator automation must authenticate through instance-admin authority before it can create customer tenants. The provisioned ERP customer/admin receives tenant-local TenantUser, TenantUserProfile, user actor, external-login binding, and TenantUserRoleGrant tenant-admin authority for that tenant only; this flow must not create PlatformUserRole rows or InstanceAdmin API keys for customer/admin identities.

Organization membership authority is also resource scoped. OrganizationMember list/detail reads use ISecureRequest plus [AuthorizeResource(ResourceKinds.OrganizationMember, AuthorizationActions.OrganizationMembers.View)]; list reads send the resolved tenant id and organization id, while detail reads send the member id and are enriched by the authorization decorator with tenant, organization, and user attributes before the provider decision. The Cerbos resource kind is islamuevent_organization_member. Local fallback mirrors the policy by allowing tenant administrators in the resolved tenant and organization administrators for the target organization, while denying regular authenticated users. HAL collection/item affordances must use the same resource/action metadata instead of local role or claim checks.

Event participation configuration uses the event manage-registrations action, not generic event update. Cerbos and local fallback allow only a controller of the verified OrganizerActorId or an event assignment carrying EventRegistrationManage. A community listing contributor, unrelated actor controller, tenant administrator, instance administrator, or machine receives no implicit participation-management authority. The CQS command and configure-participation HAL affordance use the same action metadata.

Event attendee contact export is a separate consent-resource decision. A management Event can emit export-attendees only when it has a verified organization organizer; the candidate relation requires ExportSharedContacts on event_contact_share_consent with exact tenant and organizer-organization attributes. The export command independently carries the same resource/action metadata. Blazor checks only the relation and does not infer export authority from view-participants, roles, claims, contributor status, or instance/tenant administration.

Footer configuration authority is tenant-scoped. Footer management writes reuse ResourceKinds.Tenant with AuthorizationActions.Update, and each command sends the resolved tenantId as the resource id and authorization attribute. Local fallback allows tenant administrators only for the ambient tenant and denies create/delete tenant-resource actions for non-instance administrators; Cerbos evaluates the same tenant update action. Footer UI affordances must be emitted from the same resource/action decision if HAL links are added later.

Email dispatch row operations are tenant-scoped operator actions. The status, tenant pause/resume, park, replay, resolve, and reconcile requests use ResourceKinds.EmailDispatch with tenantId and row-level outboxId where applicable. Global processor requests use ResourceKinds.InstanceSetting for email-dispatch.processor. EmailDispatch DTOs and logs remain bounded and never expose recipient email, message body, subject, provider message ids, raw provider errors, pause actors, or reconciliation evidence text.

Paid-event policy settings follow the settings boundary: instance view/update applies to the paid-event-policy instance setting, while tenant view/update applies only to the named tenant setting. Those policy resources return edit only after the matching setting decision; tenant policy revision cannot use authorization to bypass the instance policy ceiling.

Footer group deletion preserves its required current human identity. It acquires the existing footer governance setting lock before the business Serializable transaction, evaluates selected Tenant/update policy, and fences current instance or tenant administration. Multi-tenant execution reads the current stored link-group lock instead of a cached footer settings snapshot; single-tenant behavior remains exempt. The stored group must belong to the execution tenant; instance-default and foreign groups are not deletable here. Its parent fence, raw child-link deletion and parent deletion share that transaction, so failure of the parent deletion restores its links too. Bulk deletion reconciles only tracked links whose persisted IDs belong to the locked tenant-owned group; tracked child-parent substitution rejects before deletion. Those deleted children are detached before parent cascade so EF does not issue a second delete, while unrelated tracked links are retained. Twelve native cases are authored and unrun; this receipt does not establish the separately selected machine PATCH capability.

6. Implementation Patterns

6.1. CQRS Authorization Patterns

Native decorators use the standard request contract:

  1. [AuthorizeResource(ResourceKind, Action)] declares the fixed catalog capability.
  2. Optional ISecureRequest.ResourceId and AuthorizationFacts provide typed initial context.
  3. Optional IAuthorizationContextEnricher<TRequest> resolves feature context before AuthorizationResourceContextResolver applies persisted-resource authority.

There is no IAuthorizedRequest contract. An unannotated request is not newly granted authority: its reviewed handler, public capability or worker boundary remains responsible, and compiled native-aware discovery rejects new unclassified mutations.

Notification preference organization and group queries/commands use this pattern with ResourceKinds.Organization or ResourceKinds.Group and AuthorizationActions.View/AuthorizationActions.Update. Current-user preference endpoints are authenticated user-self endpoints; organization/group preference endpoints still pass through the resource authorization pipeline before handlers run.

Participation requirement writes use [AuthorizeResource(ResourceKinds.RegistrationForm, Attach|Detach)] with ISecureRequest event and requirement attributes. Persisted Event enrichment determines verified organizer control and explicit event.registration_manager authority before handlers load the attachment graph. Cerbos and local fallback use the same action catalog; tenant-only administration, listing contribution, machines, and unrelated assignments do not authorize these writes.

6.2. Claim-Based Authorization

  • User ID Extraction: Explore.Application.Authentication.PlatformIdentityPrincipalExtensions is the single authority. The chain is sub -> nameidentifier -> sid -> internal_user_id, accepting only GUID-parseable values. Call principal.GetPlatformUserId() / GetRequiredPlatformUserId() — or CurrentUserId / RequiredUserId on EventControllerBase — never a hand-rolled FindFirst.
  • internal_user_id: A BFF-enriched local-user claim added after external identity resolution. It is the last link in the chain: the provider claims are tried first because for platform-managed accounts the provider subject is the local user id.
  • Provider-linked identities: Inject IQueryHandler<ResolveCurrentUserIdByIdentityRequest, Guid?> and call identityQuery.ResolveCurrentUserIdAsync(principal, ct). A reconstructed provider account takes precedence over GUID/internal-user claims; an unlinked account returns null, never an email or claim fallback. The native query remains unannotated identity resolution, not a new PDP capability.
  • Admin Claims: A BffAdminClaimsTransformation service enriches the user's principal with specific admin claims after authentication, which can be used for UI-level authorization checks.

7. Related Documentation