Authorization does not ask whether a user signed in. It asks whether this trusted caller may perform this action on this resource, in this tenant, now. Several policies contribute evidence, and the result must remain auditable, invalidatable, and reproducible.
1. One capability, two source areas
The runtime is split between Framework and Platform code:
| Owner | Representative types | Responsibility |
|---|---|---|
| Framework Application | IAuthorizedRequest, AuthorizationDecisionService, evaluators, DataScopeResolver | Runtime decision protocol and composition rules |
| Authorization Contracts | RBAC, field, sharing, delegation, request contracts, catalogs | Public contracts and owner extension points |
| Authorization Application | policy management, effective-permission explanation, temporary grants | Change and explain policy state, publish events, invalidate caches |
| Authorization Infrastructure | owner-local records, stores, caches, and dual-ORM adapters | Persist relations, rules, classification, overrides, and temporary grants |
| Host composition | CoreRuntime and Persistence registration | Install placeholders first, then close the graph with production adapters |
Platform owns policy facts; Framework evaluates them. A business module must not query Authorization tables or reproduce policy with local role checks.
2. Request-to-decision path
The authorization behavior executes before the transaction behavior. A denial returns Authorization.Denied, skips the handler, and must not start a transaction. AuthorizationPipelineBehaviorTests.Deny_Should_Prevent_Transaction_From_Starting fixes this ordering.
3. Evaluators are not independent gates
| Evaluator | Allow | Deny | Neutral |
|---|---|---|---|
| RBAC | A non-Application caller has {module}.{resource}.{action} | Never directly | Permission absent or caller not applicable |
| AppScope | An Application caller has the same scope | Never directly | Scope absent or caller not applicable |
| ABAC | First matching rule says Allow | First match says Deny; store failure also denies | No rules or no condition match |
| ReBAC Lite | A user has an action-compatible relation to a Case, Client, or Billing instance | Relation store returns Failure | No subject, instance, relation, or supported resource |
| Feature | Mapped feature for tickets, chat, workflow, or reporting is enabled | Mapped feature is disabled or provider returns false | Module has no mapping |
Every evaluator runs before the final merge. Any Deny rejects; otherwise one or more Allows permit; all Neutral yields NoMatchingPolicy. Registration order affects the first denial reason and obligation order, but placing Feature last is not what gives Deny priority.
See decision engine for the algorithm, cache key, DataScope, and audit behavior.
4. Management plane versus decision plane
The management plane changes roles, bindings, permissions, ABAC, Feature, field policy, classification, sharing, and delegated grants through owner-local stores. A workflow-approved access request is a separate, absolutely expiring authorization source. The decision plane reads trusted subjects, resource facts, policy stores, and caches; it never calls a management handler.
public sealed record ApproveInvoiceCommand( string InvoiceId, decimal Amount, string Status, string Sensitivity) : ICommand<Result>, IAuthorizedRequest{ // The same descriptor feeds the RBAC code, ABAC facts, and instance-level ReBAC. public ResourceDescriptor Resource => new( Module: "billing", ResourceType: "invoice", ResourceId: InvoiceId, TenantId: TrustedTenantId, Status: Status, Amount: Amount, Sensitivity: Sensitivity);
// The action participates in permission derivation and policy filtering. public AuthorizationAction Action => AuthorizationAction.Approve;
// Use the caller's trusted tenant accessor, never a tenant supplied in the body. private string TrustedTenantId => CurrentTenantAccessor.EffectiveTenantId;}This complete contract example uses CurrentTenantAccessor as a stand-in for the caller’s trusted tenant port. In production, load owner, status, amount, and sensitivity from server-side data rather than accepting self-declared facts.
5. The current model is not a Role aggregate
Earlier prose described a Role.Grant() aggregate, but the implementation uses explicit store records:
| Model | Stable key and boundary |
|---|---|
RoleRecord / RoleCatalogRecord | Database ID for management; tenant-local role name for relation rows |
UserRoleRecord / UserRoleRelationRecord | Identity subject key plus a role normalized to its name |
RolePermissionRecord / relation record | Role name, Menu module code, permission code |
| Permission records | Global generated or deterministically seeded catalog |
| ABAC records | Tenant rule with integer condition and verdict fields |
| Feature definition + override | Global default separated from tenant override |
ResourceRelationRecord | Tenant-local subject-to-resource relation without cross-context CLR navigation |
See RBAC management and role lifecycle for stable-key and write-path details.
6. Management endpoints
| Capability | Representative route | Resource / action |
|---|---|---|
| Roles | GET/POST /api/authorization/roles, PUT/DELETE /roles/{roleId} | authorization/role + View/Create/Update/Delete |
| User roles | GET/POST/DELETE /users/{userId}/roles/{roleId} | authorization/user-role + View/Assign/Revoke |
| Role permissions | GET/POST/DELETE /roles/{roleId}/permissions | authorization/role-permission + View/Grant/Revoke |
| Permission tree | GET /api/authorization/permissions/tree | authorization/permission + View |
| ABAC | GET/POST/PUT/DELETE /api/authorization/abac-rules | authorization/abac-rule + View/Create/Update/Delete |
| Feature | GET /api/authorization/features, PUT /features/{featureCode} | authorization/feature + View/Update |
| Advanced governance | /field-security, /sharing, /delegated-admin, /permission-audit, /permission-simulation, /permission-requests | capability-specific View/Manage/Approve/Reject/Revoke |
Grant and revoke commands now use AuthorizationAction.Grant and AuthorizationAction.Revoke, matching the public permission catalog. User-role assignment and revocation invalidate the target subject’s permission and menu caches rather than the actor’s.
7. Cache and event boundaries
- Role and role-permission changes publish events and invalidate the tenant Permission cache.
- ABAC CRUD invalidates the effective tenant’s Permission decision cache.
- Feature updates publish
FeatureChangedIntegrationEvent; a global-default change invalidates all Feature entries, while an override targets one tenant. - The decision cache key covers caller, claims, tenant, client, all resource facts, and action; Delegated callers bypass it.
- DataScope and ReBAC use independent caches and require independent invalidation.
Role changes use AuthorizationSubjectCacheInvalidation for the target subject and cross-instance menu synchronization. Group relationships, field policy, sharing rules, and temporary grants retain separate sources and invalidation paths; clearing the Permission cache does not refresh every derived catalog. See configuration, persistence, and caching.
8. Chapter map
| Goal | Read |
|---|---|
| Understand merge, caching, DataScope, and audit | Decision engine |
| Design roles, grants, assignments, and cleanup | RBAC lifecycle |
| Apply attributes, relations, and entitlements | ABAC, ReBAC, and Feature |
| Compose stores and reason about invalidation | Configuration and persistence |
| Adopt field, sharing, delegation, and temporary grants | Advanced governance, testing, and operations |
9. Source review commands
Run from the BitzOrcasVNext root:
# Read both halves of the capability: runtime decisions and policy management.rg -n "class AuthorizationDecisionService|class .*PolicyEvaluator|class .*Role" \ src/Framework/BitzOrcas.Application/Authorization src/Platform/Authorization -g '*.cs'
# Grant and Revoke actions must remain isomorphic with the public catalog.rg -n "AuthorizationAction\.(Grant|Revoke)|role-permission\.(grant|revoke)|user-role\.revoke" \ src/Platform/Authorization -g '*.cs'
# Every invalidation must target the changed user, tenant, resource, or Feature.rg -n "InvalidateBy(User|Tenant|Resource)Async" \ src/Platform/Authorization src/Framework/BitzOrcas.Application/Authorization -g '*.cs'10. Minimum GA evidence
- Every request-derived permission exists in the catalog.
- Assignment and revocation invalidate the target subject’s decisions and menu projection immediately.
- ABAC, ReBAC, and Feature failures retain fail-closed behavior.
- Consumers convert DataScope into query filters and prove tenant isolation.
- Cache hits recompute DataScope and audit the call; delegated sessions never outlive their grant through cache.
- Field security, sharing, delegation, and temporary grants have no unregistered owner execution surfaces.
- Both ORM contracts, seed idempotency, and global permission/Feature uniqueness pass.