124 lines
4.6 KiB
Python
124 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},
|
||
|
|
)
|