diff --git a/.claude/RESUME.md b/.claude/RESUME.md new file mode 100644 index 0000000..6834cd6 --- /dev/null +++ b/.claude/RESUME.md @@ -0,0 +1,23 @@ +# Claude Code — resume checkpoint + +Session: 20dd5562-9f87-4ec7-9e89-0182ad59f4fa +Written: 2026-08-18T13:25:17.115Z +Trigger: rate-limited +Ref with your in-progress files: refs/claude/checkpoint-20dd5562 + +## To resume + + claude --resume 20dd5562-9f87-4ec7-9e89-0182ad59f4fa + +(or open Claude Code in this directory and run /resume) + +## Plan (from TodoWrite state) + +No task list was active; see transcript via the resume command above. + +--- + +Don't want these changes? Resume this session (above), then run +`/rewind` to roll back the turn's tool edits (bash-made changes +excluded). refs/claude/checkpoint-20dd5562 holds a full snapshot until this session's +next checkpoint, or for up to ~2 weeks. diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 9bf8b73..8c2b2f1 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -90,7 +90,7 @@ | task | FLEX-WP-0014-T03 | done | — | workplans/FLEX-WP-0014-tenant-guardrail-policy-actions.md | | task | FLEX-WP-0014-T04 | done | — | workplans/FLEX-WP-0014-tenant-guardrail-policy-actions.md | | task | FLEX-WP-0015-T01 | done | — | workplans/FLEX-WP-0015-tenancy-posture-conformance.md | -| task | FLEX-WP-0015-T02 | progress | — | workplans/FLEX-WP-0015-tenancy-posture-conformance.md | +| task | FLEX-WP-0015-T02 | wait | — | workplans/FLEX-WP-0015-tenancy-posture-conformance.md | | task | FLEX-WP-0015-T03 | done | — | workplans/FLEX-WP-0015-tenancy-posture-conformance.md | | task | FLEX-WP-0015-T04 | wait | — | workplans/FLEX-WP-0015-tenancy-posture-conformance.md | | task | FLEX-WP-0015-T05 | done | — | workplans/FLEX-WP-0015-tenancy-posture-conformance.md | diff --git a/docs/tenant-engine-action-vocabulary.md b/docs/tenant-engine-action-vocabulary.md index 1290734..bbd1537 100644 --- a/docs/tenant-engine-action-vocabulary.md +++ b/docs/tenant-engine-action-vocabulary.md @@ -6,11 +6,14 @@ flex-auth protected system (`FLEX-WP-0008-T01`). Action names here match (`tenant_engine/src/tenant_engine/authz.py`) — coordinated between the two repos, not independently invented. -| Action | tenant-engine write endpoint | Resource type | Decision effects | +| Action | tenant-engine endpoint | Resource type | Decision effects | | --- | --- | --- | --- | +| `tenant.read` | `GET /tenants/{id}` | `tenant` | `allow`, `deny` | | `tenant.create` | `POST /tenants` | `tenant` | `allow`, `deny` | | `tenant.role.grant` | `POST /tenants/{id}/roles/grant` | `role-grant` | `allow`, `deny` | | `tenant.role.revoke` | `POST /tenants/{id}/roles/revoke` | `role-grant` | `allow`, `deny` | +| `tenant.role.read` | `GET /tenants/{id}/roles` | `role-grant` | `allow`, `deny` | +| `tenant.role.read.live` | `GET /tenants/{id}/roles/live` | `role-grant` | `allow`, `deny` | | `tenant.plan.assign` | `POST /tenants/{id}/plan` | `plan-assignment` | `allow`, `deny` | | `tenant.update` | `PATCH /tenants/{id}` | `tenant` | `allow`, `deny` | | `tenant.retire` | `POST /tenants/{id}/retire` | `tenant` | `allow`, `deny` | @@ -26,12 +29,12 @@ without granting the write. That split is used today: `flex-auth` may may do both. Reasoning is recorded in `examples/tenant-engine/policy_package.md`. -These are the only nine actions `tenant-engine` sends to `POST /v1/check` — +These are the only twelve actions `tenant-engine` sends to `POST /v1/check` — its `FlexAuthCheckClient` (`TEN-WP-0003`) never sends anything else. -`tenant-engine`'s role reads (`/roles`, `/roles/live`) are still not gated -through flex-auth; they remain `tenant-engine`'s own cache-read and -live-lookup endpoints. The guardrail GET is different: it is authorized, -because a ceiling is an input to a decision, not an unauthenticated lookup. +Every tenant and role read is gated before the store is touched, preventing +both unauthorized data access and tenant-existence probing. `user-engine` +may read tenant records, `flex-auth` may perform the high-stakes live role +lookup, and no caller receives a role-read privilege through a mutation rule. ## Not in scope for this vocabulary diff --git a/examples/tenant-engine/policy_fixtures.yaml b/examples/tenant-engine/policy_fixtures.yaml index 9e740ae..da6c873 100644 --- a/examples/tenant-engine/policy_fixtures.yaml +++ b/examples/tenant-engine/policy_fixtures.yaml @@ -1,4 +1,40 @@ [ + { + "id": "fixture:user-engine-tenant-read-allow", + "request": { + "id": "check:user-engine-tenant-read-t1", + "tenant": "tenant:friendly:binky", + "subject": {"id": "user-engine", "type": "service"}, + "action": "tenant.read", + "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, + "context": {} + }, + "expect": {"effect": "allow", "reason": "write_api_policy_matched"} + }, + { + "id": "fixture:flex-auth-live-role-read-allow", + "request": { + "id": "check:flex-auth-live-role-read-t1", + "tenant": "tenant:friendly:binky", + "subject": {"id": "flex-auth", "type": "service"}, + "action": "tenant.role.read.live", + "resource": {"id": "t-1", "type": "role-grant", "system": "tenant-engine"}, + "context": {} + }, + "expect": {"effect": "allow", "reason": "write_api_policy_matched"} + }, + { + "id": "fixture:user-engine-live-role-read-deny", + "request": { + "id": "check:user-engine-live-role-read-t1", + "tenant": "tenant:friendly:binky", + "subject": {"id": "user-engine", "type": "service"}, + "action": "tenant.role.read.live", + "resource": {"id": "t-1", "type": "role-grant", "system": "tenant-engine"}, + "context": {} + }, + "expect": {"effect": "deny", "reason": "action_not_granted"} + }, { "id": "fixture:tenant-engine-create-allow", "request": { diff --git a/examples/tenant-engine/policy_package.md b/examples/tenant-engine/policy_package.md index 1bccd44..15eb018 100644 --- a/examples/tenant-engine/policy_package.md +++ b/examples/tenant-engine/policy_package.md @@ -6,9 +6,12 @@ version: v1 status: ready package: flexauth.tenant_engine.write_api actions: + - tenant.read - tenant.create - tenant.role.grant - tenant.role.revoke + - tenant.role.read + - tenant.role.read.live - tenant.plan.assign - tenant.update - tenant.retire @@ -165,9 +168,12 @@ import future.keywords.if import future.keywords.in valid_actions := { + "tenant.read", "tenant.create", "tenant.role.grant", "tenant.role.revoke", + "tenant.role.read", + "tenant.role.read.live", "tenant.plan.assign", "tenant.update", "tenant.retire", @@ -176,15 +182,25 @@ valid_actions := { "tenant.guardrail.set", } -read_actions := {"tenant.guardrail.read"} +tenant_read_actions := {"tenant.read", "tenant.role.read"} + +live_read_actions := {"tenant.role.read.live"} + +guardrail_read_actions := {"tenant.guardrail.read"} + +read_actions := tenant_read_actions | live_read_actions | guardrail_read_actions mutate_actions := valid_actions - read_actions -known_subjects := {"tenant-engine", "flex-auth"} +known_subjects := {"tenant-engine", "user-engine", "flex-auth"} -read_subjects := {"tenant-engine", "flex-auth"} +tenant_read_subjects := {"tenant-engine", "user-engine"} -mutate_subjects := {"tenant-engine"} +live_read_subjects := {"flex-auth"} + +guardrail_read_subjects := {"tenant-engine", "flex-auth"} + +mutate_subjects := {"tenant-engine", "user-engine"} decision := {"effect": "allow", "reason": "write_api_policy_matched"} if { allowed @@ -194,9 +210,23 @@ decision := {"effect": "allow", "reason": "write_api_policy_matched"} if { allowed if { input.resource.system == "tenant-engine" - input.action in read_actions + input.action in tenant_read_actions input.subject.type == "service" - input.subject.id in read_subjects + input.subject.id in tenant_read_subjects +} + +allowed if { + input.resource.system == "tenant-engine" + input.action in live_read_actions + input.subject.type == "service" + input.subject.id in live_read_subjects +} + +allowed if { + input.resource.system == "tenant-engine" + input.action in guardrail_read_actions + input.subject.type == "service" + input.subject.id in guardrail_read_subjects } allowed if { diff --git a/examples/tenant-engine/protected_system_manifest.yaml b/examples/tenant-engine/protected_system_manifest.yaml index 958a94f..7542c74 100644 --- a/examples/tenant-engine/protected_system_manifest.yaml +++ b/examples/tenant-engine/protected_system_manifest.yaml @@ -33,6 +33,17 @@ resource_types: A tenant's resolved ceiling (spend, seats, and similar limits) plus the provenance of that value. Not an entitlement and not a meter. actions: + - name: tenant.read + capabilities: + - View + - Audit + planes: + - Identity + - Audit + exposure_modes: + - Metadata + metadata: + required_context: [] - name: tenant.create capabilities: - Create @@ -68,6 +79,30 @@ actions: - Metadata metadata: required_context: [] + - name: tenant.role.read + capabilities: + - View + - Audit + planes: + - Identity + - Policy + - Audit + exposure_modes: + - Metadata + metadata: + required_context: [] + - name: tenant.role.read.live + capabilities: + - View + - Audit + planes: + - Identity + - Policy + - Audit + exposure_modes: + - Metadata + metadata: + required_context: [] - name: tenant.plan.assign capabilities: - Bind diff --git a/examples/tenant-engine/registry_snapshot.json b/examples/tenant-engine/registry_snapshot.json index de838b6..a48e662 100644 --- a/examples/tenant-engine/registry_snapshot.json +++ b/examples/tenant-engine/registry_snapshot.json @@ -51,6 +51,23 @@ } ], "actions": [ + { + "name": "tenant.read", + "capabilities": [ + "View", + "Audit" + ], + "planes": [ + "Identity", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [] + } + }, { "name": "tenant.create", "capabilities": [ @@ -104,6 +121,42 @@ "required_context": [] } }, + { + "name": "tenant.role.read", + "capabilities": [ + "View", + "Audit" + ], + "planes": [ + "Identity", + "Policy", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [] + } + }, + { + "name": "tenant.role.read.live", + "capabilities": [ + "View", + "Audit" + ], + "planes": [ + "Identity", + "Policy", + "Audit" + ], + "exposure_modes": [ + "Metadata" + ], + "metadata": { + "required_context": [] + } + }, { "name": "tenant.plan.assign", "capabilities": [ @@ -258,6 +311,23 @@ "metadata": { "description": "flex-auth's own service identity. Authorized only for tenant.guardrail.read so the PDP can consume ceilings without being able to change them (FLEX-WP-0014)." } + }, + { + "id": "user-engine", + "type": "Service", + "display_name": "user-engine service principal", + "organization_relation": "ServiceProvider", + "roles": [ + "Operator" + ], + "groups": [ + "group:tenant-engine-writers", + "group:tenant-engine-readers" + ], + "tenant": "tenant:platform", + "metadata": { + "description": "user-engine's service identity. It owns the portal workflow that reads and mutates tenant lifecycle records through tenant-engine." + } } ], "groups": [ @@ -265,7 +335,8 @@ "id": "group:tenant-engine-writers", "display_name": "tenant-engine Write API Callers", "members": [ - "tenant-engine" + "tenant-engine", + "user-engine" ], "tenant": "tenant:platform" }, @@ -274,7 +345,8 @@ "display_name": "tenant-engine Guardrail Readers", "members": [ "flex-auth", - "tenant-engine" + "tenant-engine", + "user-engine" ], "tenant": "tenant:platform" } diff --git a/examples/tenant-engine/subject_manifest.yaml b/examples/tenant-engine/subject_manifest.yaml index 987add3..1118d56 100644 --- a/examples/tenant-engine/subject_manifest.yaml +++ b/examples/tenant-engine/subject_manifest.yaml @@ -21,6 +21,20 @@ subjects: tenant.role.revoke, tenant.plan.assign, the lifecycle actions tenant.update, tenant.retire, tenant.reactivate, and the guardrail actions tenant.guardrail.read, tenant.guardrail.set. + - id: user-engine + type: Service + display_name: user-engine service principal + organization_relation: ServiceProvider + roles: + - Operator + groups: + - group:tenant-engine-writers + - group:tenant-engine-readers + tenant: tenant:platform + metadata: + description: >- + user-engine's service identity. It owns the portal workflow that reads + and mutates tenant lifecycle records through tenant-engine. - id: flex-auth type: Service display_name: flex-auth service principal @@ -40,10 +54,12 @@ groups: display_name: tenant-engine Write API Callers members: - tenant-engine + - user-engine tenant: tenant:platform - id: group:tenant-engine-readers display_name: tenant-engine Guardrail Readers members: - flex-auth - tenant-engine + - user-engine tenant: tenant:platform