Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions parity/policy-detail-disclosure-authority-v0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
{
"profile": "urn:iicp:profile:policy-detail-disclosure:v0",
"fixture_version": "0.2.0-draft",
"status": "pre-normative",
"allowed_detail_fields": [
"retention_intervals",
"subprocessor_references",
"approval_evidence_references",
"operational_evidence_references"
],
"reason_precedence": [
"consumer_auth_required",
"consumer_auth_invalid",
"consumer_auth_expired",
"consumer_auth_revoked",
"dispatch_ticket_invalid",
"dispatch_ticket_expired",
"dispatch_ticket_revoked",
"disclosure_forbidden",
"resource_concealed",
"compatible"
],
"cases": [
{
"id": "POLICY-DISCLOSURE-01",
"context": {
"consumer_auth": "valid",
"disclosure_allowed": true,
"provider_node_id": "node-a",
"consumer_target_node_id": "node-a",
"ticket_target_node_id": "node-a",
"consumer_intent": "urn:iicp:intent:llm:chat:v1",
"ticket_intent": "urn:iicp:intent:llm:chat:v1",
"manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"ticket_manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"details": {
"retention_intervals": {
"task_payload_max_seconds": 30,
"logs_max_days": 7
},
"subprocessor_references": [
"urn:iicp:processor:self-hosted"
],
"approval_evidence_references": [
"urn:iicp:evidence:approval:abc"
],
"operational_evidence_references": [
"urn:iicp:evidence:retention:def"
],
"prompt": "must-not-leak",
"response": "must-not-leak",
"credential": "must-not-leak",
"endpoint": "https://private.example",
"natural_person_contact": "must-not-leak@example.invalid",
"backend_topology": {
"peer": "private"
}
},
"dispatch_ticket": "valid"
},
"expected": {
"status": 200,
"reason": "compatible"
}
},
{
"id": "POLICY-DISCLOSURE-02",
"context": {
"consumer_auth": "missing",
"disclosure_allowed": true
},
"expected": {
"status": 401,
"reason": "consumer_auth_required"
}
},
{
"id": "POLICY-DISCLOSURE-03",
"context": {
"consumer_auth": "invalid",
"disclosure_allowed": true
},
"expected": {
"status": 401,
"reason": "consumer_auth_invalid"
}
},
{
"id": "POLICY-DISCLOSURE-04",
"context": {
"consumer_auth": "expired",
"disclosure_allowed": true
},
"expected": {
"status": 401,
"reason": "consumer_auth_expired"
}
},
{
"id": "POLICY-DISCLOSURE-05",
"context": {
"consumer_auth": "valid",
"disclosure_allowed": false,
"dispatch_ticket": "valid"
},
"expected": {
"status": 403,
"reason": "disclosure_forbidden"
}
},
{
"id": "POLICY-DISCLOSURE-06",
"context": {
"consumer_auth": "valid",
"disclosure_allowed": true,
"provider_node_id": "node-a",
"consumer_target_node_id": "node-b",
"ticket_target_node_id": "node-a",
"consumer_intent": "urn:iicp:intent:llm:chat:v1",
"ticket_intent": "urn:iicp:intent:llm:chat:v1",
"manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"ticket_manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"dispatch_ticket": "valid"
},
"expected": {
"status": 404,
"reason": "resource_concealed"
}
},
{
"id": "POLICY-DISCLOSURE-07",
"context": {
"consumer_auth": "valid",
"disclosure_allowed": true,
"provider_node_id": "node-a",
"consumer_target_node_id": "node-a",
"ticket_target_node_id": "node-a",
"consumer_intent": "urn:iicp:intent:llm:chat:v1",
"ticket_intent": "urn:iicp:intent:llm:embedding:v1",
"manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"ticket_manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"dispatch_ticket": "valid"
},
"expected": {
"status": 404,
"reason": "resource_concealed"
}
},
{
"id": "POLICY-DISCLOSURE-08",
"context": {
"consumer_auth": "valid",
"disclosure_allowed": true,
"provider_node_id": "node-a",
"consumer_target_node_id": "node-a",
"ticket_target_node_id": "node-a",
"consumer_intent": "urn:iicp:intent:llm:chat:v1",
"ticket_intent": "urn:iicp:intent:llm:chat:v1",
"manifest_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"ticket_manifest_sha256": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"dispatch_ticket": "valid"
},
"expected": {
"status": 404,
"reason": "resource_concealed"
}
},
{
"id": "POLICY-DISCLOSURE-09",
"context": {
"consumer_auth": "revoked",
"disclosure_allowed": true
},
"expected": {
"status": 401,
"reason": "consumer_auth_revoked"
}
},
{
"id": "POLICY-DISCLOSURE-10",
"context": {
"consumer_auth": "valid",
"dispatch_ticket": "invalid",
"disclosure_allowed": true
},
"expected": {
"status": 401,
"reason": "dispatch_ticket_invalid"
}
},
{
"id": "POLICY-DISCLOSURE-11",
"context": {
"consumer_auth": "valid",
"dispatch_ticket": "expired",
"disclosure_allowed": true
},
"expected": {
"status": 401,
"reason": "dispatch_ticket_expired"
}
},
{
"id": "POLICY-DISCLOSURE-12",
"context": {
"consumer_auth": "valid",
"dispatch_ticket": "revoked",
"disclosure_allowed": true
},
"expected": {
"status": 401,
"reason": "dispatch_ticket_revoked"
}
}
],
"crypto_vectors": {
"evaluated_at_unix": 1800000000,
"public_key_hex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664",
"expected_subject": "consumer-a",
"expected_target_node_id": "node-a",
"expected_intent": "urn:iicp:intent:llm:chat:v1",
"valid_consumer_token": "eyJ2IjoxLCJpc3MiOiJodHRwczovL2lpY3AubmV0d29yayIsInN1YiI6ImNvbnN1bWVyLWEiLCJhdWQiOiJub2RlLWEiLCJpbnRlbnQiOiJ1cm46aWljcDppbnRlbnQ6bGxtOmNoYXQ6djEiLCJpYXQiOjE4MDAwMDAwMDAsImV4cCI6MTgwMDAwMDMwMH0.1e84446db9af38ec1b34f1bb81a29609cc972772a2dc6e409b35574e74f74803d721f8a673d46ba789dc45e4bf35afe05099a776b90600c57a6abc6fd9a8e801",
"expired_consumer_token": "eyJ2IjoxLCJpc3MiOiJodHRwczovL2lpY3AubmV0d29yayIsInN1YiI6ImNvbnN1bWVyLWEiLCJhdWQiOiJub2RlLWEiLCJpbnRlbnQiOiJ1cm46aWljcDppbnRlbnQ6bGxtOmNoYXQ6djEiLCJpYXQiOjE4MDAwMDAwMDAsImV4cCI6MTc5OTk5OTk5OX0.4bb5e2260800b4934db2e8873f6a0ac96dc0ad2912afb66587646b54ce1a26937f0b4245993261fd4c514753a667e243c81eb215cc7da88eb458b61a50073e02",
"tampered_consumer_token": "eyJ2IjoxLCJpc3MiOiJodHRwczovL2lpY3AubmV0d29yayIsInN1YiI6ImNvbnN1bWVyLWEiLCJhdWQiOiJub2RlLWEiLCJpbnRlbnQiOiJ1cm46aWljcDppbnRlbnQ6bGxtOmNoYXQ6djEiLCJpYXQiOjE4MDAwMDAwMDAsImV4cCI6MTgwMDAwMDMwMH0.1e84446db9af38ec1b34f1bb81a29609cc972772a2dc6e409b35574e74f74803d721f8a673d46ba789dc45e4bf35afe05099a776b90600c57a6abc6fd9a8e800"
}
}
18 changes: 15 additions & 3 deletions src/iicp_client/policy_detail_disclosure.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,29 @@ def verify_policy_detail_consumer_token(
def evaluate_policy_detail_disclosure(context: dict[str, Any]) -> PolicyDetailDisclosureDecision:
"""Apply the portable authorization, concealment and redaction contract.

``consumer_auth`` MUST be the result of cryptographic verification by the
integration adapter, never a value copied from an untrusted request body.
``consumer_auth`` and ``dispatch_ticket`` MUST be verification results from
the integration adapter, never values copied from an untrusted request body.
"""

auth = context.get("consumer_auth")
if auth == "missing":
return PolicyDetailDisclosureDecision(401, "consumer_auth_required")
if auth == "invalid" or auth not in {"valid", "expired"}:
if auth == "invalid" or auth not in {"valid", "expired", "revoked"}:
return PolicyDetailDisclosureDecision(401, "consumer_auth_invalid")
if auth == "expired":
return PolicyDetailDisclosureDecision(401, "consumer_auth_expired")
if auth == "revoked":
return PolicyDetailDisclosureDecision(401, "consumer_auth_revoked")

ticket = context.get("dispatch_ticket")
if ticket is None:
pass # Historical v0 fixture: ticket authorization was not projected.
elif ticket == "expired":
return PolicyDetailDisclosureDecision(401, "dispatch_ticket_expired")
elif ticket == "revoked":
return PolicyDetailDisclosureDecision(401, "dispatch_ticket_revoked")
elif ticket != "valid":
return PolicyDetailDisclosureDecision(401, "dispatch_ticket_invalid")
if context.get("disclosure_allowed") is not True:
return PolicyDetailDisclosureDecision(403, "disclosure_forbidden")

Expand Down
33 changes: 21 additions & 12 deletions tests/test_policy_detail_disclosure.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,30 @@
verify_policy_detail_consumer_token,
)

FIXTURE = json.loads((Path(__file__).parents[1] / "parity/policy-detail-disclosure-v0.json").read_text())
PARITY = Path(__file__).parents[1] / "parity"
FIXTURES = [
json.loads((PARITY / name).read_text())
for name in (
"policy-detail-disclosure-v0.json",
"policy-detail-disclosure-authority-v0.json",
)
]
FIXTURE = FIXTURES[0]


def test_policy_detail_disclosure_fixture() -> None:
assert tuple(FIXTURE["allowed_detail_fields"]) == ALLOWED_DETAIL_FIELDS
for case in FIXTURE["cases"]:
decision = evaluate_policy_detail_disclosure(case["context"])
assert decision.status == case["expected"]["status"], case["id"]
assert decision.reason == case["expected"]["reason"], case["id"]
if decision.status == 200:
assert decision.body is not None
assert set(decision.body["details"]) <= set(ALLOWED_DETAIL_FIELDS)
serialized = json.dumps(decision.body)
for forbidden in ("must-not-leak", "private.example", "backend_topology", "natural_person_contact"):
assert forbidden not in serialized
for fixture in FIXTURES:
assert tuple(fixture["allowed_detail_fields"]) == ALLOWED_DETAIL_FIELDS
for case in fixture["cases"]:
decision = evaluate_policy_detail_disclosure(case["context"])
assert decision.status == case["expected"]["status"], case["id"]
assert decision.reason == case["expected"]["reason"], case["id"]
if decision.status == 200:
assert decision.body is not None
assert set(decision.body["details"]) <= set(ALLOWED_DETAIL_FIELDS)
serialized = json.dumps(decision.body)
for forbidden in ("must-not-leak", "private.example", "backend_topology", "natural_person_contact"):
assert forbidden not in serialized


def test_unrecognized_auth_state_fails_as_invalid() -> None:
Expand Down
Loading