# 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`: ```json { "id": "check:", "tenant": "tenant:friendly:binky", "subject": {"id": "", "type": "service"}, "action": "tenant.create", "resource": {"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. | ## Related - `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`