secrets-engine/tests/test_live_decision_enrichment.py
tegwick 03c0569820
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
feat: adopt the owner-documented PDP access path, and prove it live
Adoption asked for by flex-auth (FLEX-WP-0021-T05) and glas-harness
(GLAS-WP-0015), plus the first real decision this engine has obtained from
the deployed pin -- which found a defect the fixtures could not.

ACCESS PATH. require_supported_pdp_address refuses in-cluster Service names
and any non-loopback host. This is no longer a unilateral call: the owner
path is documented as loopback kubectl port-forward over the authenticated
Kubernetes API, which is what authenticates the responder transitively
(FLEX-DEC-2026-010). A Service name from a workstation does not fail, it
resolves through the DNS search suffix to an unrelated public host, and
since decision records carry no signature, a responder knowing the
published package and version can return an allow that passes every check
we make. Fail-closed protects against a PDP that is absent, not one that
lies. The guard runs before the token is read, so a misdirected request
cannot leak it; a test pins that ordering.

LIVE PROOF. Minted a 10-minute TokenRequest token (audience flex-auth, SA
secrets-engine/secrets-engine, mode 0600 outside the worktree, shredded
after), forwarded to the named pod, and sent a real CheckRequest for
glas-claude-agent-dev-anthropic. Result: allow, catalog_lane_policy_matched,
served by v2 (sha256:bd11c5fe...) -- so the redeploy flex-auth flagged as
outstanding has landed and the pin no longer serves the tenant-blind v1.
Our tenant fix is confirmed against the real service: binding.tenant is
tenant:platform.

THE DEFECT IT FOUND. The evaluator enriches from its registry before
hashing -- subject gains attributes and tenant, resource gains tenant --
so binding.request_digest is over material we never sent and cannot
reproduce. validate_decision_envelope rejects every real allow.

Every replay test passes because _request_from() rebuilds the request out
of the binding, i.e. the enriched form: a self-consistent fake agreeing
with itself, which hid this through three rounds of digest work. Third
time a real artifact has beaten a fake in this integration.

NOT FIXED, DELIBERATELY. Rejecting a valid allow is wrong in the safe
direction. Which fields may be enriched is flex-auth's contract to publish;
inferring it means accepting a binding that differs from our proposal in a
way we decided was benign -- the fail-open shape GH-DEC-2026-008 rejected
for vocabularies and FLEX-DEC-2026-007 for digests. Raised with them.

Tenant question closed by operator decision 5ed3fb35: tenant:platform
exactly, and service_auth.TENANT stays tenant:coulomb because the two
identity layers are to remain distinct. Declining to author that mapping
was right -- the answer was neither reading offered.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715726@bnt-lap001
Assistant-Session: 80a42b32-cba6-4b23-8be0-68819b1a6092
2026-09-07 00:22:12 +02:00

123 lines
4.6 KiB
Python

"""The digest join against a REAL request, not one rebuilt from the binding.
tests/test_decision_replay.py passes every digest assertion because
``_request_from()`` reconstructs the request out of ``envelope["binding"]`` --
which is the *enriched* form the evaluator hashed. That is a self-consistent
fake agreeing with itself, and it hid the gap below through three rounds of
digest work.
This module builds the request the way the engine actually builds it and
compares it to a decision the engine actually received, so the gap is pinned
until flex-auth publishes the enrichment rule.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from secrets_engine.authorization import (
build_action_request,
canonical_check_request,
request_digest,
validate_decision_envelope,
)
from secrets_engine.catalog import load_catalog
from secrets_engine.errors import DecisionError
LIVE = Path(__file__).parent / "fixtures" / "flex-auth-live" / "decision_rotate_glas_live.json"
CATALOG = Path(__file__).resolve().parents[1] / "catalog"
PACKAGE = "secrets-engine.catalog-lane.lifecycle"
VERSION = "v2"
def _live():
return json.loads(LIVE.read_text())
def _our_request():
entry = load_catalog(CATALOG)["glas-claude-agent-dev-anthropic"]
return build_action_request(
entry,
"rotate",
subject_id="secrets-engine",
subject_type="service",
purpose="live-adoption-proof",
fields=["api_key"],
request_id="check:secrets-engine-adoption-proof",
)
def test_the_live_decision_is_a_real_v2_allow():
"""Provenance of the artifact these assertions rest on."""
env = _live()
assert env["effect"] == "allow"
assert env["reason"] == "catalog_lane_policy_matched"
provenance = env["provenance"]
assert provenance["policy_package"] == PACKAGE
assert provenance["policy_version"] == VERSION
assert env["binding"]["tenant"] == "tenant:platform"
def test_the_evaluator_enriches_subject_and_resource_before_hashing():
"""Names exactly which fields appeared that we never sent.
If flex-auth publishes an enrichment rule that differs from this, this test
fails and tells us the shape moved -- which is the point. It asserts the
observed gap, not a rule we invented.
"""
sent = _our_request()
bound = _live()["binding"]
assert "attributes" not in sent["subject"]
assert set(bound["subject"]["attributes"]) == {
"description", "display_name", "groups", "organization_relation", "roles",
}
assert "tenant" not in sent["subject"]
assert bound["subject"]["tenant"] == sent["tenant"]
assert "tenant" not in sent["resource"]
assert bound["resource"]["tenant"] == sent["tenant"]
# Everything we DID send survived unchanged. The enrichment is additive, so
# the decision is about the action we proposed -- which is why staying
# fail-closed here costs correctness nothing today.
assert bound["action"] == sent["action"]
assert bound["tenant"] == sent["tenant"]
assert bound["context"] == sent["context"]
assert bound["subject"]["id"] == sent["subject"]["id"]
assert bound["subject"]["type"] == sent["subject"]["type"]
for key in ("id", "type", "system", "attributes"):
assert bound["resource"][key] == sent["resource"][key]
def test_our_digest_cannot_match_a_real_binding():
"""The join is unsatisfiable against a real request, not merely mismatched.
We hash what we sent; the evaluator hashed what it enriched. No amount of
care on our side closes that, because the registry material is not ours.
"""
sent = _our_request()
bound = _live()["binding"]
assert bound["request_digest"] != request_digest(sent)
assert canonical_check_request(bound) != canonical_check_request(sent)
def test_we_currently_reject_every_real_allow_and_that_is_fail_closed():
"""Pins today's behaviour honestly rather than asserting it is correct.
Rejecting a valid allow is wrong, but it is wrong in the safe direction, so
it stays until flex-auth publishes which fields may be enriched. Guessing
the rule would mean accepting a binding that differs from our proposal in
some way we decided was benign -- the fail-open shape GH-DEC-2026-008
rejected for vocabularies and FLEX-DEC-2026-007 rejected for digests.
"""
with pytest.raises(DecisionError, match="binding does not match request"):
validate_decision_envelope(
_live(),
_our_request(),
accepted_policy_packages={PACKAGE},
accepted_policy_versions={VERSION},
)