102 lines
4.6 KiB
Python
102 lines
4.6 KiB
Python
|
|
"""Opt-in actual engine contract check, using signed synthetic identity fixtures.
|
||
|
|
|
||
|
|
INFD_APPROVAL_ENGINE_SOURCE=/path/to/approval-engine python -m pytest -q
|
||
|
|
No native credentials, human login, policy verdict or audit admission is proved.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from datetime import datetime, timedelta, timezone
|
||
|
|
import io
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
from pathlib import Path
|
||
|
|
import sys
|
||
|
|
|
||
|
|
import jwt
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from informed_decision.approval_client import ApprovalEngineError
|
||
|
|
from informed_decision.approval_http import ApprovalHTTPClient
|
||
|
|
from test_browser_auth import ISSUER, IssuerFixture, finish, signing_key
|
||
|
|
from informed_decision.oidc import KeyCapeLogin
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def component(signing_key, tmp_path):
|
||
|
|
source = os.environ.get("INFD_APPROVAL_ENGINE_SOURCE")
|
||
|
|
if not source:
|
||
|
|
pytest.skip("set INFD_APPROVAL_ENGINE_SOURCE for the real-engine contract check")
|
||
|
|
assert (Path(source) / "approval_engine" / "api.py").is_file()
|
||
|
|
sys.path.insert(0, source)
|
||
|
|
from approval_engine.api import App
|
||
|
|
from approval_engine.auth import JWTAuthenticator
|
||
|
|
from approval_engine.store import Engine
|
||
|
|
|
||
|
|
issuer = IssuerFixture(signing_key)
|
||
|
|
login = KeyCapeLogin(ISSUER, transport=issuer)
|
||
|
|
sid = finish((login, issuer))
|
||
|
|
session = login.session(sid)
|
||
|
|
|
||
|
|
class Keys:
|
||
|
|
def get_signing_key_from_jwt(self, token):
|
||
|
|
# Use the exact same synthetic JWKS that the login client verified.
|
||
|
|
return jwt.PyJWK.from_dict(issuer.request("GET", ISSUER + "/jwks")[1]["keys"][0])
|
||
|
|
|
||
|
|
engine = Engine(tmp_path / "approval.db")
|
||
|
|
auth = JWTAuthenticator(issuer=ISSUER, audience="approval-engine", jwks_url=ISSUER + "/jwks", jwks_client=Keys())
|
||
|
|
app = App(engine, auth)
|
||
|
|
|
||
|
|
class Transport:
|
||
|
|
def __init__(self): self.calls = []
|
||
|
|
def request(self, method, url, *, headers=None, body=None):
|
||
|
|
self.calls.append((method, url))
|
||
|
|
payload = body or b""
|
||
|
|
environ = {"PATH_INFO": url.removeprefix("https://approval.test"), "REQUEST_METHOD": method,
|
||
|
|
"CONTENT_LENGTH": str(len(payload)), "wsgi.input": io.BytesIO(payload),
|
||
|
|
"HTTP_AUTHORIZATION": (headers or {}).get("Authorization", "")}
|
||
|
|
result = {}
|
||
|
|
def start(status, headers): result["status"] = int(status.split()[0])
|
||
|
|
raw = b"".join(app(environ, start))
|
||
|
|
return result["status"], json.loads(raw)
|
||
|
|
|
||
|
|
transport = Transport()
|
||
|
|
now = datetime.now(timezone.utc)
|
||
|
|
engine.create({"actor": "synthetic-requester", "principal": "factory-fixture",
|
||
|
|
"action": "deliver", "purpose": "local-component-proof", "target": {"resource": "fixture"}},
|
||
|
|
{"not_before": (now - timedelta(seconds=1)).isoformat(),
|
||
|
|
"expires_at": (now + timedelta(minutes=10)).isoformat()},
|
||
|
|
human_control=True, approval_id="fixture")
|
||
|
|
client = ApprovalHTTPClient("https://approval.test", session, transport=transport)
|
||
|
|
yield client, engine, transport, session, signing_key
|
||
|
|
|
||
|
|
|
||
|
|
def test_real_engine_verifies_human_access_jwt_and_duplicate_correlation(component):
|
||
|
|
client, engine, transport, session, key = component
|
||
|
|
result = client.add_entry("fixture")
|
||
|
|
assert result.subject == session.subject and result.status == "approved"
|
||
|
|
entry = engine.get("fixture").as_dict()["entries"][0]
|
||
|
|
assert result.approved_at == entry["approved_at"]
|
||
|
|
assert entry["principal_type"] == "human" and entry["evidence_ref"].startswith("jwt-sha256:")
|
||
|
|
duplicate = client.add_entry("fixture")
|
||
|
|
assert duplicate.duplicate and duplicate.correlation == result.correlation
|
||
|
|
assert len(engine.get("fixture").entries) == 1
|
||
|
|
assert not any(url.endswith("/consume") for _, url in transport.calls)
|
||
|
|
|
||
|
|
|
||
|
|
def test_real_engine_service_token_cannot_bind_declared_control(component):
|
||
|
|
client, engine, transport, session, key = component
|
||
|
|
claims = jwt.decode(session.access_token, options={"verify_signature": False})
|
||
|
|
claims["principal_type"] = "service"
|
||
|
|
service = jwt.encode(claims, key, algorithm="RS256", headers={"kid": "key-1"})
|
||
|
|
status, body = transport.request("POST", "https://approval.test/v1/approvals/fixture/entries",
|
||
|
|
headers={"Authorization": "Bearer " + service}, body=b"{}")
|
||
|
|
assert (status, body["error"]) == (403, "forbidden")
|
||
|
|
assert engine.get("fixture").entries == []
|
||
|
|
|
||
|
|
|
||
|
|
def test_real_engine_revocation_stays_a_conflict(component):
|
||
|
|
client, engine, transport, session, key = component
|
||
|
|
engine.revoke("fixture")
|
||
|
|
with pytest.raises(ApprovalEngineError) as error: client.add_entry("fixture")
|
||
|
|
assert error.value.status == 409 and error.value.reason == "conflict"
|
||
|
|
assert engine.get("fixture").entries == []
|