tenant-engine named the relation in TEN-DEC-2026-002: `tenant` is the target tenant record and always equals `resource.id`; the write API is cross-tenant by design and `tenant.guardrail.read` does not differ. v2 carried no tenant rule and a constant fixture tenant, so the deliberate scope and an omitted rule were indistinguishable. tenant-engine.write-api.mutate v3 (FLEX-DEC-2026-016): - allowed requires tenant_is_target; a mismatch or absent tenant is denied tenant_not_target (object.get, so an absent key names the right cause). - the cross-tenant scope is stated in the package and quantified by test_tenant_never_changes_effect over every action, three subjects and four tenants, with guards against passing by denying everything. - fixtures rotate tenant across four tenants; five cross-tenant allows and two tenant_not_target denies added (42 fixtures, 33 tests, all pass). - user-engine's tenant:platform exclusion is named as a fixed-record rule, not a subject/tenant relation, and tested separately. Closes FLEX-WP-0022 (T01, T02 done). Also records TEN-IN-0004 and SECRETS-IN-0002 on FLEX-WP-0020 and acknowledges the GH-DEC-2026-017 replies on FLEX-WP-0030-T04. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 63291@bnt-lap001 Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
23 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 | v3 | 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.
Tenant relation (FLEX-WP-0022-T02, TEN-DEC-2026-002)
Version v3. v2 had no tenant rule and every fixture carried the same
tenant, so a deliberate cross-tenant scope and an omitted rule were
indistinguishable — the FLEX-DEC-2026-008 shape. tenant-engine has now
named the relation in its own record (TEN-DEC-2026-002,
tenant-engine/docs/flex-auth-integration.md); this section states it so a
reviewer can check it, and FLEX-DEC-2026-016 records the version change.
tenantdenotes the target tenant record, not the caller's tenant. tenant-engine verifies no inbound token and holds no caller tenant to send. On the guardrail actions the "tenant the guardrail applies to" is the same record.- Invariant — encoded as a rule. On every check tenant-engine sends,
tenantequalsresource.id(authz.FlexAuthWriteAuthorizercopies onetenant_idonto both). A check where they differ, or wheretenantis absent, did not come from this engine and is deniedtenant_not_target. This is fail-closed and denies nothing tenant-engine sends. - Scope — deliberately cross-tenant, stated here. No action in
valid_actionsis refused on the relationship betweensubjectandtenant. The caller administers tenants: its subjects are platform service identities and the targets are arbitrary tenant records, andtenant.createhas no existing target at check time. Authorization is a service-identity question over(subject.id, action).tenant.guardrail.readdoes not differ and must not: flex-auth calls it while deciding about arbitrary tenants. No rule below comparestenantto the subject; the embedded testtest_tenant_never_changes_effectquantifies that over every action and subject, and the fixtures varytenantso the suite reports on the field. - The one target-dependent rule is not a tenant relation. The
user-engineonboarding grant (NK-WP-0036) excludes the fixed recordtenant:platform. That depends on which record is targeted, not on any relation between the subject and the target, so it is outside the scope statement above and is tested separately (test_portal_tenant_changes_effect_only_on_platform_record).
Revisit when tenant-engine gains a verified inbound identity
(tenancy.yaml gap I): the caller's tenant becomes knowable, and it arrives as
a new field — this one keeps its meaning.
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"}
# TEN-DEC-2026-002: tenant is the target record and always equals resource.id.
# No rule compares tenant to the subject: the scope is cross-tenant by design.
request_tenant := object.get(input, "tenant", "")
tenant_is_target if {
request_tenant != ""
request_tenant == input.resource.id
}
allowed if {
tenant_is_target
granted
}
decision := {"effect": "allow", "reason": "write_api_policy_matched"} if {
allowed
} else := {"effect": "deny", "reason": first_denial} if {
true
}
granted if {
input.resource.system == "tenant-engine"
input.action in read_actions
input.subject.type == "service"
input.subject.id in read_subjects
}
granted if {
input.resource.system == "tenant-engine"
input.action in mutate_actions
input.subject.type == "service"
input.subject.id in mutate_subjects
}
granted 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"
}
granted 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 := "tenant_not_target" if {
not tenant_is_target
} 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.every
import future.keywords.if
import future.keywords.in
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": "tenant:friendly:binky", "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 { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.role.grant",
"resource": {"id": "tenant:friendly:binky", "type": "role-grant", "system": "tenant-engine"}
}
}
test_tenant_update_allowed if {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.update",
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_tenant_retire_allowed if {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.retire",
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_tenant_reactivate_allowed if {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.reactivate",
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_misspelled_lifecycle_action_denied if {
write_api.decision.reason == "unknown_action" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.retired",
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_unknown_subject_retire_denied if {
write_api.decision.reason == "unknown_subject" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "some-other-service", "type": "service"},
"action": "tenant.retire",
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_unknown_subject_denied if {
write_api.decision.reason == "unknown_subject" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "some-other-service", "type": "service"},
"action": "tenant.create",
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_wrong_system_denied if {
write_api.decision.reason == "wrong_system" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.create",
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "some-other-system"}
}
}
test_unknown_action_denied if {
write_api.decision.reason == "unknown_action" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.delete",
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_wrong_subject_type_denied if {
write_api.decision.reason == "wrong_subject_type" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "human"},
"action": "tenant.create",
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_guardrail_read_by_pdp_allowed if {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "flex-auth", "type": "service"},
"action": "tenant.guardrail.read",
"resource": {"id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine"}
}
}
test_guardrail_read_by_writer_allowed if {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.guardrail.read",
"resource": {"id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine"}
}
}
test_guardrail_set_by_writer_allowed if {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.guardrail.set",
"resource": {"id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine"}
}
}
test_guardrail_set_by_pdp_denied if {
write_api.decision.reason == "action_not_granted" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "flex-auth", "type": "service"},
"action": "tenant.guardrail.set",
"resource": {"id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine"}
}
}
test_guardrail_read_unknown_subject_denied if {
write_api.decision.reason == "unknown_subject" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "ops", "type": "service"},
"action": "tenant.guardrail.read",
"resource": {"id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine"}
}
}
test_misspelled_guardrail_action_denied if {
write_api.decision.reason == "unknown_action" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "flex-auth", "type": "service"},
"action": "tenant.guardrail.get",
"resource": {"id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine"}
}
}
test_portal_create if {
write_api.decision.effect == "allow" with input as { "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"}}
}
test_portal_read if {
write_api.decision.effect == "allow" with input as { "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"}}
}
test_owner_read if {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:trial:demo-company","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 { "tenant": "tenant:platform","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 { "tenant": "tenant:trial:demo-company","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 { "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"}}
}
test_portal_no_retirement if {
write_api.decision.effect == "deny" with input as { "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"}}
}
test_portal_no_guardrail if {
write_api.decision.effect == "deny" with input as { "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"}}
}
test_pdp_no_tenant_read if {
write_api.decision.effect == "deny" with input as { "tenant": "tenant:trial:demo-company","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 { "tenant": "tenant:trial:demo-company","subject": {"id": "unknown", "type": "service"}, "action": "tenant.read", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
}
# --- Tenant relation (TEN-DEC-2026-002, FLEX-WP-0022-T02) ---
scope_tenants := {"tenant:platform", "tenant:friendly:binky", "tenant:acme:prod", "tenant:trial:demo-company"}
resource_type_for := {
"tenant.read": "tenant",
"tenant.create": "tenant",
"tenant.role.grant": "role-grant",
"tenant.role.revoke": "role-grant",
"tenant.plan.assign": "plan-assignment",
"tenant.update": "tenant",
"tenant.retire": "tenant",
"tenant.reactivate": "tenant",
"tenant.guardrail.read": "guardrail",
"tenant.guardrail.set": "guardrail",
}
scoped_request(subject, action, t) := {
"tenant": t,
"subject": {"id": subject, "type": "service"},
"action": action,
"resource": {"id": t, "type": resource_type_for[action], "system": "tenant-engine"},
}
effects_for(subject, action, tenants) := {e |
some t in tenants
e := write_api.decision.effect with input as scoped_request(subject, action, t)
}
# Scope: varying the target tenant never changes the effect.
test_tenant_never_changes_effect if {
every subject in {"tenant-engine", "flex-auth", "some-other-service"} {
every action in write_api.valid_actions {
count(effects_for(subject, action, scope_tenants)) == 1
}
}
}
# The quantified test above must not pass by denying everything.
test_cross_tenant_writes_allowed if {
every action in write_api.mutate_actions {
effects_for("tenant-engine", action, scope_tenants) == {"allow"}
}
}
test_pdp_guardrail_read_is_cross_tenant if {
effects_for("flex-auth", "tenant.guardrail.read", scope_tenants) == {"allow"}
}
# user-engine's grant excludes one fixed record; that is not a tenant relation.
test_portal_tenant_changes_effect_only_on_platform_record if {
every action in write_api.valid_actions {
count(effects_for("user-engine", action, scope_tenants - {"tenant:platform"})) == 1
}
effects_for("user-engine", "tenant.create", {"tenant:platform"}) == {"deny"}
}
# Invariant: tenant must equal resource.id.
test_tenant_not_target_denied if {
write_api.decision.reason == "tenant_not_target" with input as {
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.create",
"resource": {"id": "tenant:acme:prod", "type": "tenant", "system": "tenant-engine"}
}
}
test_tenant_absent_denied if {
write_api.decision.reason == "tenant_not_target" with input as {
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.create",
"resource": {"id": "tenant:acme:prod", "type": "tenant", "system": "tenant-engine"}
}
}