fix: authorize native user portal tenant onboarding
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
88b354377c
commit
dd8dd51743
3 changed files with 378 additions and 4 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"}}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue