fix: authorize native user portal tenant onboarding
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 57s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-11 20:52:46 +02:00
parent 88b354377c
commit dd8dd51743
3 changed files with 378 additions and 4 deletions

View file

@ -274,5 +274,269 @@
"context": {}
},
"expect": {"effect": "deny", "reason": "unknown_action"}
},
{
"id": "fixture:tenant-engine-portal-create",
"request": {
"id": "check:portal-create",
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "user-engine",
"type": "service"
},
"action": "tenant.create",
"resource": {
"id": "tenant:trial:demo-company",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-portal-read",
"request": {
"id": "check:portal-read",
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "user-engine",
"type": "service"
},
"action": "tenant.read",
"resource": {
"id": "tenant:trial:demo-company",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-portal-platform-create",
"request": {
"id": "check:portal-platform-create",
"tenant": "tenant:platform",
"subject": {
"id": "user-engine",
"type": "service"
},
"action": "tenant.create",
"resource": {
"id": "tenant:platform",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "deny",
"reason": "action_not_granted"
}
},
{
"id": "fixture:tenant-engine-portal-platform-read",
"request": {
"id": "check:portal-platform-read",
"tenant": "tenant:platform",
"subject": {
"id": "user-engine",
"type": "service"
},
"action": "tenant.read",
"resource": {
"id": "tenant:platform",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "deny",
"reason": "action_not_granted"
}
},
{
"id": "fixture:tenant-engine-portal-role-grant",
"request": {
"id": "check:portal-role-grant",
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "user-engine",
"type": "service"
},
"action": "tenant.role.grant",
"resource": {
"id": "tenant:trial:demo-company",
"type": "role-grant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "deny",
"reason": "action_not_granted"
}
},
{
"id": "fixture:tenant-engine-portal-role-revoke",
"request": {
"id": "check:portal-role-revoke",
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "user-engine",
"type": "service"
},
"action": "tenant.role.revoke",
"resource": {
"id": "tenant:trial:demo-company",
"type": "role-grant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "deny",
"reason": "action_not_granted"
}
},
{
"id": "fixture:tenant-engine-portal-plan-assign",
"request": {
"id": "check:portal-plan-assign",
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "user-engine",
"type": "service"
},
"action": "tenant.plan.assign",
"resource": {
"id": "tenant:trial:demo-company",
"type": "plan-assignment",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "deny",
"reason": "action_not_granted"
}
},
{
"id": "fixture:tenant-engine-portal-update",
"request": {
"id": "check:portal-update",
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "user-engine",
"type": "service"
},
"action": "tenant.update",
"resource": {
"id": "tenant:trial:demo-company",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "deny",
"reason": "action_not_granted"
}
},
{
"id": "fixture:tenant-engine-portal-retire",
"request": {
"id": "check:portal-retire",
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "user-engine",
"type": "service"
},
"action": "tenant.retire",
"resource": {
"id": "tenant:trial:demo-company",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "deny",
"reason": "action_not_granted"
}
},
{
"id": "fixture:tenant-engine-portal-reactivate",
"request": {
"id": "check:portal-reactivate",
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "user-engine",
"type": "service"
},
"action": "tenant.reactivate",
"resource": {
"id": "tenant:trial:demo-company",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "deny",
"reason": "action_not_granted"
}
},
{
"id": "fixture:tenant-engine-portal-guardrail-read",
"request": {
"id": "check:portal-guardrail-read",
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "user-engine",
"type": "service"
},
"action": "tenant.guardrail.read",
"resource": {
"id": "tenant:trial:demo-company",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "deny",
"reason": "action_not_granted"
}
},
{
"id": "fixture:tenant-engine-portal-guardrail-set",
"request": {
"id": "check:portal-guardrail-set",
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "user-engine",
"type": "service"
},
"action": "tenant.guardrail.set",
"resource": {
"id": "tenant:trial:demo-company",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "deny",
"reason": "action_not_granted"
}
}
]

View file

@ -2,10 +2,11 @@
id: tenant-engine.write-api.mutate
name: tenant-engine Write API authorization
namespace: tenant-engine:tenant
version: v1
version: v2
status: ready
package: flexauth.tenant_engine.write_api
actions:
- tenant.read
- tenant.create
- tenant.role.grant
- tenant.role.revoke
@ -157,6 +158,26 @@ Reasoning:
**Revisit when** a second human/operator subject is registered, or when
flex-auth itself needs to read as a different subject id than `flex-auth`.
## User portal onboarding (NK-WP-0036, 2026-09-11)
The native User Engine platform portal is the existing tenant-onboarding PEP.
It authenticates the human through KeyCape and checks `tenant:platform` plus
`platform-operator` before calling Tenant Engine as `user-engine`. Register
that real service subject for `tenant.create` and `tenant.read` on non-platform
`tenant` resources only. No role grants, lifecycle mutations, plan assignment
or guardrail changes are granted to it. Tenant Engine's own read is also
registered because its shipped read endpoint now calls `tenant.read`.
This repairs the demonstrated native demo-company onboarding refusal; it does
not rename the caller to tenant-engine or weaken inbound caller authentication.
The PDP's authenticated caller remains the exact Tenant Engine ServiceAccount,
bound to resource.system tenant-engine. Tenant Engine's namespace-and-pod ingress
continues to admit only User Engine. Human operator entitlement stays with the
portal's existing PEP; no client-supplied human assurance is invented here.
The earlier single-write-subject decisions above describe their dated baseline;
this explicit minimal onboarding grant supersedes that baseline only for these
operations and this existing service integration.
## Rules
```rego
@ -165,6 +186,7 @@ import future.keywords.if
import future.keywords.in
valid_actions := {
"tenant.read",
"tenant.create",
"tenant.role.grant",
"tenant.role.revoke",
@ -178,9 +200,9 @@ valid_actions := {
read_actions := {"tenant.guardrail.read"}
mutate_actions := valid_actions - read_actions
mutate_actions := valid_actions - read_actions - {"tenant.read"}
known_subjects := {"tenant-engine", "flex-auth"}
known_subjects := {"tenant-engine", "flex-auth", "user-engine"}
read_subjects := {"tenant-engine", "flex-auth"}
@ -206,6 +228,23 @@ allowed if {
input.subject.id in mutate_subjects
}
allowed if {
input.resource.system == "tenant-engine"
input.resource.type == "tenant"
input.resource.id != "tenant:platform"
input.action in {"tenant.create", "tenant.read"}
input.subject.type == "service"
input.subject.id == "user-engine"
}
allowed if {
input.resource.system == "tenant-engine"
input.resource.type == "tenant"
input.action == "tenant.read"
input.subject.type == "service"
input.subject.id == "tenant-engine"
}
default first_denial := "no_matching_rule"
first_denial := "wrong_system" if {
@ -368,4 +407,44 @@ test_misspelled_guardrail_action_denied if {
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}
}
}
test_portal_create if {
write_api.decision.effect == "allow" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.create", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
}
test_portal_read if {
write_api.decision.effect == "allow" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.read", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
}
test_owner_read if {
write_api.decision.effect == "allow" with input as {"subject": {"id": "tenant-engine", "type": "service"}, "action": "tenant.read", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
}
test_portal_platform if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.create", "resource": {"id": "tenant:platform", "type": "tenant", "system": "tenant-engine"}}
}
test_portal_wrong_type if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.create", "resource": {"id": "tenant:trial:demo-company", "type": "guardrail", "system": "tenant-engine"}}
}
test_portal_no_grants if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.role.grant", "resource": {"id": "tenant:trial:demo-company", "type": "role-grant", "system": "tenant-engine"}}
}
test_portal_no_retirement if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.retire", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
}
test_portal_no_guardrail if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.guardrail.set", "resource": {"id": "tenant:trial:demo-company", "type": "guardrail", "system": "tenant-engine"}}
}
test_pdp_no_tenant_read if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "flex-auth", "type": "service"}, "action": "tenant.read", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
}
test_unknown_no_read if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "unknown", "type": "service"}, "action": "tenant.read", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
}
```

View file

@ -207,6 +207,23 @@
"metadata": {
"required_context": []
}
},
{
"name": "tenant.read",
"capabilities": [
"View",
"Audit"
],
"planes": [
"Identity",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"metadata": {
"required_context": []
}
}
],
"caring_profiles": [
@ -240,7 +257,7 @@
],
"tenant": "tenant:platform",
"metadata": {
"description": "tenant-engine's own service identity, used for the nine write-API actions it sends to POST /v1/check (authz.FlexAuthWriteAuthorizer): tenant.create, tenant.role.grant, 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."
"description": "tenant-engine's own service identity, used for tenant.read and the nine original write-API actions it sends to POST /v1/check (authz.FlexAuthWriteAuthorizer): tenant.create, tenant.role.grant, 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."
}
},
{
@ -258,6 +275,20 @@
"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 tenant onboarding",
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
],
"groups": [],
"tenant": "tenant:platform",
"metadata": {
"description": "Existing KeyCape-authenticated platform portal PEP; may create/read non-platform tenant records only. No role, lifecycle, plan or guardrail mutation grant. NK-WP-0036."
}
}
],
"groups": [