flex-auth/workplans/FLEX-WP-0022-tenant-scope-coverage.md
tegwick d98323b2bb
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 41s
fix(secrets-engine): v2 adds the tenant rule v1 never had
secrets-engine.catalog-lane.lifecycle v1 contained no reference to
input.tenant — not in well_formed, not in the denial ladder, not in a
test. A rotate on lane:glas-primary under tenant:coulomb returned allow
against the deployed package (decision:066e629bbf0c0924).

Found while answering glas-harness's tenant-alignment request, which had
asked for wrong-tenant denial evidence. There was none to return.

Three covers failed the same way: every one of the 29 fixtures carried
tenant:platform, so the suite could not report on the field; T02's own
gate named "wrong-tenant deny" and was recorded done unmet; and the
engine hashes tenant into request_digest but never compares it. Four
other published packages carry the branch — this one was the outlier.

v2 adds wrong_tenant above wrong_system, three Rego tests and three
fixtures (28/28, 32/32). The absent-tenant test caught a second defect
in the first draft: a bare input.tenant != comparison is undefined on a
missing key, so the branch dropped and the ladder reported the wrong
rung. request_tenant := object.get(input, "tenant", "") fixes it.

v2 supersedes rather than amends v1 because the defect failed open: a
consumer pinned to _VERSION=v1 would keep receiving allows with no
signal the rule beneath the version string had changed. The earlier
dual-control correction stayed at v1 because it denied everything.

Replay envelopes regenerated at v2; both request_digest values are
byte-identical, so secrets-engine's digest join needs no re-pinning.

The sweep this prompted found tenant-engine unscoped on tenant as well —
deployed, and verified allowing tenant:coulomb. Not the same fix: its
request tenant names the target rather than the caller, so a constant
would break it. Recorded and carried by FLEX-WP-0022 rather than patched
unilaterally.

FLEX-WP-0021 closes at T05; the pin still serves v1 until a redeploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aQMM1dPXaPiXVn6DwwtLd

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715613@bnt-lap001
Assistant-Session: fabd95c1-4c9e-4080-8849-8707ae025f80
2026-09-06 20:38:45 +02:00

3.8 KiB

id type title domain repo status owner topic_slug planning_priority planning_order depends_on_workplans related_workplans created updated
FLEX-WP-0022 workplan Tenant scoping is unstated in tenant-engine and untested in two more packages infotech flex-auth proposed claude netkingdom P1 220
FLEX-WP-0021
FLEX-WP-0010
FLEX-WP-0014
2026-09-06 2026-09-06

FLEX-WP-0022 — Tenant scoping is unstated in tenant-engine and untested in two more packages

Opened by the sweep in FLEX-DEC-2026-008, which found secrets-engine.catalog-lane.lifecycle v1 allowing a foreign tenant and then asked whether the other packages shared the defect.

What the sweep found

Package Fixture tenants Tenant rule State
secrets-engine 3 distinct added at v2 fixed, FLEX-DEC-2026-008
qonto-assistant 2 distinct wrong_tenant fine
user-engine 2 distinct cross_tenant fine
ops-warden constant wrong_tenant rule real, fixtures do not vary it
railiance-platform constant wrong_tenant same
tenant-engine constant none deployed, unscoped

Verified against tenant-engine: an allowed tenant.create re-sent under tenant: tenant:coulomb returns allow / write_api_policy_matched.

Why tenant-engine is not the same fix

secrets-engine sends its own calling identity's tenant, so a constant known_tenant is the right rule. tenant-engine is different in kind: its subjects all sit in tenant:platform while its fixtures send tenant:friendly:binky, so the request tenant names the target of the operation. A service whose purpose is creating and retiring tenants acts across them by design, and a constant would break it on its first real call.

The defect today is therefore not "the rule is missing" but "nobody can tell whether it is missing": a deliberate cross-tenant scope and an omitted rule look identical in the artifact. That is the same publishing-shape argument gate-house made for §12's derived-artifact rule.

1. Get the intended tenant relation from tenant-engine

id: FLEX-WP-0022-T01
status: todo
priority: high

Owner: flex-auth to ask; tenant-engine owns the answer.

  • Ask what the CheckRequest tenant denotes on the write API: the caller's tenant, the target tenant record, or the tenant a guardrail applies to.
  • Ask whether any of the nine write actions must be refused cross-tenant, and whether tenant.guardrail.read (which flex-auth itself calls) differs.

Gate: the relation is named by tenant-engine, not inferred here. This is the FLEX-WP-0021-T01 rule applied to a field rather than to an action list.

2. Encode the relation, or record that there is none

id: FLEX-WP-0022-T02
status: wait
priority: high

Owner: flex-auth.

If a relation exists, encode it in tenant-engine.write-api.mutate as a new version — fail-open corrections are visible as version changes (FLEX-DEC-2026-008) — with a fixture per side.

If the scope is genuinely unrestricted, say so in the package: a stated "this package is deliberately cross-tenant, because the caller administers tenants" is a rule a reviewer can check. Silence is not.

Either way the fixtures must vary tenant, so the suite reports on the field.

3. Vary tenant in the two suites that hold it constant

id: FLEX-WP-0022-T03
status: todo
priority: medium

Owner: flex-auth.

ops-warden and railiance-platform have working wrong_tenant rules exercised only by Rego tests. Add a wrong-tenant deny fixture to each so the fixture suite covers what the rule claims. No policy change and no version bump: the behaviour is already correct, only the evidence is thin.

Gate: no package's fixture suite holds tenant constant.