flex-auth/examples/tenant-engine/policy_package.md
tegwick dd8dd51743
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
fix: authorize native user portal tenant onboarding
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
2026-09-11 20:52:46 +02:00

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
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
team:platform-security
policy_fixtures.yaml
profile enforce canonical_roles organization_relations scopes planes capabilities exposure_modes conditions restrictions
caring-0.4.0-rc2 false
Operator
ServiceProvider
level id tenant
Platform platform:tenant-engine tenant:platform
Identity
Policy
Audit
Create
Grant
Revoke
Bind
EditAny
Archive
Restore
View
Audit
Metadata
Logged
PrivilegeEscalationBlocked
mode
local
source flex_auth_contract
examples/tenant-engine/policy_package.md protected-system-v0

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:

  1. 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 no assurance claim — the CheckRequest shape from FLEX-WP-0008 has no field that could differentiate retirement from an update. A "stricter" rule today could only be stricter in name.
  2. 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.
  3. 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.
  4. The seam is already cut. Because the three actions are distinct strings in valid_actions, tightening tenant.retire later 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:

  1. 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 single tenant-engine subject would keep the seam unused and leave the intended reader failing unknown_subject.
  2. 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.set as flex-authaction_not_granted).
  3. Do not invent an ops write subject. Production writes already authorize only the tenant-engine service identity. Guardrail mutations stay on that same identity. Adding ops would widen the write set without a registered operator.
  4. 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"}}
}