Chapter 05 / 09
ENSK

SmartExchange · .NET 10 · Chapter 05

Principal context and Policy

The resolver prepares bounded facts. The policy chooses which facts authorize an operation; it contains the business decision, including read-to-export relationships.

Shared code · Intaker + Web APIReviewed · 2026-10-27

The three shared interfaces

Reader → Resolver → Policy client
// BmcGroup.Authorization.Contracts
Task<IReadOnlyList<AuthorizationGrant>> IAuthorizationGrantReader.ReadAsync(
    AuthorizationGrantRequest request, CancellationToken cancellationToken);

Task<AuthorizationPrincipal> IAuthorizationContextResolver.ResolveAsync(
    AuthorizationContextRequest request, CancellationToken cancellationToken);

Task<AuthorizationQueryPlan> IAuthorizationPolicyClient.PlanResourcesAsync(
    AuthorizationPlanRequest request, CancellationToken cancellationToken);
// Signatures condensed; cancellationToken defaults to default in source.

AuthorizationContextRequest(UserId, Product, Resource) selects context. The resolver creates AuthorizationGrantRequest(UserId, Product, ResourceKinds) for the reader. Planning then uses AuthorizationPlanRequest(RequestId, Principal, Resource, Action).

Implemented preparation

  1. LegacySqlAuthorizationGrantAdapter.ReadAsync() loads active assignments, role permissions and direct/group/public access from the database bound to the product. SQL narrows scope anchors and supported permissions before materialization.
  2. Normalize(rows, diagram, resourceKinds) converts numeric storage codes to named grants and validates their scopes. The raw LegacyAuthorizationGrantRow stays next to this adapter.
  3. SmartExchangeAuthorizationContextResolver.ResolveAsync() selects root + annotated targets, validates selected facts and emits roles: ["user"], attr.product and attr.grants. It filters again even when another reader is used.

AuthorizationGrant has six fields: assignment provenance, scoped record ID, authorized resource, action, scope and public-assignment flag. IsPublic=false can mean direct user or group access; it does not identify the membership route.

The temporary */admin/Instance fact is still retained in source. Both the legacy admin pseudoaction and * resource are planned for removal. The resolver does not automatically map a database role named exportAdmin into a Cerbos role.

Two grants → two derived roles → two complete policies

Both examples follow the same steps and use the same shared implementation. Person export accepts a related Company/read grant. Proposed InterestedParty export accepts InterestedParty/read in Project scope. This read-to-export choice is explicit policy logic; it does not change the grant action or imply access from Project/read.

Both complete demo policies use version presentation-demo, import an existing derived-role set and contain one export ALLOW rule. They intentionally isolate the relationship path; no exportAdmin, direct or instance-wide root bypass is included. Unmatched requests are denied. These are documentation proposals, not the current full production/default policies.

Person/export

The synthetic CompanyReader assignment normalizes to Company/read/Resource. The demo policy permits Person export through that Company relationship.

Normalized grant
{
  "assignmentId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
  "resourceId": "11111111-1111-1111-1111-111111111111",
  "resource": "Company",
  "action": "read",
  "scope": "Resource",
  "isPublic": false
}
Principal · complete JSON
Principal · complete JSON
{
  "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
  "roles": [
    "user"
  ],
  "attr": {
    "product": "QA",
    "grants": [
      {
        "assignmentId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
        "resourceId": "11111111-1111-1111-1111-111111111111",
        "resource": "Company",
        "action": "read",
        "scope": "Resource",
        "isPublic": false
      }
    ]
  }
}
Derived role · installed definition
Derived role · installed definition
# Runtime snapshot: aks-smartcloud-nonprod / dev-authorization / derived-role-person-company-read
# Read on 2026-10-27. YAML content only; not a Kubernetes installation manifest.
apiVersion: api.cerbos.dev/v1
description: >-
  Person.companyIds ReBAC condition: the user has a normalized Company/read
  grant for at least one related record. This definition alone grants no action.
derivedRoles:
  name: person_company_read
  variables:
    local:
      personCompanyReadAll: >-
        has(P.attr.grants) && P.attr.grants.exists(g,
          g.resource == "Company" && g.action == "read" && g.scope == "Instance")
      personCompanyReadIds: >-
        has(P.attr.grants) ? P.attr.grants.filter(g,
          g.resource == "Company" && g.action == "read" && g.scope == "Resource" &&
          g.resourceId != "00000000-0000-0000-0000-000000000000").map(g, g.resourceId) : []
  definitions:
    - name: person_with_readable_company
      parentRoles: [user]
      condition:
        match:
          expr: >-
            R.kind == "Person" &&
            R.attr.companyIds.exists(relatedId,
              V.personCompanyReadAll || relatedId in V.personCompanyReadIds)
Resource policy · complete demo YAML
Resource policy · complete demo YAML
# Documentation proposal only; not deployed. Separate version, no admin/direct bypass.
apiVersion: api.cerbos.dev/v1
description: Person export using read access to a related Company.
resourcePolicy:
  version: presentation-demo
  resource: Person
  importDerivedRoles:
    - person_company_read
  rules:
    - name: export-persons-through-readable-company
      actions: [export]
      derivedRoles: [person_with_readable_company]
      effect: EFFECT_ALLOW

Derived-role YAML · Complete demo policy · Plan request

InterestedParty/export

The snapshot-backed Project View assignment normalizes to InterestedParty/read/Project. Export is the proposed additional operation accepted by this demo policy.

Normalized grant
{
  "assignmentId": "a7f53aab-6f40-4204-b1d3-02ea54f1713e",
  "resourceId": "b48e464a-88cf-4743-bbce-98d6ba89e399",
  "resource": "InterestedParty",
  "action": "read",
  "scope": "Project",
  "isPublic": false
}
Principal · complete JSON
Principal · complete JSON
{
  "id": "5a3e5b0f-fb6d-4abf-ba4f-8b581365a795",
  "roles": [
    "user"
  ],
  "attr": {
    "product": "API_Dev1",
    "grants": [
      {
        "assignmentId": "a7f53aab-6f40-4204-b1d3-02ea54f1713e",
        "resourceId": "b48e464a-88cf-4743-bbce-98d6ba89e399",
        "resource": "InterestedParty",
        "action": "read",
        "scope": "Project",
        "isPublic": false
      }
    ]
  }
}
Derived role · installed definition
Derived role · installed definition
# Runtime snapshot: aks-smartcloud-nonprod / dev-authorization / derived-role-interested-party-project-scope
# Read on 2026-10-27. YAML content only; not a Kubernetes installation manifest.
apiVersion: api.cerbos.dev/v1
description: InterestedParty read restricted by an explicit Project scope.
derivedRoles:
  name: interested_party_project_scope
  variables:
    local:
      projectScopeIds: >-
        has(P.attr.grants) ? P.attr.grants.filter(g,
          g.resource == "InterestedParty" &&
          g.action == "read" &&
          g.scope == "Project" &&
          g.resourceId != "00000000-0000-0000-0000-000000000000"
        ).map(g, g.resourceId) : []
  definitions:
    - name: interested_party_in_project_scope
      parentRoles: [user]
      condition:
        match:
          expr: >-
            R.kind == "InterestedParty" &&
            size(V.projectScopeIds) > 0 &&
            R.attr.projectIds.exists(projectId,
              projectId in V.projectScopeIds)
Resource policy · complete demo YAML
Resource policy · complete demo YAML
# Documentation proposal only; not deployed. Separate version, no admin/direct bypass.
apiVersion: api.cerbos.dev/v1
description: InterestedParty export using explicit read permission in Project scope.
resourcePolicy:
  version: presentation-demo
  resource: InterestedParty
  importDerivedRoles:
    - interested_party_project_scope
  rules:
    - name: export-interested-parties-in-project-scope
      actions: [export]
      derivedRoles: [interested_party_in_project_scope]
      effect: EFFECT_ALLOW

Derived-role YAML · Complete demo policy · Plan request

Same mechanism, different grant meaning. The Person role checks Company/read grants. The InterestedParty role checks InterestedParty/read scoped to Project; it must not be replaced with the installed target-Project/read role. Importing a derived-role set alone grants no action; the resource policy must reference its role.

Current source/runtime references, separate from these demos: Person source policy · Person live default · InterestedParty live default. Person source currently spells the relationship inline; the demo shows its equivalent derived-role composition. InterestedParty/default currently enables this scoped role for read, not export.