TEN-DEC-2026-002 answers flex-auth's FLEX-WP-0022-T01, open since 2026-09-15: `tenant` denotes the target tenant record, equals `resource.id` by intent, and the write API is cross-tenant by design — no action is refused on the subject/tenant relationship, and tenant.guardrail.read must not differ because flex-auth itself calls it across tenants. docs/flex-auth-integration.md states the relation in this repo's voice. TEN-IN-0004 is the live record flex-auth asked for on FLEX-WP-0020. Runtime names stay flex-auth (FLEX-DEC-2026-013) and all deploy, cluster and settings coordinates verify as retained; only five documentation repository paths change when the rename lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 63291@bnt-lap001 Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
157 lines
7.2 KiB
Markdown
157 lines
7.2 KiB
Markdown
# 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:<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.
|
|
|
|
## What `tenant` denotes (TEN-DEC-2026-002)
|
|
|
|
`tenant` is the **target tenant record** — the tenant the action operates on.
|
|
It is *not* the caller's tenant, and on guardrail actions it is not a separate
|
|
"tenant the guardrail applies to": a guardrail is addressed only through the
|
|
tenant it constrains, so that is the same tenant.
|
|
|
|
`tenant` always equals `resource.id`. That is intended, not incidental: one
|
|
`tenant_id` is copied onto both fields in `authz.FlexAuthWriteAuthorizer`.
|
|
A check where the two differ did not come from this engine.
|
|
|
|
`tenant` cannot denote the caller's tenant because this service does not know
|
|
it. No inbound token is verified; the acting identity arrives as a
|
|
caller-supplied `actor` string (`tenancy.yaml` I1). There is no caller tenant
|
|
in scope when the check is built.
|
|
|
|
**The write API is cross-tenant by design. No action in the vocabulary is
|
|
refused on the relationship between `subject` and `tenant`.** The subjects are
|
|
platform service identities in `tenant:platform`; the targets are arbitrary
|
|
tenant records. Creating and retiring tenants cannot be done from inside the
|
|
tenant concerned, and `tenant.create` has no existing target at check time, so
|
|
a same-tenant rule would deny every create. Authorization here is a
|
|
service-identity question over `(subject.id, action)`; `tenant` and
|
|
`resource.id` say *which record* is being touched, for the decision record and
|
|
the audit trail.
|
|
|
|
`tenant.guardrail.read` does not differ, and must not: flex-auth calls it while
|
|
deciding about arbitrary tenants, so a same-tenant rule on it would break the
|
|
PDP itself.
|
|
|
|
Varying `tenant` in a conformance fixture must not change the effect. That is
|
|
the assertion this engine wants the published package to make — an unrestricted
|
|
scope that is *stated* is a rule a reviewer can check; an unrestricted scope
|
|
that is merely unwritten is indistinguishable from an omitted one.
|
|
|
|
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. |
|
|
|
|
## Repository coordinate vs runtime name (TEN-IN-0004)
|
|
|
|
The `flex-auth` repository is renaming to `access-engine` (`FLEX-WP-0020`).
|
|
**Runtime names stay `flex-auth`** per `FLEX-DEC-2026-013`: the cluster
|
|
namespace, the `flex-auth-tenant-engine` Service, the token audience, and this
|
|
engine's `TENANT_ENGINE_FLEX_AUTH_*` settings are all unchanged by the rename.
|
|
Only the `flex-auth/...` repository paths cited below and above change, and
|
|
only once the rename lands.
|
|
|
|
## 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`
|