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": {}
|
"context": {}
|
||||||
},
|
},
|
||||||
"expect": {"effect": "deny", "reason": "unknown_action"}
|
"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
|
id: tenant-engine.write-api.mutate
|
||||||
name: tenant-engine Write API authorization
|
name: tenant-engine Write API authorization
|
||||||
namespace: tenant-engine:tenant
|
namespace: tenant-engine:tenant
|
||||||
version: v1
|
version: v2
|
||||||
status: ready
|
status: ready
|
||||||
package: flexauth.tenant_engine.write_api
|
package: flexauth.tenant_engine.write_api
|
||||||
actions:
|
actions:
|
||||||
|
- tenant.read
|
||||||
- tenant.create
|
- tenant.create
|
||||||
- tenant.role.grant
|
- tenant.role.grant
|
||||||
- tenant.role.revoke
|
- tenant.role.revoke
|
||||||
|
|
@ -157,6 +158,26 @@ Reasoning:
|
||||||
**Revisit when** a second human/operator subject is registered, or when
|
**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`.
|
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
|
## Rules
|
||||||
|
|
||||||
```rego
|
```rego
|
||||||
|
|
@ -165,6 +186,7 @@ import future.keywords.if
|
||||||
import future.keywords.in
|
import future.keywords.in
|
||||||
|
|
||||||
valid_actions := {
|
valid_actions := {
|
||||||
|
"tenant.read",
|
||||||
"tenant.create",
|
"tenant.create",
|
||||||
"tenant.role.grant",
|
"tenant.role.grant",
|
||||||
"tenant.role.revoke",
|
"tenant.role.revoke",
|
||||||
|
|
@ -178,9 +200,9 @@ valid_actions := {
|
||||||
|
|
||||||
read_actions := {"tenant.guardrail.read"}
|
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"}
|
read_subjects := {"tenant-engine", "flex-auth"}
|
||||||
|
|
||||||
|
|
@ -206,6 +228,23 @@ allowed if {
|
||||||
input.subject.id in mutate_subjects
|
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"
|
default first_denial := "no_matching_rule"
|
||||||
|
|
||||||
first_denial := "wrong_system" if {
|
first_denial := "wrong_system" if {
|
||||||
|
|
@ -368,4 +407,44 @@ test_misspelled_guardrail_action_denied if {
|
||||||
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}
|
"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": {
|
"metadata": {
|
||||||
"required_context": []
|
"required_context": []
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tenant.read",
|
||||||
|
"capabilities": [
|
||||||
|
"View",
|
||||||
|
"Audit"
|
||||||
|
],
|
||||||
|
"planes": [
|
||||||
|
"Identity",
|
||||||
|
"Audit"
|
||||||
|
],
|
||||||
|
"exposure_modes": [
|
||||||
|
"Metadata"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"required_context": []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"caring_profiles": [
|
"caring_profiles": [
|
||||||
|
|
@ -240,7 +257,7 @@
|
||||||
],
|
],
|
||||||
"tenant": "tenant:platform",
|
"tenant": "tenant:platform",
|
||||||
"metadata": {
|
"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": {
|
"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)."
|
"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": [
|
"groups": [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue