tenant-engine/docs/flex-auth-integration.md
tegwick 672cf4da6e
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 37s
Implement TEN-WP-0011 security layer conformance
Engine/PIP declaration is now checkable (layer.yaml plus a Tooling-client
scan). Writes persist a decision record or the published fail-closed
stance, live-lookup freshness is published, events_for is tenant-scoped,
and mutation evidence drains to audit-core from a local outbox without
blocking the mutation.

Sender registration is requested as AUDIT-IN-0002. Boundary-contract
amendment is requested as NET-IN-0002.

Assistant: grok
Assistant-Session: 01a04cea-e5e8-7081-a0fc-808ebbc35fa9
2026-08-29 13:02:51 +02:00

4.9 KiB

flex-auth Integration

tenant-engine gates every write, and every authorized read, through flex-auth's POST /v1/check (flex_auth.FlexAuthCheckClient, wired in as authz.FlexAuthWriteAuthorizer). Writes are PEP-shaped: no mutation without a decision record or a recorded fail-closed stance (pep-stance.yaml).

What tenant-engine sends

A CheckRequest per flex-auth/schemas/check_request.schema.json:

{
  "id": "check:<uuid>",
  "tenant": "tenant:friendly:binky",
  "subject": {"id": "<actor>", "type": "service"},
  "action": "tenant.create",
  "resource": {"id": "<tenant_id>", "type": "tenant", "system": "tenant-engine"},
  "context": {}
}

context is always empty. This engine never puts tenant_roles or any other PIP claim on the check it sends. The check asks whether this caller may use this admin/PIP surface, not what the tenant is allowed to do on the platform.

Action → resource-type mapping (must match flex-auth's FLEX-WP-0008-T01 vocabulary exactly — coordinate values, don't diverge):

Action Resource type Surface
tenant.create tenant write
tenant.role.grant role-grant write
tenant.role.revoke role-grant write
tenant.plan.assign plan-assignment write
tenant.update tenant write
tenant.retire tenant write
tenant.reactivate tenant write
tenant.grouping.set tenant write
tenant.guardrail.read guardrail read
tenant.guardrail.set guardrail write
tenant.read tenant read
tenant.role.read role-grant read (cache-read)
tenant.role.read.live role-grant read (live-lookup)

What tenant-engine expects back

A DecisionEnvelope per flex-auth/schemas/decision_envelope.schema.json. Only effect: "allow" authorizes the action. Every other effect, a non-200 response, a malformed body, or a transport failure/timeout all resolve to deny. FlexAuthCheckClient.check() never raises past its own boundary; it returns a CheckResult that is either a decision or a recorded application of the fail-closed stance.

The decision id, request digest, effect, and source are persisted on authz_records for every attempt and on the mutation event payload for every successful write. Verdicts are never cached: every call is a new POST (pep-stance.yaml verdict_caching: none).

Live-lookup is not cyclic (TEN-WP-0011-T03)

GET /tenants/{id}/roles/live authorizes via tenant.role.read.live then reads the store. That check does not re-enter tenant-engine:

  1. The CheckRequest this engine sends has empty context and does not carry tenant_roles.
  2. FlexAuthCheckClient POSTs only /v1/check. Proven by tests/test_pip_claims.py::test_live_lookup_check_does_not_reenter_tenant_engine.
  3. flex-auth's tenant-engine policy package (flex-auth/examples/tenant-engine/policy_package.md) matches subject.id and action only. It does not consult tenant capability roles. The package's own scope note says those roles are tenant state a different protected system might consult via live-lookup; conflating the two would authorize the wrong thing.
  4. flex-auth's live-roles adapter is built and unwired (FLEX-WP-0015, flex-auth/docs/tenancy-posture-review.md): no non-test caller, no current policy consumes tenant_roles for a privileged path.

Together: GET /roles/live → POST /v1/check is a service-identity question. The PDP does not need tenant-engine claims to answer it, so it does not call back.

Note: tenant.role.read / tenant.role.read.live / tenant.read / tenant.grouping.set are not yet in that policy package's valid_actions. A live flex-auth will currently unknown_action those four. That is itself evidence they are not evaluated via tenant-role claims. Adding them as static service-identity rules (same shape as tenant.guardrail.read) is flex-auth work, not a cycle to unwind here.

Unreachable-engine stance

Published in pep-stance.yaml. Every scope is fail_closed. DefaultDeny (URL unset) and transport failure both apply that stance and record it. Tests assert the file equals shipped behaviour.

Configuration

Env var Default Meaning
TENANT_ENGINE_FLEX_AUTH_URL unset Base URL of a reachable flex-auth deployment. When unset, create_app() falls back to authz.DefaultDenyWriteAuthorizer.
TENANT_ENGINE_FLEX_AUTH_TIMEOUT_SECONDS 3 Bounded timeout on the synchronous path — no retries.
TENANT_ENGINE_FLEX_AUTH_TOKEN_FILE unset Rotating bearer token file, read on each check.
  • pep-stance.yaml — published fail-closed map
  • pip-claims.yaml — input-class freshness
  • flex-auth/workplans/FLEX-WP-0008-tenant-engine-consumer-integration.md
  • key-cape/workplans/KEY-WP-0005-iam-profile-core-claims.md
  • net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md