flex-auth/workplans/FLEX-WP-0014-tenant-guardrail-policy-actions.md
codex 1d90aac30f
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
fix(workplans): adopt ADR-007 derived identifiers for unregistered records
These workplans exist only in the retired local hub. Their random pre-ADR-007
identifiers are refused by C-06 as stale references, so they cannot be
registered. Deriving from the canonical record id takes no identity from
anything: central does not hold them and the old ids die with the cache.

Records central already holds were deliberately left untouched.

Refs CUST-WP-0068-T06

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-25 20:10:35 +02:00

212 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: FLEX-WP-0014
type: workplan
title: "Authorize tenant-engine guardrail actions"
domain: infotech
repo: flex-auth
status: finished
owner: grok
topic_slug: netkingdom
planning_priority: P1
planning_order: 140
depends_on_workplans:
- FLEX-WP-0010
- FLEX-WP-0013
related_workplans:
- TEN-WP-0006
created: "2026-08-16"
updated: "2026-08-16"
state_hub_workstream_id: "7de17bd2-5e7c-5355-ba1d-40a051a67746"
---
# FLEX-WP-0014 - Authorize tenant-engine guardrail actions
Extend package `tenant-engine.write-api.mutate` from seven actions to nine
with the TEN-WP-0006 guardrail surface. Until they exist here, every
guardrail check resolves to `deny` `unknown_action` — correct fail-closed
behaviour, but the surface is inert.
**Requested by:** `tenant-engine` (TEN-WP-0006, message `5572d198`).
**Blocked on:** FLEX-WP-0013 (four-action production pin). That restore is
done; this workplan starts on a current seven-action package, not a
rolled-back one.
## Requirements from tenant-engine
Two new actions, both on resource type `guardrail`, both against
`system: "tenant-engine"`:
| Action | Resource type | HTTP surface |
|---|---|---|
| `tenant.guardrail.read` | `guardrail` | `GET /tenants/{id}/guardrails?actor=` |
| `tenant.guardrail.set` | `guardrail` | `PUT` / `DELETE /tenants/{id}/guardrails/{limit_key}` |
Strings are live in `tenant-engine/src/tenant_engine/authz.py`
`_RESOURCE_TYPES`. Match them verbatim. `CheckRequest` shape is unchanged:
`subject_type: "service"`, `resource_id` = `tenant_id`.
They are two actions so policy can grant a PDP the read without granting
the write. tenant-engine's own tests call the read as `actor=flex-auth`.
`v1` stays the activated revision; the identifying fact is that `v1` as of
this commit contains the two guardrail actions.
Do **not** deploy a new production image in this workplan until the package
is verified locally. Image + pin is a follow-on, named in T04.
## T01 - Extend the policy package with the guardrail actions
```task
id: FLEX-WP-0014-T01
status: done
priority: high
state_hub_task_id: "34fb239a-7f06-5e96-9f10-a6bbb85bf4c3"
```
Add `tenant.guardrail.read` and `tenant.guardrail.set` to
`valid_actions`, the frontmatter `actions:` list, and
`protected_system_manifest.yaml`. Add resource type `guardrail`.
CARING: `View` for the read (already used by qonto-assistant), `EditAny`
for the set (already used by `tenant.update`). Extend
`caring.capabilities` with `View`.
Register `flex-auth` as a second service subject that is allowed
`tenant.guardrail.read` and nothing else. Update `subject_manifest.yaml`
and rebuild `examples/tenant-engine/registry_snapshot.json`.
Update `docs/tenant-engine-action-vocabulary.md` and
`docs/tenant-engine-resource-namespace.md`. Keep the existing note that
this policy governs who may call tenant-engine's admin API, not a tenant's
own capability roles.
Done when `valid_actions` carries all nine actions, the new resource type
is declared, and `flex-auth` is a registered read-only subject.
Done 2026-08-16: nine actions in frontmatter, `valid_actions`, and the
protected-system manifest. Resource type `guardrail` added. CARING
`View` on the read, `EditAny` on the set. `flex-auth` registered as a
read-only subject. Registry snapshot rebuilt (2 subjects, 2 groups).
## T02 - Decide how the read/write split is used today
```task
id: FLEX-WP-0014-T02
status: done
priority: medium
state_hub_task_id: "8ea87d5f-883a-5d63-89bb-33a3d0963472"
```
tenant-engine split the actions so a PDP can read ceilings without being
able to change them. Decide whether that split is used now or only reserved.
Record the decision in `policy_package.md`. Using it now — `flex-auth` may
read, `tenant-engine` may read and set, nobody else — is the expected
outcome unless a concrete reason says otherwise. Inventing an `ops` write
subject is out of scope; writes stay on the existing service identity.
Done when the decision and its reasoning are recorded and implemented.
Done 2026-08-16: **yes — use the split now.** Recorded under "Decision:
use the read/write split now" in
`examples/tenant-engine/policy_package.md`. `flex-auth` may
`tenant.guardrail.read` and is denied `action_not_granted` on
`tenant.guardrail.set`. `tenant-engine` may do both. No `ops` write
subject.
## T03 - Fixtures and verification
```task
id: FLEX-WP-0014-T03
status: done
priority: high
state_hub_task_id: "1029d2ce-bb37-530e-91ef-ded86f6f5be8"
```
Add fixture pairs for: authorized `tenant-engine` read and set → allow;
authorized `flex-auth` read → allow; `flex-auth` set → deny
`action_not_granted`; unknown subject → deny `unknown_subject`; misspelled
guardrail action → deny `unknown_action`.
Verify against the real binary: `test-policy`, `load-registry`, `check` on
standalone request files. Then close the loop with a live `flex-auth serve`
and a real `tenant-engine` on `TENANT_ENGINE_FLEX_AUTH_URL`, through
unmodified `FlexAuthWriteAuthorizer`:
- `GET /tenants/{id}/guardrails?actor=flex-auth` → 200
- `GET ...?actor=ops` → 403
- `PUT ...` as `actor=tenant-engine` → 200
- `PUT ...` as `actor=flex-auth` → 403
- `PUT ...` as `actor=ops` → 403
Done when those five calls match, `go test ./...` is green, and
`gofmt`/`go vet` are clean.
Done 2026-08-16: 17/17 Rego tests, 23/23 fixtures. `load-registry` loads
clean. Standalone `check` files return the expected allow/deny.
Live `flex-auth serve` on `127.0.0.1:9098` + real `tenant-engine` on
`TENANT_ENGINE_FLEX_AUTH_URL`:
| Call | tenant-engine | flex-auth |
|---|---|---|
| `POST /tenants` actor=`tenant-engine` | 201 | allow |
| `GET .../guardrails?actor=flex-auth` | 200 `spend.monthly=25000` grouping | allow |
| `GET ...?actor=ops` | 403 `write_denied` | deny `unknown_subject` |
| `PUT .../spend.monthly` actor=`tenant-engine` | 200 override `9000` v2 | allow |
| `PUT ...` actor=`flex-auth` | 403 `write_denied` | deny `action_not_granted` |
| `PUT ...` actor=`ops` | 403 `write_denied` | deny `unknown_subject` |
`go test ./...` green; `gofmt -l` empty; `go vet ./...` clean.
## T04 - Closure and handoff
```task
id: FLEX-WP-0014-T04
status: done
priority: low
state_hub_task_id: "b2b44215-ef08-5c50-b466-190156a88ef3"
```
Confirm T01T03. Notify `tenant-engine` naming the policy revision. State
plainly whether the nine-action package is **in source** or **deployed**
FLEX-WP-0010 taught that distinction the hard way. Production rollout of a
new image is not implied by this workplan; it is a separate pin after CI
publishes a digest.
Done when the handoff is sent and this workplan is `finished`.
Done 2026-08-16: T01T03 confirmed. Policy revision for the handoff is
package `tenant-engine.write-api.mutate` **version `v1`**, now carrying
all nine actions. The version string is unchanged because the package is
additive.
**In source, not in the cluster.** Production `flex-auth-tenant-engine`
still serves `sha256:9320df39` (seven actions). A CI-built image from
this commit plus a pin of `flex-auth-tenant-engine` is the remaining
step before a live guardrail check succeeds against the cluster Service.
tenant-engine's own TEN-WP-0006 surface is also not deployed, so the
cluster path is not yet the binding one.
Deployed 2026-08-16 after operator "sync and commit / deploy flex-auth":
CI image `main-f304688` /
`sha256:1bf060e61122693ce98359c167cc5fe8bdafc84e097e090eaa71af94d0f27cbc`
is live on `flex-auth-tenant-engine`. Approval
`8d1efcea-93c7-436b-a749-e5d464e94a33`. Rollback target remains
`sha256:9320df39`. `flex-auth-user-engine` stayed on `sha256:1f529037`.
Live `/v1/check` against the Service:
| Request | Effect | Decision |
| --- | --- | --- |
| `tenant.create` | allow `write_api_policy_matched` | `decision:ef18e8bbd2dead6a` |
| `tenant.retire` | allow `write_api_policy_matched` | `decision:996663f6902d0762` |
| `tenant.guardrail.read` as flex-auth | allow `write_api_policy_matched` | `decision:9b9e540de13cf7cf` |
| `tenant.guardrail.set` as tenant-engine | allow `write_api_policy_matched` | `decision:20c82bc38501a343` |
| `tenant.guardrail.set` as flex-auth | deny `action_not_granted` | `decision:a057a7030df65208` |
| misspelled guardrail | deny `unknown_action` | `decision:18ae70a79c6d47c6` |
| unknown subject | deny `unknown_subject` | `decision:a98c59c6a34f84ad` |
| misspelled lifecycle | deny `unknown_action` | `decision:1b3012abb4e2b27d` |
TEN-WP-0007 can pin the tenant-engine guardrail image. The seven
pre-existing actions still allow.