Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
16 KiB
| id | name | namespace | version | status | package | actions | owner | fixtures | caring | activation | metadata | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| tenant-engine.write-api.mutate | tenant-engine Write API authorization | tenant-engine:tenant | v2 | ready | flexauth.tenant_engine.write_api |
|
team:platform-security |
|
|
|
|
tenant-engine Write API authorization
This package authorizes tenant-engine's write API
(TEN-WP-0003's authz.FlexAuthWriteAuthorizer). tenant-engine keeps
custody of tenant records, role-grant audit trails, and plan assignments;
flex-auth decides whether a specific write is allowed now.
Scope note: this policy governs who may call tenant-engine's admin
API (an operator/service-identity question) — it does not evaluate a
tenant's capability roles (PLTF/IAM/VEN/CUS, ADR-0014). Those are
tenant state a different protected system's policy might consult via
tenant-engine's live-lookup endpoint (FLEX-WP-0008-T03); conflating the
two would authorize the wrong thing.
Lifecycle actions (FLEX-WP-0010)
tenant.update, tenant.retire, and tenant.reactivate gate
tenant-engine's lifecycle endpoints (PATCH /tenants/{id},
POST /tenants/{id}/retire, POST /tenants/{id}/reactivate). The action
strings are the exact values authz.FlexAuthWriteAuthorizer sends; they are
coordinated between the two repos, not independently chosen here.
They are three actions rather than one tenant.write because tenant-engine
deliberately separated them so policy can one day permit renaming a tenant
without permitting retiring it. Under the current model that separation is a
seam, not a difference — see the decision below.
Decision: tenant.retire does not require stricter authorization yet
Decision (FLEX-WP-0010-T02, 2026-08-10): no. tenant.retire is
authorized by the same rule as the other six actions.
Reasoning:
- There is nothing stricter to check. The subject set is exactly one
service identity (
tenant-engine), there is no second operator to distinguish from, and requests carry noassuranceclaim — theCheckRequestshape from FLEX-WP-0008 has no field that could differentiate retirement from an update. A "stricter" rule today could only be stricter in name. - A rule that cannot fail is worse than no rule. Adding a condition that the single known caller always satisfies would read, to a later reviewer, as though retirement were separately controlled when it is not. That is a false assurance in a policy package whose job is to be inspectable.
- The blast radius is recoverable. Retirement is reversible by design
via
tenant.reactivate, and tenant-engine hard-deletes nothing. A wrongly allowed retirement suspends new grants and plan changes until reactivated; it does not destroy tenant state. - The seam is already cut. Because the three actions are distinct
strings in
valid_actions, tighteningtenant.retirelater is an additive change to this package — no consumer change, no request-shape change, no coordination round with tenant-engine.
Revisit when KEY-WP-0005 gives callers an assurance-bearing identity,
or when a second operator subject is registered against
system: "tenant-engine" — whichever comes first. At that point the honest
options are an assurance floor on tenant.retire or a distinct
group:tenant-engine-lifecycle membership requirement; both are one rule in
allowed plus a first_denial branch.
Guardrail actions (FLEX-WP-0014)
tenant.guardrail.read and tenant.guardrail.set gate tenant-engine's
ceiling surface (GET /tenants/{id}/guardrails,
PUT/DELETE /tenants/{id}/guardrails/{limit_key}). The strings are the
exact values authz.FlexAuthWriteAuthorizer sends. Resource type is
guardrail; resource.id is still the tenant id.
They are two actions because a PDP must be able to read ceilings without being able to change them. tenant-engine is a data source, never a decision maker; flex-auth is the intended reader.
Decision: use the read/write split now
Decision (FLEX-WP-0014-T02, 2026-08-16): yes. flex-auth may
tenant.guardrail.read and may not tenant.guardrail.set.
tenant-engine may do both. Nobody else may do either.
Reasoning:
- The second subject exists and is named. TEN-WP-0006 split the
actions so "policy can grant you [flex-auth] the read without granting
anything the write." Their own HTTP tests call the read as
actor=flex-auth. Leaving both actions on the singletenant-enginesubject would keep the seam unused and leave the intended reader failingunknown_subject. - This is a real difference, not a named no-op. Unlike
FLEX-WP-0010-T02, there is a second service identity to distinguish
from, and a check the reader can fail (
tenant.guardrail.setasflex-auth→action_not_granted). - Do not invent an
opswrite subject. Production writes already authorize only thetenant-engineservice identity. Guardrail mutations stay on that same identity. Addingopswould widen the write set without a registered operator. - Do not grant flex-auth any mutate action. A PDP that can raise a ceiling is no longer only a decision maker.
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
import future.keywords.contains
import future.keywords.if
import future.keywords.in
valid_actions := {
"tenant.read",
"tenant.create",
"tenant.role.grant",
"tenant.role.revoke",
"tenant.plan.assign",
"tenant.update",
"tenant.retire",
"tenant.reactivate",
"tenant.guardrail.read",
"tenant.guardrail.set",
}
read_actions := {"tenant.guardrail.read"}
mutate_actions := valid_actions - read_actions - {"tenant.read"}
known_subjects := {"tenant-engine", "flex-auth", "user-engine"}
read_subjects := {"tenant-engine", "flex-auth"}
mutate_subjects := {"tenant-engine"}
decision := {"effect": "allow", "reason": "write_api_policy_matched"} if {
allowed
} else := {"effect": "deny", "reason": first_denial} if {
true
}
allowed if {
input.resource.system == "tenant-engine"
input.action in read_actions
input.subject.type == "service"
input.subject.id in read_subjects
}
allowed if {
input.resource.system == "tenant-engine"
input.action in mutate_actions
input.subject.type == "service"
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 {
input.resource.system != "tenant-engine"
} else := "unknown_action" if {
not input.action in valid_actions
} else := "wrong_subject_type" if {
input.subject.type != "service"
} else := "unknown_subject" if {
not input.subject.id in known_subjects
} else := "action_not_granted" if {
input.subject.id in known_subjects
}
Tests
package flexauth.tenant_engine.write_api_test
import future.keywords.if
import data.flexauth.tenant_engine.write_api
base_request := {
"id": "check:tenant-engine-create",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
}
test_known_operator_create_allowed if {
write_api.decision.effect == "allow" with input as base_request
}
test_role_grant_allowed if {
write_api.decision.effect == "allow" with input as {
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.role.grant",
"resource": {"id": "t-1", "type": "role-grant", "system": "tenant-engine"}
}
}
test_tenant_update_allowed if {
write_api.decision.effect == "allow" with input as {
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.update",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
}
}
test_tenant_retire_allowed if {
write_api.decision.effect == "allow" with input as {
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.retire",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
}
}
test_tenant_reactivate_allowed if {
write_api.decision.effect == "allow" with input as {
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.reactivate",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
}
}
test_misspelled_lifecycle_action_denied if {
write_api.decision.reason == "unknown_action" with input as {
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.retired",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
}
}
test_unknown_subject_retire_denied if {
write_api.decision.reason == "unknown_subject" with input as {
"subject": {"id": "some-other-service", "type": "service"},
"action": "tenant.retire",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
}
}
test_unknown_subject_denied if {
write_api.decision.reason == "unknown_subject" with input as {
"subject": {"id": "some-other-service", "type": "service"},
"action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
}
}
test_wrong_system_denied if {
write_api.decision.reason == "wrong_system" with input as {
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "some-other-system"}
}
}
test_unknown_action_denied if {
write_api.decision.reason == "unknown_action" with input as {
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.delete",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
}
}
test_wrong_subject_type_denied if {
write_api.decision.reason == "wrong_subject_type" with input as {
"subject": {"id": "tenant-engine", "type": "human"},
"action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
}
}
test_guardrail_read_by_pdp_allowed if {
write_api.decision.effect == "allow" with input as {
"subject": {"id": "flex-auth", "type": "service"},
"action": "tenant.guardrail.read",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}
}
}
test_guardrail_read_by_writer_allowed if {
write_api.decision.effect == "allow" with input as {
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.guardrail.read",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}
}
}
test_guardrail_set_by_writer_allowed if {
write_api.decision.effect == "allow" with input as {
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.guardrail.set",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}
}
}
test_guardrail_set_by_pdp_denied if {
write_api.decision.reason == "action_not_granted" with input as {
"subject": {"id": "flex-auth", "type": "service"},
"action": "tenant.guardrail.set",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}
}
}
test_guardrail_read_unknown_subject_denied if {
write_api.decision.reason == "unknown_subject" with input as {
"subject": {"id": "ops", "type": "service"},
"action": "tenant.guardrail.read",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}
}
}
test_misspelled_guardrail_action_denied if {
write_api.decision.reason == "unknown_action" with input as {
"subject": {"id": "flex-auth", "type": "service"},
"action": "tenant.guardrail.get",
"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"}}
}