2026-08-29 13:02:51 +02:00
# flex-auth Integration
TEN-WP-0003: FlexAuthWriteAuthorizer -- gate writes through flex-auth
flex_auth.py: CheckRequest + FlexAuthCheckClient against flex-auth's real
POST /v1/check contract (schemas/check_request.schema.json,
decision_envelope.schema.json, read directly from the flex-auth repo, not
guessed). Fail-closed by construction: only effect=="allow" authorizes;
every other effect, non-200, malformed body, or transport failure resolves
to deny, nothing raises past is_allowed().
authz.FlexAuthWriteAuthorizer implements the existing WriteAuthorizer
Protocol. Action -> resource-type mapping coordinated with FLEX-WP-0008's
planned vocabulary (both repos reference the same table).
DefaultDenyWriteAuthorizer stays the fallback when no flex-auth URL is
configured.
config.py: Settings.from_env(), mirroring qonto-assistant's pattern.
docs/flex-auth-integration.md documents the contract, fail-closed rule,
and current real state (denies everything until FLEX-WP-0008 lands).
60 tests passing. Verified live twice over real HTTP between separate
processes (not just MockTransport): a deny-returning flex-auth double
produces 403 from POST /tenants, an allow-returning one produces 201.
Also registered (not implemented) the two workplans this depends on for a
complete picture: flex-auth/FLEX-WP-0008 (protected-system registration --
what makes allow reachable) and key-cape/KEY-WP-0005 (discovered key-cape
emits none of iam-profile_v0.3.md's core claims yet, not just missing
tenant_roles -- a bigger, security-sensitive gap flagged rather than
quietly worked around).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:54:44 +02:00
2026-08-29 13:02:51 +02:00
`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` ).
TEN-WP-0003: FlexAuthWriteAuthorizer -- gate writes through flex-auth
flex_auth.py: CheckRequest + FlexAuthCheckClient against flex-auth's real
POST /v1/check contract (schemas/check_request.schema.json,
decision_envelope.schema.json, read directly from the flex-auth repo, not
guessed). Fail-closed by construction: only effect=="allow" authorizes;
every other effect, non-200, malformed body, or transport failure resolves
to deny, nothing raises past is_allowed().
authz.FlexAuthWriteAuthorizer implements the existing WriteAuthorizer
Protocol. Action -> resource-type mapping coordinated with FLEX-WP-0008's
planned vocabulary (both repos reference the same table).
DefaultDenyWriteAuthorizer stays the fallback when no flex-auth URL is
configured.
config.py: Settings.from_env(), mirroring qonto-assistant's pattern.
docs/flex-auth-integration.md documents the contract, fail-closed rule,
and current real state (denies everything until FLEX-WP-0008 lands).
60 tests passing. Verified live twice over real HTTP between separate
processes (not just MockTransport): a deny-returning flex-auth double
produces 403 from POST /tenants, an allow-returning one produces 201.
Also registered (not implemented) the two workplans this depends on for a
complete picture: flex-auth/FLEX-WP-0008 (protected-system registration --
what makes allow reachable) and key-cape/KEY-WP-0005 (discovered key-cape
emits none of iam-profile_v0.3.md's core claims yet, not just missing
tenant_roles -- a bigger, security-sensitive gap flagged rather than
quietly worked around).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:54:44 +02:00
## 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",
2026-08-29 13:02:51 +02:00
"resource": {"id": "< tenant_id > ", "type": "tenant", "system": "tenant-engine"},
"context": {}
TEN-WP-0003: FlexAuthWriteAuthorizer -- gate writes through flex-auth
flex_auth.py: CheckRequest + FlexAuthCheckClient against flex-auth's real
POST /v1/check contract (schemas/check_request.schema.json,
decision_envelope.schema.json, read directly from the flex-auth repo, not
guessed). Fail-closed by construction: only effect=="allow" authorizes;
every other effect, non-200, malformed body, or transport failure resolves
to deny, nothing raises past is_allowed().
authz.FlexAuthWriteAuthorizer implements the existing WriteAuthorizer
Protocol. Action -> resource-type mapping coordinated with FLEX-WP-0008's
planned vocabulary (both repos reference the same table).
DefaultDenyWriteAuthorizer stays the fallback when no flex-auth URL is
configured.
config.py: Settings.from_env(), mirroring qonto-assistant's pattern.
docs/flex-auth-integration.md documents the contract, fail-closed rule,
and current real state (denies everything until FLEX-WP-0008 lands).
60 tests passing. Verified live twice over real HTTP between separate
processes (not just MockTransport): a deny-returning flex-auth double
produces 403 from POST /tenants, an allow-returning one produces 201.
Also registered (not implemented) the two workplans this depends on for a
complete picture: flex-auth/FLEX-WP-0008 (protected-system registration --
what makes allow reachable) and key-cape/KEY-WP-0005 (discovered key-cape
emits none of iam-profile_v0.3.md's core claims yet, not just missing
tenant_roles -- a bigger, security-sensitive gap flagged rather than
quietly worked around).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:54:44 +02:00
}
```
2026-08-29 13:02:51 +02:00
`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.
TEN-WP-0003: FlexAuthWriteAuthorizer -- gate writes through flex-auth
flex_auth.py: CheckRequest + FlexAuthCheckClient against flex-auth's real
POST /v1/check contract (schemas/check_request.schema.json,
decision_envelope.schema.json, read directly from the flex-auth repo, not
guessed). Fail-closed by construction: only effect=="allow" authorizes;
every other effect, non-200, malformed body, or transport failure resolves
to deny, nothing raises past is_allowed().
authz.FlexAuthWriteAuthorizer implements the existing WriteAuthorizer
Protocol. Action -> resource-type mapping coordinated with FLEX-WP-0008's
planned vocabulary (both repos reference the same table).
DefaultDenyWriteAuthorizer stays the fallback when no flex-auth URL is
configured.
config.py: Settings.from_env(), mirroring qonto-assistant's pattern.
docs/flex-auth-integration.md documents the contract, fail-closed rule,
and current real state (denies everything until FLEX-WP-0008 lands).
60 tests passing. Verified live twice over real HTTP between separate
processes (not just MockTransport): a deny-returning flex-auth double
produces 403 from POST /tenants, an allow-returning one produces 201.
Also registered (not implemented) the two workplans this depends on for a
complete picture: flex-auth/FLEX-WP-0008 (protected-system registration --
what makes allow reachable) and key-cape/KEY-WP-0005 (discovered key-cape
emits none of iam-profile_v0.3.md's core claims yet, not just missing
tenant_roles -- a bigger, security-sensitive gap flagged rather than
quietly worked around).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:54:44 +02:00
Action → resource-type mapping (must match `flex-auth` 's
`FLEX-WP-0008-T01` vocabulary exactly — coordinate values, don't diverge):
2026-08-29 13:02:51 +02:00
| 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) |
TEN-WP-0003: FlexAuthWriteAuthorizer -- gate writes through flex-auth
flex_auth.py: CheckRequest + FlexAuthCheckClient against flex-auth's real
POST /v1/check contract (schemas/check_request.schema.json,
decision_envelope.schema.json, read directly from the flex-auth repo, not
guessed). Fail-closed by construction: only effect=="allow" authorizes;
every other effect, non-200, malformed body, or transport failure resolves
to deny, nothing raises past is_allowed().
authz.FlexAuthWriteAuthorizer implements the existing WriteAuthorizer
Protocol. Action -> resource-type mapping coordinated with FLEX-WP-0008's
planned vocabulary (both repos reference the same table).
DefaultDenyWriteAuthorizer stays the fallback when no flex-auth URL is
configured.
config.py: Settings.from_env(), mirroring qonto-assistant's pattern.
docs/flex-auth-integration.md documents the contract, fail-closed rule,
and current real state (denies everything until FLEX-WP-0008 lands).
60 tests passing. Verified live twice over real HTTP between separate
processes (not just MockTransport): a deny-returning flex-auth double
produces 403 from POST /tenants, an allow-returning one produces 201.
Also registered (not implemented) the two workplans this depends on for a
complete picture: flex-auth/FLEX-WP-0008 (protected-system registration --
what makes allow reachable) and key-cape/KEY-WP-0005 (discovered key-cape
emits none of iam-profile_v0.3.md's core claims yet, not just missing
tenant_roles -- a bigger, security-sensitive gap flagged rather than
quietly worked around).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:54:44 +02:00
## What tenant-engine expects back
A `DecisionEnvelope` per `flex-auth/schemas/decision_envelope.schema.json` .
2026-08-29 13:02:51 +02:00
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.
TEN-WP-0003: FlexAuthWriteAuthorizer -- gate writes through flex-auth
flex_auth.py: CheckRequest + FlexAuthCheckClient against flex-auth's real
POST /v1/check contract (schemas/check_request.schema.json,
decision_envelope.schema.json, read directly from the flex-auth repo, not
guessed). Fail-closed by construction: only effect=="allow" authorizes;
every other effect, non-200, malformed body, or transport failure resolves
to deny, nothing raises past is_allowed().
authz.FlexAuthWriteAuthorizer implements the existing WriteAuthorizer
Protocol. Action -> resource-type mapping coordinated with FLEX-WP-0008's
planned vocabulary (both repos reference the same table).
DefaultDenyWriteAuthorizer stays the fallback when no flex-auth URL is
configured.
config.py: Settings.from_env(), mirroring qonto-assistant's pattern.
docs/flex-auth-integration.md documents the contract, fail-closed rule,
and current real state (denies everything until FLEX-WP-0008 lands).
60 tests passing. Verified live twice over real HTTP between separate
processes (not just MockTransport): a deny-returning flex-auth double
produces 403 from POST /tenants, an allow-returning one produces 201.
Also registered (not implemented) the two workplans this depends on for a
complete picture: flex-auth/FLEX-WP-0008 (protected-system registration --
what makes allow reachable) and key-cape/KEY-WP-0005 (discovered key-cape
emits none of iam-profile_v0.3.md's core claims yet, not just missing
tenant_roles -- a bigger, security-sensitive gap flagged rather than
quietly worked around).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:54:44 +02:00
## Configuration
| Env var | Default | Meaning |
| --- | --- | --- |
2026-08-29 13:02:51 +02:00
| `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. |
TEN-WP-0003: FlexAuthWriteAuthorizer -- gate writes through flex-auth
flex_auth.py: CheckRequest + FlexAuthCheckClient against flex-auth's real
POST /v1/check contract (schemas/check_request.schema.json,
decision_envelope.schema.json, read directly from the flex-auth repo, not
guessed). Fail-closed by construction: only effect=="allow" authorizes;
every other effect, non-200, malformed body, or transport failure resolves
to deny, nothing raises past is_allowed().
authz.FlexAuthWriteAuthorizer implements the existing WriteAuthorizer
Protocol. Action -> resource-type mapping coordinated with FLEX-WP-0008's
planned vocabulary (both repos reference the same table).
DefaultDenyWriteAuthorizer stays the fallback when no flex-auth URL is
configured.
config.py: Settings.from_env(), mirroring qonto-assistant's pattern.
docs/flex-auth-integration.md documents the contract, fail-closed rule,
and current real state (denies everything until FLEX-WP-0008 lands).
60 tests passing. Verified live twice over real HTTP between separate
processes (not just MockTransport): a deny-returning flex-auth double
produces 403 from POST /tenants, an allow-returning one produces 201.
Also registered (not implemented) the two workplans this depends on for a
complete picture: flex-auth/FLEX-WP-0008 (protected-system registration --
what makes allow reachable) and key-cape/KEY-WP-0005 (discovered key-cape
emits none of iam-profile_v0.3.md's core claims yet, not just missing
tenant_roles -- a bigger, security-sensitive gap flagged rather than
quietly worked around).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:54:44 +02:00
## Related
2026-08-29 13:02:51 +02:00
- `pep-stance.yaml` — published fail-closed map
- `pip-claims.yaml` — input-class freshness
TEN-WP-0003: FlexAuthWriteAuthorizer -- gate writes through flex-auth
flex_auth.py: CheckRequest + FlexAuthCheckClient against flex-auth's real
POST /v1/check contract (schemas/check_request.schema.json,
decision_envelope.schema.json, read directly from the flex-auth repo, not
guessed). Fail-closed by construction: only effect=="allow" authorizes;
every other effect, non-200, malformed body, or transport failure resolves
to deny, nothing raises past is_allowed().
authz.FlexAuthWriteAuthorizer implements the existing WriteAuthorizer
Protocol. Action -> resource-type mapping coordinated with FLEX-WP-0008's
planned vocabulary (both repos reference the same table).
DefaultDenyWriteAuthorizer stays the fallback when no flex-auth URL is
configured.
config.py: Settings.from_env(), mirroring qonto-assistant's pattern.
docs/flex-auth-integration.md documents the contract, fail-closed rule,
and current real state (denies everything until FLEX-WP-0008 lands).
60 tests passing. Verified live twice over real HTTP between separate
processes (not just MockTransport): a deny-returning flex-auth double
produces 403 from POST /tenants, an allow-returning one produces 201.
Also registered (not implemented) the two workplans this depends on for a
complete picture: flex-auth/FLEX-WP-0008 (protected-system registration --
what makes allow reachable) and key-cape/KEY-WP-0005 (discovered key-cape
emits none of iam-profile_v0.3.md's core claims yet, not just missing
tenant_roles -- a bigger, security-sensitive gap flagged rather than
quietly worked around).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:54:44 +02:00
- `flex-auth/workplans/FLEX-WP-0008-tenant-engine-consumer-integration.md`
2026-08-29 13:02:51 +02:00
- `key-cape/workplans/KEY-WP-0005-iam-profile-core-claims.md`
- `net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md`