import copy from dataclasses import replace from datetime import datetime, timezone import json import time import pytest from informed_decision.http_transport import TransportError from informed_decision.oidc import HumanSession from informed_decision.policy import CONTRACT, PolicyClient, PolicyError, build_request, submitted_digest from informed_decision.provenance import Claim, Route from test_skeleton import make_memo PIN = "sha256:" + "b" * 64 STAMP = lambda t: datetime.fromtimestamp(t, timezone.utc).isoformat() def human(): return HumanSession("human-fixture", Claim("tenant:platform", Route.REGISTRATION), Claim("human", Route.AUTHENTICATION), {"at": int(time.time()) - 20, "level": "aal2", "mfa": True, "methods": ["pwd", "otp"], "source": "key-cape"}, time.time()+600, "synthetic-browser-token", roles=("Reviewer",)) def envelope(request): now = time.time() return {"id": "decision-fixture", "contract_version": CONTRACT, "request_id": request["id"], "effect": "allow", "matched_policy_version": "v1", "subject": copy.deepcopy(request["subject"]), "resource": copy.deepcopy(request["resource"]), "binding": {**copy.deepcopy({k:request[k] for k in ("tenant","subject","action","resource","context")}), "request_digest": "sha256:" + "e" * 64, "submitted_request_digest": submitted_digest(request)}, "lifetime": {"kind":"ttl", "not_before":STAMP(now-1), "expires_at":STAMP(now+60)}, "provenance": {"evaluator":"flex-auth/local", "mode":"standalone", "policy_package":"informed-decision.fixture", "policy_version":"v1", "policy_package_digest":PIN, "registry_snapshot_digest":"sha256:"+"c"*64, "decision_time":STAMP(now)}} class PolicyTransport: def __init__(self): self.calls=[]; self.change=lambda d: None; self.status=200 def request(self, method, url, *, headers=None, body=None): self.calls.append((method,url,headers,json.loads(body))) data=envelope(json.loads(body)); self.change(data) return self.status, data def policy(transport=None): return PolicyClient("https://policy.test", lambda:"synthetic-workload-token", package="informed-decision.fixture", version="v1", package_digest=PIN, transport=transport or PolicyTransport()) def request(): return build_request(human(), make_memo(approval_binding_digest="sha256:"+"a"*64), "read", "v1") def test_request_preserves_identity_routes_and_excludes_presentation_claims(): req=request(); attributes=req["subject"]["attributes"] assert attributes["tenant_source"]=="registration-supplied" and attributes["principal_type_source"]=="authentication-derived" assert attributes["roles"]==["Reviewer"] assert set(req["context"])=={"memo_version","approval_id","approval_binding_digest"} assert "synthetic-browser-token" not in json.dumps(req) assert not any(k in json.dumps(req) for k in ["view_hash","presentation_id","acked_highlight_ids"]) def test_fresh_exact_enriched_decision_and_separate_workload_identity(): client=policy();req=request();observed=client.check(req) observed.require_current(time.time()) data=json.loads(observed.decision_json) assert data["binding"]["request_digest"]!=data["binding"]["submitted_request_digest"] assert client.transport.calls[0][2]["Authorization"]=="Bearer synthetic-workload-token" assert observed.usable_until<=time.time()+30 with pytest.raises(PolicyError):observed.require_current(time.time()+31) @pytest.mark.parametrize("mutate", [ lambda d:d.update(contract_version="unknown"), lambda d:d.update(request_id="other"), lambda d:d["binding"].pop("submitted_request_digest"), lambda d:d["binding"].update(submitted_request_digest="sha256:"+"0"*64), lambda d:d["binding"].update(action="accept"), lambda d:d["binding"]["subject"].update(id="other"), lambda d:d["binding"]["resource"].update(system="other"), lambda d:d["binding"]["context"].update(memo_version=99), lambda d:d["binding"].update(tenant="tenant:other"), lambda d:d["resource"].update(id="memo:other"), lambda d:d["provenance"].update(policy_package="other"), lambda d:d["provenance"].update(policy_version="other"), lambda d:d["provenance"].update(policy_package_digest="sha256:"+"0"*64), lambda d:d.update(matched_policy_version="other"), lambda d:d["provenance"].pop("registry_snapshot_digest"), lambda d:d["provenance"].update(decision_time=STAMP(time.time()-300)), lambda d:d["lifetime"].update(expires_at=STAMP(time.time()-1)), lambda d:d["lifetime"].update(not_before=STAMP(time.time()+300)), lambda d:d["lifetime"].update(kind="forever"), lambda d:d.update(lifetime={}), lambda d:d.update(obligations=[{"type":"unimplemented"}]), lambda d:d.update(obligations={}), lambda d:d.update(obligations=None), lambda d:d.update(effect="redact"), lambda d:d.update(effect="audit_only"), lambda d:d.update(effect="not_applicable"), lambda d:d.update(effect="deny"), lambda d:d.update(diagnostics={"bad":float("nan")}), ]) def test_unusable_decision_never_allows(mutate): transport=PolicyTransport();transport.change=mutate observed=policy(transport).check(request()) with pytest.raises(PolicyError):observed.require_current(time.time()) @pytest.mark.parametrize("status", [400,401,403,404,409,500,503]) def test_upstream_errors_never_persist_response_body(status): transport=PolicyTransport();transport.status=status transport.change=lambda d:d.update(reason="secret-sentinel",diagnostics={"token":"secret-sentinel"}) observed=policy(transport).check(request()) assert observed.decision_json is None and "secret-sentinel" not in repr(observed) with pytest.raises(PolicyError):observed.require_current(time.time()) def test_transport_and_missing_caller_have_typed_observations(): class Broken: def request(self,*a,**kw):raise TransportError("secret-sentinel") client=policy(Broken());assert client.check(request()).outcome=="policy_unavailable" client.token_provider=lambda:None assert client.check(request()).outcome=="caller_unavailable"