From f77b512d90778c8648991282bdf1494dba0818cf Mon Sep 17 00:00:00 2001 From: John Andersen Date: Thu, 30 Mar 2023 22:21:47 -0700 Subject: [PATCH 01/13] Simple file presence based policy evaluation Signed-off-by: John Andersen --- README.md | 1 + scitt_emulator/scitt.py | 43 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/README.md b/README.md index daff0e4f..31b3981b 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ The `service_parameters.json` file gets created when starting a service using `. "serviceId": "emulator", "treeAlgorithm": "CCF", "signatureAlgorithm": "ES256", + "insertPolicy": "*", "serviceCertificate": "-----BEGIN CERTIFICATE-----..." } ``` diff --git a/scitt_emulator/scitt.py b/scitt_emulator/scitt.py index 18469941..867c8fe9 100644 --- a/scitt_emulator/scitt.py +++ b/scitt_emulator/scitt.py @@ -22,6 +22,10 @@ COSE_Headers_Tree_Alg = "tree_alg" COSE_Headers_Issued_At = "issued_at" +# permissive insert policy +MOST_PERMISSIVE_INSERT_POLICY = "*" +DEFAULT_INSERT_POLICY = MOST_PERMISSIVE_INSERT_POLICY + class ClaimInvalidError(Exception): pass @@ -94,8 +98,14 @@ def get_claim(self, entry_id: str) -> bytes: return claim def submit_claim(self, claim: bytes, long_running=True) -> dict: + insert_policy = self.service_parameters.get("insertPolicy", DEFAULT_INSERT_POLICY) + if long_running: return self._create_operation(claim) + elif insert_policy != MOST_PERMISSIVE_INSERT_POLICY: + raise NotImplementedError( + f"non-* insertPolicy only works with long_running=True: {insert_policy!r}" + ) else: return self._create_entry(claim) @@ -142,11 +152,44 @@ def _create_operation(self, claim: bytes): return operation + def _sync_policy_result(self, operation: dict): + operation_id = operation["operationId"] + policy_insert_path = self.operations_path / f"{operation_id}.policy.insert" + policy_denied_path = self.operations_path / f"{operation_id}.policy.denied" + policy_failed_path = self.operations_path / f"{operation_id}.policy.failed" + insert_policy = self.service_parameters.get("insertPolicy", DEFAULT_INSERT_POLICY) + + policy_result = {"status": operation["status"]} + + if insert_policy == MOST_PERMISSIVE_INSERT_POLICY: + policy_result["status"] = "succeeded" + if policy_insert_path.exists(): + policy_result["status"] = "succeeded" + policy_insert_path.unlink() + if policy_failed_path.exists(): + policy_result["status"] = "failed" + policy_failed_path.unlink() + if policy_denied_path.exists(): + policy_result["status"] = "denied" + policy_denied_path.unlink() + + return policy_result + def _finish_operation(self, operation: dict): operation_id = operation["operationId"] operation_path = self.operations_path / f"{operation_id}.json" claim_src_path = self.operations_path / f"{operation_id}.cose" + policy_result = self._sync_policy_result(operation) + if policy_result["status"] == "running": + return operation + if policy_result["status"] != "succeeded": + operation["status"] = "failed" + operation["error"] = policy_result + operation_path.unlink() + claim_src_path.unlink() + return operation + claim = claim_src_path.read_bytes() entry = self._create_entry(claim) claim_src_path.unlink() From 07a63a31330c9477c63d7142058f809ec512580f Mon Sep 17 00:00:00 2001 From: John Andersen Date: Fri, 31 Mar 2023 01:01:31 -0700 Subject: [PATCH 02/13] Document simple decoupled file based policy engine Signed-off-by: John Andersen --- docs/registration_policies.md | 124 ++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 docs/registration_policies.md diff --git a/docs/registration_policies.md b/docs/registration_policies.md new file mode 100644 index 00000000..d8f331b1 --- /dev/null +++ b/docs/registration_policies.md @@ -0,0 +1,124 @@ +# Registration Policies + +- References + - [5.2.2. Registration Policies](https://www.ietf.org/archive/id/draft-birkholz-scitt-architecture-02.html#name-registration-policies) + +## Simple decoupled file based policy engine + +The SCITT API emulator can deny entry based on presence of +`operation.policy.{insert,denied,failed}` files. Currently only for use with +`use_lro=True`. + +This is a simple way to enable evaluation of claims prior to submission by +arbitrary policy engines which watch the workspace (fanotify, inotify, etc.). + +Start the server + +```console +$ rm -rf workspace/ +$ mkdir -p workspace/storage/operations +$ scitt-emulator server --workspace workspace/ --tree-alg CCF --use-lro +Service parameters: workspace/service_parameters.json +^C +``` + +Modification of config to non-`*` insert policy. Restart SCITT API emulator server after this. + +```console +$ echo "$(cat workspace/service_parameters.json)" \ + | jq '.insertPolicy = "external"' \ + | tee workspace/service_parameters.json.new \ + && mv workspace/service_parameters.json.new workspace/service_parameters.json +{ + "serviceId": "emulator", + "treeAlgorithm": "CCF", + "signatureAlgorithm": "ES256", + "serviceCertificate": "-----BEGIN CERTIFICATE-----", + "insertPolicy": "external" +} +``` + +Basic policy engine in two files + +**enforce_policy.py** + +```python +import os +import sys +import pathlib + +cose_path = pathlib.Path(sys.argv[-1]) +policy_action_path = cose_path.with_suffix(".policy." + os.environ["POLICY_ACTION"].lower()) +policy_action_path.write_text("") +``` + +Simple drop rule based on claim content blocklist. + +**is_on_blocklist.py** + +```python +import os +import sys +import json + +import cbor2 +import pycose +from pycose.messages import CoseMessage, Sign1Message + +from scitt_emulator.scitt import ClaimInvalidError, COSE_Headers_Issuer + +BLOCKLIST_DEFAULT = [ + "did:web:example.com", +] +BLOCKLIST_DEFAULT_JSON = json.dumps(BLOCKLIST_DEFAULT) +BLOCKLIST = json.loads(os.environ.get("BLOCKLIST", BLOCKLIST_DEFAULT_JSON)) + +claim = sys.stdin.buffer.read() + +msg = CoseMessage.decode(claim) + +if pycose.headers.ContentType not in msg.phdr: + raise ClaimInvalidError( + "Claim does not have a content type header parameter" + ) +if COSE_Headers_Issuer not in msg.phdr: + raise ClaimInvalidError("Claim does not have an issuer header parameter") + +if msg.phdr[COSE_Headers_Issuer] not in BLOCKLIST: + sys.exit(1) + +# EXIT_SUCCESS == MUST block. In case of thrown errors/exceptions. +``` + +Example running blocklist check and enforcement to disable issuer (example: +`did:web:example.com`). + +```console +$ npm install -g nodemon +$ nodemon -e .cose --exec 'find workspace/storage/operations -name \*.cose -exec nohup sh -xc "echo {} && (python3 is_on_blocklist.py < {} && POLICY_ACTION=denied python3 enforce_policy.py {}) || POLICY_ACTION=insert python3 enforce_policy.py {}" \;' +``` + +Create claim from blocked issuer (`.com`) and from non-blocked (`.org`). + +```console +$ scitt-emulator client create-claim --issuer did:web:example.com --content-type application/json --payload '{"sun": "yellow"}' --out claim.cose +Claim written to claim.cose +$ scitt-emulator client submit-claim --claim claim.cose --out claim.receipt.cbor +Traceback (most recent call last): + File "/home/alice/.local/bin/scitt-emulator", line 33, in + sys.exit(load_entry_point('scitt-emulator', 'console_scripts', 'scitt-emulator')()) + File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/cli.py", line 22, in main + args.func(args) + File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 182, in + func=lambda args: submit_claim( + File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 93, in submit_claim + raise_for_operation_status(operation) + File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 29, in raise_for_operation_status + raise RuntimeError(f"Operation error: {operation['error']}") +RuntimeError: Operation error: {'status': 'denied'} +$ scitt-emulator client create-claim --issuer did:web:example.org --content-type application/json --payload '{"sun": "yellow"}' --out claim.cose +Claim written to claim.cose +$ scitt-emulator client submit-claim --claim claim.cose --out claim.receipt.cbor +Claim registered with entry ID 1 +Receipt written to claim.receipt.cbor +``` From 4c386b8d77c2031a6820f61cd1b5e48e64315c0d Mon Sep 17 00:00:00 2001 From: John Andersen Date: Fri, 31 Mar 2023 02:20:25 -0700 Subject: [PATCH 03/13] Tests for simple decoupled file based policy engine Signed-off-by: John Andersen --- dev-requirements.txt | 1 + environment.yml | 1 + tests/__init__.py | 0 tests/test_docs.py | 214 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_docs.py diff --git a/dev-requirements.txt b/dev-requirements.txt index ee22e660..b0b31ac2 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,3 +3,4 @@ pytest requests==2.31.0 requests-toolbelt==0.9 urllib3<2.0.0 +myst-parser diff --git a/environment.yml b/environment.yml index f4dc8bcd..e0f127cf 100644 --- a/environment.yml +++ b/environment.yml @@ -34,3 +34,4 @@ dependencies: - rkvst-archivist==0.20.0 - six==1.16.0 - urllib3<2.0.0 + - myst-parser==1.0.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_docs.py b/tests/test_docs.py new file mode 100644 index 00000000..854953a3 --- /dev/null +++ b/tests/test_docs.py @@ -0,0 +1,214 @@ +# Copyright (c) SCITT Authors +# Licensed under the MIT License. +import os +import sys +import time +import types +import pathlib +import threading +import itertools +import subprocess +import contextlib +import unittest.mock +import pytest +import myst_parser.parsers.docutils_ +import docutils.nodes +import docutils.utils + +from .test_cli import ( + Service, + content_type, + payload, + execute_cli, +) + + +repo_root = pathlib.Path(__file__).parents[1] +docs_dir = repo_root.joinpath("docs") +blocklisted_issuer = "did:web:example.com" +non_blocklisted_issuer = "did:web:example.org" + + +class SimpleFileBasedPolicyEngine: + def __init__(self, config): + self.config = config + + def __enter__(self): + self.stop_event = threading.Event() + self.thread = threading.Thread( + name="policy", + target=self.poll_workspace, + args=[self.config, self.stop_event], + ) + self.thread.start() + return self + + def __exit__(self, *args): + self.stop_event.set() + self.thread.join() + + @staticmethod + def poll_workspace(config, stop_event): + operations_path = pathlib.Path(config["storage_path"], "operations") + command_is_on_blocklist = [ + sys.executable, + str(config["is_on_blocklist"].resolve()), + ] + command_enforce_policy = [ + sys.executable, + str(config["enforce_policy"].resolve()), + ] + + running = True + while running: + for cose_path in operations_path.glob("*.cose"): + with open(cose_path, "rb") as stdin_fileobj: + exit_code = subprocess.call( + command_is_on_blocklist, + stdin=stdin_fileobj, + ) + # EXIT_SUCCESS from blocklist == MUST block + env = { + **os.environ, + "POLICY_ACTION": { + 0: "denied", + }.get(exit_code, "insert"), + } + command = command_enforce_policy + [cose_path] + exit_code = subprocess.call(command, env=env) + time.sleep(0.1) + running = not stop_event.is_set() + +def docutils_recursively_extract_nodes(node, samples = None): + if samples is None: + samples = [] + if isinstance(node, list): + node = types.SimpleNamespace(children=node) + return samples + list(itertools.chain(*[ + [ + child, + *docutils_recursively_extract_nodes(child), + ] + for child in node.children + if hasattr(child, "children") + ])) + +def docutils_find_code_samples(nodes): + samples = {} + for i, node in enumerate(nodes): + # Look ahead for next literal block with code sample. Pattern is: + # + # **strong.suffix** + # + # ```language + # content + # ```` + # TODO Gracefully handle expections to index out of bounds + if ( + isinstance(node, docutils.nodes.strong) + and isinstance(nodes[i + 3], docutils.nodes.literal_block) + ): + samples[node.astext()] = nodes[i + 3].astext() + return samples + +def test_docs_registration_policies(tmp_path): + workspace_path = tmp_path / "workspace" + + claim_path = tmp_path / "claim.cose" + receipt_path = tmp_path / "claim.receipt.cbor" + entry_id_path = tmp_path / "claim.entry_id.txt" + retrieved_claim_path = tmp_path / "claim.retrieved.cose" + + # Grab code samples from docs + # TODO Abstract into abitrary docs testing code + doc_path = docs_dir.joinpath("registration_policies.md") + markdown_parser = myst_parser.parsers.docutils_.Parser() + document = docutils.utils.new_document(str(doc_path.resolve())) + parsed = markdown_parser.parse(doc_path.read_text(), document) + nodes = docutils_recursively_extract_nodes(document) + for name, content in docutils_find_code_samples(nodes).items(): + tmp_path.joinpath(name).write_text(content) + + with Service( + { + "tree_alg": "CCF", + "workspace": workspace_path, + "error_rate": 0.1, + "use_lro": True, + } + ) as service, SimpleFileBasedPolicyEngine( + { + "storage_path": service.server.app.scitt_service.storage_path, + "enforce_policy": tmp_path.joinpath("enforce_policy.py"), + "is_on_blocklist": tmp_path.joinpath("is_on_blocklist.py"), + } + ) as policy_engine: + # set the policy to enforce + service.server.app.scitt_service.service_parameters["insertPolicy"] = "external" + + # create denied claim + command = [ + "client", + "create-claim", + "--out", + claim_path, + "--issuer", + blocklisted_issuer, + "--content-type", + content_type, + "--payload", + payload, + ] + execute_cli(command) + assert os.path.exists(claim_path) + + # submit denied claim + command = [ + "client", + "submit-claim", + "--claim", + claim_path, + "--out", + receipt_path, + "--out-entry-id", + entry_id_path, + "--url", + service.url + ] + with pytest.raises(RuntimeError, match=r"denied"): + execute_cli(command) + assert not os.path.exists(receipt_path) + assert not os.path.exists(entry_id_path) + + # create accepted claim + command = [ + "client", + "create-claim", + "--out", + claim_path, + "--issuer", + non_blocklisted_issuer, + "--content-type", + content_type, + "--payload", + payload, + ] + execute_cli(command) + assert os.path.exists(claim_path) + + # submit accepted claim + command = [ + "client", + "submit-claim", + "--claim", + claim_path, + "--out", + receipt_path, + "--out-entry-id", + entry_id_path, + "--url", + service.url + ] + execute_cli(command) + assert os.path.exists(receipt_path) + assert os.path.exists(entry_id_path) From 84efde5d7e994e8008e4f7ba6aa7178786f845f0 Mon Sep 17 00:00:00 2001 From: John Andersen Date: Fri, 31 Mar 2023 15:29:19 -0700 Subject: [PATCH 04/13] asciicast example simple decoupled file based policy engine for documentation Signed-off-by: John Andersen --- docs/registration_policies.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/registration_policies.md b/docs/registration_policies.md index d8f331b1..a4825718 100644 --- a/docs/registration_policies.md +++ b/docs/registration_policies.md @@ -12,6 +12,8 @@ The SCITT API emulator can deny entry based on presence of This is a simple way to enable evaluation of claims prior to submission by arbitrary policy engines which watch the workspace (fanotify, inotify, etc.). +[![asciicast-of-simple-decoupled-file-based-policy-engine](https://asciinema.org/a/572766.svg)](https://asciinema.org/a/572766) + Start the server ```console From a46d35ee805ad111b67712bcb0d63dc4ff70b342 Mon Sep 17 00:00:00 2001 From: John Andersen Date: Sat, 1 Apr 2023 16:05:45 -0700 Subject: [PATCH 05/13] Implement reason communication associated with policy enforcement - Updated simple file based policy engine to align with @darrelmiller review of SCITT architecture documentation - His full review can be found at https://mailarchive.ietf.org/arch/msg/scitt/c0t5zLUJtCQ9_Jrf7mykWXSIn94/ - Do not attempt to load policy failed/denied JSON information if present triggering file empty Signed-off-by: John Andersen --- docs/registration_policies.md | 19 ++++++++++++------- scitt_emulator/client.py | 10 +++++++++- scitt_emulator/scitt.py | 20 +++++++++++++++++++- tests/test_docs.py | 33 ++++++++++++++++++++++++--------- 4 files changed, 64 insertions(+), 18 deletions(-) diff --git a/docs/registration_policies.md b/docs/registration_policies.md index a4825718..49b8f4e4 100644 --- a/docs/registration_policies.md +++ b/docs/registration_policies.md @@ -49,9 +49,13 @@ import os import sys import pathlib +policy_reason = "" +if "POLICY_REASON_PATH" in os.environ: + policy_reason = pathlib.Path(os.environ["POLICY_REASON_PATH"]).read_text() + cose_path = pathlib.Path(sys.argv[-1]) policy_action_path = cose_path.with_suffix(".policy." + os.environ["POLICY_ACTION"].lower()) -policy_action_path.write_text("") +policy_action_path.write_text(policy_reason) ``` Simple drop rule based on claim content blocklist. @@ -97,7 +101,8 @@ Example running blocklist check and enforcement to disable issuer (example: ```console $ npm install -g nodemon -$ nodemon -e .cose --exec 'find workspace/storage/operations -name \*.cose -exec nohup sh -xc "echo {} && (python3 is_on_blocklist.py < {} && POLICY_ACTION=denied python3 enforce_policy.py {}) || POLICY_ACTION=insert python3 enforce_policy.py {}" \;' +$ echo '{"type": "denied", "detail": "content_address_of_reason"}' | tee reason.json +$ nodemon -e .cose --exec 'find workspace/storage/operations -name \*.cose -exec nohup sh -xc "echo {} && (python3 is_on_blocklist.py < {} && POLICY_ACTION=denied POLICY_REASON_PATH=reason.json python3 enforce_policy.py {}) || POLICY_ACTION=insert python3 enforce_policy.py {}" \;' ``` Create claim from blocked issuer (`.com`) and from non-blocked (`.org`). @@ -111,13 +116,13 @@ Traceback (most recent call last): sys.exit(load_entry_point('scitt-emulator', 'console_scripts', 'scitt-emulator')()) File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/cli.py", line 22, in main args.func(args) - File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 182, in + File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 190, in func=lambda args: submit_claim( - File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 93, in submit_claim + File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 101, in submit_claim raise_for_operation_status(operation) - File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 29, in raise_for_operation_status - raise RuntimeError(f"Operation error: {operation['error']}") -RuntimeError: Operation error: {'status': 'denied'} + File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 37, in raise_for_operation_status + raise ClaimOperationError(operation) +scitt_emulator.client.ClaimOperationError: Operation error: {'error': {'detail': 'content_address_of_reason', 'type': 'denied'}, 'operationId': '9693b076-f992-44e1-b7b9-865c600a96f7', 'status': 'failed'} $ scitt-emulator client create-claim --issuer did:web:example.org --content-type application/json --payload '{"sun": "yellow"}' --out claim.cose Claim written to claim.cose $ scitt-emulator client submit-claim --claim claim.cose --out claim.receipt.cbor diff --git a/scitt_emulator/client.py b/scitt_emulator/client.py index d7f5edd3..110ac9ac 100644 --- a/scitt_emulator/client.py +++ b/scitt_emulator/client.py @@ -17,6 +17,14 @@ HTTP_DEFAULT_RETRY_DELAY = 1 +class ClaimOperationError(Exception): + def __init__(self, operation): + self.operation = operation + + def __str__(self): + return f"Operation error: {self.operation}" + + def raise_for_status(response: httpx.Response): if response.is_success: return @@ -26,7 +34,7 @@ def raise_for_status(response: httpx.Response): def raise_for_operation_status(operation: dict): if operation["status"] != "failed": return - raise RuntimeError(f"Operation error: {operation['error']}") + raise ClaimOperationError(operation) class HttpClient: diff --git a/scitt_emulator/scitt.py b/scitt_emulator/scitt.py index 867c8fe9..f33ca89f 100644 --- a/scitt_emulator/scitt.py +++ b/scitt_emulator/scitt.py @@ -4,6 +4,7 @@ from typing import Optional from abc import ABC, abstractmethod from pathlib import Path +import contextlib import time import json import uuid @@ -39,6 +40,10 @@ class OperationNotFoundError(Exception): pass +class PolicyResultDecodeError(Exception): + pass + + class SCITTServiceEmulator(ABC): def __init__( self, service_parameters_path: Path, storage_path: Optional[Path] = None @@ -168,9 +173,21 @@ def _sync_policy_result(self, operation: dict): policy_insert_path.unlink() if policy_failed_path.exists(): policy_result["status"] = "failed" + if policy_failed_path.stat().st_size != 0: + try: + policy_result_error = json.loads(policy_failed_path.read_text()) + except Exception as error: + raise PolicyResultDecodeError(operation_id) from error + policy_result["error"] = policy_result_error policy_failed_path.unlink() if policy_denied_path.exists(): policy_result["status"] = "denied" + if policy_denied_path.stat().st_size != 0: + try: + policy_result_error = json.loads(policy_denied_path.read_text()) + except Exception as error: + raise PolicyResultDecodeError(operation_id) from error + policy_result["error"] = policy_result_error policy_denied_path.unlink() return policy_result @@ -185,7 +202,8 @@ def _finish_operation(self, operation: dict): return operation if policy_result["status"] != "succeeded": operation["status"] = "failed" - operation["error"] = policy_result + if "error" in policy_result: + operation["error"] = policy_result["error"] operation_path.unlink() claim_src_path.unlink() return operation diff --git a/tests/test_docs.py b/tests/test_docs.py index 854953a3..93c1c398 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -3,8 +3,10 @@ import os import sys import time +import json import types import pathlib +import tempfile import threading import itertools import subprocess @@ -15,6 +17,8 @@ import docutils.nodes import docutils.utils +from scitt_emulator.client import ClaimOperationError + from .test_cli import ( Service, content_type, @@ -27,6 +31,7 @@ docs_dir = repo_root.joinpath("docs") blocklisted_issuer = "did:web:example.com" non_blocklisted_issuer = "did:web:example.org" +CLAIM_DENIED_ERROR = {"type": "denied", "detail": "content_address_of_reason"} class SimpleFileBasedPolicyEngine: @@ -68,14 +73,18 @@ def poll_workspace(config, stop_event): stdin=stdin_fileobj, ) # EXIT_SUCCESS from blocklist == MUST block - env = { - **os.environ, - "POLICY_ACTION": { - 0: "denied", - }.get(exit_code, "insert"), - } - command = command_enforce_policy + [cose_path] - exit_code = subprocess.call(command, env=env) + with tempfile.TemporaryDirectory() as tempdir: + policy_reason_path = pathlib.Path(tempdir, "reason.json") + policy_reason_path.write_text(json.dumps(CLAIM_DENIED_ERROR)) + env = { + **os.environ, + "POLICY_REASON_PATH": str(policy_reason_path), + "POLICY_ACTION": { + 0: "denied", + }.get(exit_code, "insert"), + } + command = command_enforce_policy + [cose_path] + exit_code = subprocess.call(command, env=env) time.sleep(0.1) running = not stop_event.is_set() @@ -175,8 +184,14 @@ def test_docs_registration_policies(tmp_path): "--url", service.url ] - with pytest.raises(RuntimeError, match=r"denied"): + check_error = None + try: execute_cli(command) + except ClaimOperationError as error: + check_error = error + assert check_error + assert "error" in check_error.operation + assert check_error.operation["error"] == CLAIM_DENIED_ERROR assert not os.path.exists(receipt_path) assert not os.path.exists(entry_id_path) From ad8aed3941c6242afa62a64d83abad098c14c764 Mon Sep 17 00:00:00 2001 From: John Andersen Date: Thu, 29 Jun 2023 17:18:59 -0700 Subject: [PATCH 06/13] docs: registration_policies: Leverage jsonschema for evaluation of claim Signed-off-by: John Andersen --- docs/registration_policies.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/registration_policies.md b/docs/registration_policies.md index 49b8f4e4..62e7ff51 100644 --- a/docs/registration_policies.md +++ b/docs/registration_policies.md @@ -69,6 +69,7 @@ import json import cbor2 import pycose +from jsonschema import validate from pycose.messages import CoseMessage, Sign1Message from scitt_emulator.scitt import ClaimInvalidError, COSE_Headers_Issuer @@ -84,16 +85,25 @@ claim = sys.stdin.buffer.read() msg = CoseMessage.decode(claim) if pycose.headers.ContentType not in msg.phdr: - raise ClaimInvalidError( - "Claim does not have a content type header parameter" - ) + raise ClaimInvalidError("Claim does not have a content type header parameter") if COSE_Headers_Issuer not in msg.phdr: raise ClaimInvalidError("Claim does not have an issuer header parameter") -if msg.phdr[COSE_Headers_Issuer] not in BLOCKLIST: - sys.exit(1) +if not msg[pycose.headers.ContentType].startswith("application/json"): + raise TypeError( + f"Claim content type does not start with application/json: {msg[pycose.headers.ContentType]!r}" + ) + +SCHEMA = json.loads(pathlib.Path(os.environ["SCHEMA"]).read_text()) -# EXIT_SUCCESS == MUST block. In case of thrown errors/exceptions. +validate( + instance={ + "$schema": "TODO", + "issuer": msg.phdr[COSE_Headers_Issuer], + "cliam": json.loads(msg.payload), + }, + schema=SCHEMA, +) ``` Example running blocklist check and enforcement to disable issuer (example: From 00087fdf818557a5e05d504e21d4cc0a5bc4009b Mon Sep 17 00:00:00 2001 From: John Andersen Date: Thu, 29 Jun 2023 17:32:55 -0700 Subject: [PATCH 07/13] docs: registration_policies: Decode COSE messages payload before JSON load Signed-off-by: John Andersen --- docs/registration_policies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/registration_policies.md b/docs/registration_policies.md index 62e7ff51..c2872e39 100644 --- a/docs/registration_policies.md +++ b/docs/registration_policies.md @@ -100,7 +100,7 @@ validate( instance={ "$schema": "TODO", "issuer": msg.phdr[COSE_Headers_Issuer], - "cliam": json.loads(msg.payload), + "cliam": json.loads(msg.payload.decode()), }, schema=SCHEMA, ) From c6fdd26950b0bb1dd3a17dd393ba20dd58af7567 Mon Sep 17 00:00:00 2001 From: John Andersen Date: Fri, 30 Jun 2023 01:01:08 +0000 Subject: [PATCH 08/13] Add jsonschema to dev-requirements.txt Signed-off-by: John Andersen --- dev-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index b0b31ac2..4b601892 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,6 @@ pyOpenSSL pytest +jsonschema requests==2.31.0 requests-toolbelt==0.9 urllib3<2.0.0 From bd059a8ae44cd94102ec7f649e98e56a65307b68 Mon Sep 17 00:00:00 2001 From: John Andersen Date: Fri, 30 Jun 2023 02:10:26 +0000 Subject: [PATCH 09/13] docs: registration_policies: Add policy_engine.sh wrapper and source policy from service parameters insertPolicy Signed-off-by: John Andersen --- docs/registration_policies.md | 95 ++++++++++++++++++++++++++--------- tests/test_docs.py | 53 ++++++++++++++----- 2 files changed, 112 insertions(+), 36 deletions(-) diff --git a/docs/registration_policies.md b/docs/registration_policies.md index c2872e39..6ad929a9 100644 --- a/docs/registration_policies.md +++ b/docs/registration_policies.md @@ -28,7 +28,7 @@ Modification of config to non-`*` insert policy. Restart SCITT API emulator serv ```console $ echo "$(cat workspace/service_parameters.json)" \ - | jq '.insertPolicy = "external"' \ + | jq '.insertPolicy = "blocklist.schema.json"' \ | tee workspace/service_parameters.json.new \ && mv workspace/service_parameters.json.new workspace/service_parameters.json { @@ -36,7 +36,7 @@ $ echo "$(cat workspace/service_parameters.json)" \ "treeAlgorithm": "CCF", "signatureAlgorithm": "ES256", "serviceCertificate": "-----BEGIN CERTIFICATE-----", - "insertPolicy": "external" + "insertPolicy": "blocklist.schema.json" } ``` @@ -60,26 +60,50 @@ policy_action_path.write_text(policy_reason) Simple drop rule based on claim content blocklist. -**is_on_blocklist.py** +**blocklist.schema.json** + +```json +{ + "$id": "https://schema.example.com/scitt-blocklist.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "$schema": { + "type": "string" + }, + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "issuer": { + "type": "string", + "not": { + "enum": [ + "did:web:example.com" + ] + } + } + } +} +``` + +**jsonschema_validator.py** ```python import os import sys import json +import pathlib +import traceback import cbor2 import pycose -from jsonschema import validate +from jsonschema import validate, ValidationError from pycose.messages import CoseMessage, Sign1Message from scitt_emulator.scitt import ClaimInvalidError, COSE_Headers_Issuer -BLOCKLIST_DEFAULT = [ - "did:web:example.com", -] -BLOCKLIST_DEFAULT_JSON = json.dumps(BLOCKLIST_DEFAULT) -BLOCKLIST = json.loads(os.environ.get("BLOCKLIST", BLOCKLIST_DEFAULT_JSON)) - claim = sys.stdin.buffer.read() msg = CoseMessage.decode(claim) @@ -89,21 +113,39 @@ if pycose.headers.ContentType not in msg.phdr: if COSE_Headers_Issuer not in msg.phdr: raise ClaimInvalidError("Claim does not have an issuer header parameter") -if not msg[pycose.headers.ContentType].startswith("application/json"): +if not msg.phdr[pycose.headers.ContentType].startswith("application/json"): raise TypeError( - f"Claim content type does not start with application/json: {msg[pycose.headers.ContentType]!r}" + f"Claim content type does not start with application/json: {msg.phdr[pycose.headers.ContentType]!r}" ) -SCHEMA = json.loads(pathlib.Path(os.environ["SCHEMA"]).read_text()) +SCHEMA = json.loads(pathlib.Path(os.environ["SCHEMA_PATH"]).read_text()) -validate( - instance={ - "$schema": "TODO", - "issuer": msg.phdr[COSE_Headers_Issuer], - "cliam": json.loads(msg.payload.decode()), - }, - schema=SCHEMA, -) +try: + validate( + instance={ + "$schema": "TODO", + "issuer": msg.phdr[COSE_Headers_Issuer], + "claim": json.loads(msg.payload.decode()), + }, + schema=SCHEMA, + ) +except ValidationError as error: + print(str(error), file=sys.stderr) + sys.exit(1) +``` + +We'll create a small wrapper to serve in place of a more fully featured policy +engine. + +**policy_engine.sh** + +```bash +export SCHEMA_PATH="${1}" +CLAIM_PATH="${2}" + +echo ${CLAIM_PATH} + +(python3 jsonschema_validator.py < ${CLAIM_PATH} 2>error && POLICY_ACTION=insert python3 enforce_policy.py ${CLAIM_PATH}) || (python3 -c 'import sys, json; print(json.dumps({"type": "denied", "detail": json.dumps(sys.stdin.read())}))' < error > reason.json; POLICY_ACTION=denied POLICY_REASON_PATH=reason.json python3 enforce_policy.py ${CLAIM_PATH}) ``` Example running blocklist check and enforcement to disable issuer (example: @@ -111,8 +153,13 @@ Example running blocklist check and enforcement to disable issuer (example: ```console $ npm install -g nodemon -$ echo '{"type": "denied", "detail": "content_address_of_reason"}' | tee reason.json -$ nodemon -e .cose --exec 'find workspace/storage/operations -name \*.cose -exec nohup sh -xc "echo {} && (python3 is_on_blocklist.py < {} && POLICY_ACTION=denied POLICY_REASON_PATH=reason.json python3 enforce_policy.py {}) || POLICY_ACTION=insert python3 enforce_policy.py {}" \;' +$ nodemon -e .cose --exec 'find workspace/storage/operations -name \*.cose -exec nohup sh -xe policy_engine.sh $(cat workspace/service_parameters.json | jq -r .insertPolicy) {} \;' +``` + +Also ensure you restart the server with the new config we edited. + +```console +$ scitt-emulator server --workspace workspace/ --tree-alg CCF --use-lro ``` Create claim from blocked issuer (`.com`) and from non-blocked (`.org`). @@ -132,7 +179,7 @@ Traceback (most recent call last): raise_for_operation_status(operation) File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 37, in raise_for_operation_status raise ClaimOperationError(operation) -scitt_emulator.client.ClaimOperationError: Operation error: {'error': {'detail': 'content_address_of_reason', 'type': 'denied'}, 'operationId': '9693b076-f992-44e1-b7b9-865c600a96f7', 'status': 'failed'} +scitt_emulator.client.ClaimOperationError: Operation error: {'error': {'detail': '"\'did:web:example.com\' should not be valid under {\'enum\': [\'did:web:example.com\']}\\n\\nFailed validating \'not\' in schema[\'properties\'][\'issuer\']:\\n {\'not\': {\'enum\': [\'did:web:example.com\']}, \'type\': \'string\'}\\n\\nOn instance[\'issuer\']:\\n \'did:web:example.com\'\\n"', 'type': 'denied'}, 'operationId': '7bf1101b-ec10-409f-884a-a3747a270394', 'status': 'failed'} $ scitt-emulator client create-claim --issuer did:web:example.org --content-type application/json --payload '{"sun": "yellow"}' --out claim.cose Claim written to claim.cose $ scitt-emulator client submit-claim --claim claim.cose --out claim.receipt.cbor diff --git a/tests/test_docs.py b/tests/test_docs.py index 93c1c398..d776fb5f 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -4,9 +4,11 @@ import sys import time import json +import copy import types import pathlib import tempfile +import textwrap import threading import itertools import subprocess @@ -32,6 +34,20 @@ blocklisted_issuer = "did:web:example.com" non_blocklisted_issuer = "did:web:example.org" CLAIM_DENIED_ERROR = {"type": "denied", "detail": "content_address_of_reason"} +CLAIM_DENIED_ERROR_BLOCKED = { + "type": "denied", + "detail": textwrap.dedent( + """ + 'did:web:example.com' should not be valid under {'enum': ['did:web:example.com']} + + Failed validating 'not' in schema['properties']['issuer']: + {'not': {'enum': ['did:web:example.com']}, 'type': 'string'} + + On instance['issuer']: + 'did:web:example.com' + """ + ).lstrip(), +} class SimpleFileBasedPolicyEngine: @@ -55,9 +71,9 @@ def __exit__(self, *args): @staticmethod def poll_workspace(config, stop_event): operations_path = pathlib.Path(config["storage_path"], "operations") - command_is_on_blocklist = [ + command_jsonschema_validator = [ sys.executable, - str(config["is_on_blocklist"].resolve()), + str(config["jsonschema_validator"].resolve()), ] command_enforce_policy = [ sys.executable, @@ -67,21 +83,33 @@ def poll_workspace(config, stop_event): running = True while running: for cose_path in operations_path.glob("*.cose"): + denial = copy.deepcopy(CLAIM_DENIED_ERROR) with open(cose_path, "rb") as stdin_fileobj: - exit_code = subprocess.call( - command_is_on_blocklist, - stdin=stdin_fileobj, - ) - # EXIT_SUCCESS from blocklist == MUST block + env = { + **os.environ, + "SCHEMA_PATH": str(config["schema_path"].resolve()), + } + exit_code = 0 + try: + subprocess.check_output( + command_jsonschema_validator, + stdin=stdin_fileobj, + stderr=subprocess.STDOUT, + env=env, + ) + except subprocess.CalledProcessError as error: + denial["detail"] = error.output.decode() + exit_code = error.returncode + # EXIT_FAILRUE from validator == MUST block with tempfile.TemporaryDirectory() as tempdir: policy_reason_path = pathlib.Path(tempdir, "reason.json") - policy_reason_path.write_text(json.dumps(CLAIM_DENIED_ERROR)) + policy_reason_path.write_text(json.dumps(denial)) env = { **os.environ, "POLICY_REASON_PATH": str(policy_reason_path), "POLICY_ACTION": { - 0: "denied", - }.get(exit_code, "insert"), + 0: "insert", + }.get(exit_code, "denied"), } command = command_enforce_policy + [cose_path] exit_code = subprocess.call(command, env=env) @@ -149,7 +177,8 @@ def test_docs_registration_policies(tmp_path): { "storage_path": service.server.app.scitt_service.storage_path, "enforce_policy": tmp_path.joinpath("enforce_policy.py"), - "is_on_blocklist": tmp_path.joinpath("is_on_blocklist.py"), + "jsonschema_validator": tmp_path.joinpath("jsonschema_validator.py"), + "schema_path": tmp_path.joinpath("blocklist.schema.json"), } ) as policy_engine: # set the policy to enforce @@ -191,7 +220,7 @@ def test_docs_registration_policies(tmp_path): check_error = error assert check_error assert "error" in check_error.operation - assert check_error.operation["error"] == CLAIM_DENIED_ERROR + assert check_error.operation["error"] == CLAIM_DENIED_ERROR_BLOCKED assert not os.path.exists(receipt_path) assert not os.path.exists(entry_id_path) From f11c77159ffc7ec38afa66d73e6387fee7f5a65d Mon Sep 17 00:00:00 2001 From: John Andersen Date: Fri, 30 Jun 2023 02:25:51 +0000 Subject: [PATCH 10/13] Add jsonschema to envrionment.yml Signed-off-by: John Andersen --- environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/environment.yml b/environment.yml index e0f127cf..d46121c8 100644 --- a/environment.yml +++ b/environment.yml @@ -35,3 +35,4 @@ dependencies: - six==1.16.0 - urllib3<2.0.0 - myst-parser==1.0.0 + - jsonschema==4.17.3 From 47426054a88276cda4dd14c435704f63d8c3b207 Mon Sep 17 00:00:00 2001 From: John Andersen Date: Fri, 30 Jun 2023 02:33:33 +0000 Subject: [PATCH 11/13] docs: registration_polices: Convert example from blocklist to allowlist per @OR13 review Related: https://github.com/pdxjohnny/scitt-api-emulator/pull/1#pullrequestreview-1506375585 Signed-off-by: John Andersen --- docs/registration_policies.md | 34 +++++++++++----------------------- tests/test_docs.py | 18 +++++++++--------- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/docs/registration_policies.md b/docs/registration_policies.md index 6ad929a9..95e871f2 100644 --- a/docs/registration_policies.md +++ b/docs/registration_policies.md @@ -28,7 +28,7 @@ Modification of config to non-`*` insert policy. Restart SCITT API emulator serv ```console $ echo "$(cat workspace/service_parameters.json)" \ - | jq '.insertPolicy = "blocklist.schema.json"' \ + | jq '.insertPolicy = "allowlist.schema.json"' \ | tee workspace/service_parameters.json.new \ && mv workspace/service_parameters.json.new workspace/service_parameters.json { @@ -36,7 +36,7 @@ $ echo "$(cat workspace/service_parameters.json)" \ "treeAlgorithm": "CCF", "signatureAlgorithm": "ES256", "serviceCertificate": "-----BEGIN CERTIFICATE-----", - "insertPolicy": "blocklist.schema.json" + "insertPolicy": "allowlist.schema.json" } ``` @@ -58,31 +58,20 @@ policy_action_path = cose_path.with_suffix(".policy." + os.environ["POLICY_ACTIO policy_action_path.write_text(policy_reason) ``` -Simple drop rule based on claim content blocklist. +Simple drop rule based on claim content allowlist. -**blocklist.schema.json** +**allowlist.schema.json** ```json { - "$id": "https://schema.example.com/scitt-blocklist.schema.json", + "$id": "https://schema.example.com/scitt-allowlist.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { - "$schema": { - "type": "string" - }, - "@context": { - "type": "array", - "items": { - "type": "string" - } - }, "issuer": { "type": "string", - "not": { - "enum": [ - "did:web:example.com" - ] - } + "enum": [ + "did:web:example.org" + ] } } } @@ -123,7 +112,7 @@ SCHEMA = json.loads(pathlib.Path(os.environ["SCHEMA_PATH"]).read_text()) try: validate( instance={ - "$schema": "TODO", + "$schema": "https://schema.example.com/scitt-policy-engine-jsonschema.schema.json", "issuer": msg.phdr[COSE_Headers_Issuer], "claim": json.loads(msg.payload.decode()), }, @@ -148,8 +137,7 @@ echo ${CLAIM_PATH} (python3 jsonschema_validator.py < ${CLAIM_PATH} 2>error && POLICY_ACTION=insert python3 enforce_policy.py ${CLAIM_PATH}) || (python3 -c 'import sys, json; print(json.dumps({"type": "denied", "detail": json.dumps(sys.stdin.read())}))' < error > reason.json; POLICY_ACTION=denied POLICY_REASON_PATH=reason.json python3 enforce_policy.py ${CLAIM_PATH}) ``` -Example running blocklist check and enforcement to disable issuer (example: -`did:web:example.com`). +Example running allowlist check and enforcement. ```console $ npm install -g nodemon @@ -162,7 +150,7 @@ Also ensure you restart the server with the new config we edited. $ scitt-emulator server --workspace workspace/ --tree-alg CCF --use-lro ``` -Create claim from blocked issuer (`.com`) and from non-blocked (`.org`). +Create claim from allowed issuer (`.org`) and from non-allowed (`.com`). ```console $ scitt-emulator client create-claim --issuer did:web:example.com --content-type application/json --payload '{"sun": "yellow"}' --out claim.cose diff --git a/tests/test_docs.py b/tests/test_docs.py index d776fb5f..61806de9 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -31,17 +31,17 @@ repo_root = pathlib.Path(__file__).parents[1] docs_dir = repo_root.joinpath("docs") -blocklisted_issuer = "did:web:example.com" -non_blocklisted_issuer = "did:web:example.org" +allowlisted_issuer = "did:web:example.org" +non_allowlisted_issuer = "did:web:example.com" CLAIM_DENIED_ERROR = {"type": "denied", "detail": "content_address_of_reason"} CLAIM_DENIED_ERROR_BLOCKED = { "type": "denied", "detail": textwrap.dedent( """ - 'did:web:example.com' should not be valid under {'enum': ['did:web:example.com']} + 'did:web:example.com' is not one of ['did:web:example.org'] - Failed validating 'not' in schema['properties']['issuer']: - {'not': {'enum': ['did:web:example.com']}, 'type': 'string'} + Failed validating 'enum' in schema['properties']['issuer']: + {'enum': ['did:web:example.org'], 'type': 'string'} On instance['issuer']: 'did:web:example.com' @@ -133,7 +133,7 @@ def docutils_recursively_extract_nodes(node, samples = None): def docutils_find_code_samples(nodes): samples = {} for i, node in enumerate(nodes): - # Look ahead for next literal block with code sample. Pattern is: + # Look ahead for next literal allow with code sample. Pattern is: # # **strong.suffix** # @@ -178,7 +178,7 @@ def test_docs_registration_policies(tmp_path): "storage_path": service.server.app.scitt_service.storage_path, "enforce_policy": tmp_path.joinpath("enforce_policy.py"), "jsonschema_validator": tmp_path.joinpath("jsonschema_validator.py"), - "schema_path": tmp_path.joinpath("blocklist.schema.json"), + "schema_path": tmp_path.joinpath("allowlist.schema.json"), } ) as policy_engine: # set the policy to enforce @@ -191,7 +191,7 @@ def test_docs_registration_policies(tmp_path): "--out", claim_path, "--issuer", - blocklisted_issuer, + non_allowlisted_issuer, "--content-type", content_type, "--payload", @@ -231,7 +231,7 @@ def test_docs_registration_policies(tmp_path): "--out", claim_path, "--issuer", - non_blocklisted_issuer, + allowlisted_issuer, "--content-type", content_type, "--payload", From 24b8fb28437e5cb48b4acbee23cac5921fe7be41 Mon Sep 17 00:00:00 2001 From: John Andersen Date: Sat, 26 Aug 2023 00:45:05 +0000 Subject: [PATCH 12/13] docs: registration_policies: Fix policy_engine.sh wrarper to remove errantly encode jsonschema validator output into detail string Signed-off-by: John Andersen --- docs/registration_policies.md | 17 ++++++++++++----- scitt_emulator/client.py | 8 +++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/registration_policies.md b/docs/registration_policies.md index 95e871f2..2fc0e235 100644 --- a/docs/registration_policies.md +++ b/docs/registration_policies.md @@ -134,7 +134,7 @@ CLAIM_PATH="${2}" echo ${CLAIM_PATH} -(python3 jsonschema_validator.py < ${CLAIM_PATH} 2>error && POLICY_ACTION=insert python3 enforce_policy.py ${CLAIM_PATH}) || (python3 -c 'import sys, json; print(json.dumps({"type": "denied", "detail": json.dumps(sys.stdin.read())}))' < error > reason.json; POLICY_ACTION=denied POLICY_REASON_PATH=reason.json python3 enforce_policy.py ${CLAIM_PATH}) +(python3 jsonschema_validator.py < ${CLAIM_PATH} 2>error && POLICY_ACTION=insert python3 enforce_policy.py ${CLAIM_PATH}) || (python3 -c 'import sys, json; print(json.dumps({"type": "denied", "detail": sys.stdin.read()}))' < error > reason.json; POLICY_ACTION=denied POLICY_REASON_PATH=reason.json python3 enforce_policy.py ${CLAIM_PATH}) ``` Example running allowlist check and enforcement. @@ -161,13 +161,20 @@ Traceback (most recent call last): sys.exit(load_entry_point('scitt-emulator', 'console_scripts', 'scitt-emulator')()) File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/cli.py", line 22, in main args.func(args) - File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 190, in + File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 196, in func=lambda args: submit_claim( - File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 101, in submit_claim + File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 107, in submit_claim raise_for_operation_status(operation) - File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 37, in raise_for_operation_status + File "/home/alice/Documents/python/scitt-api-emulator/scitt_emulator/client.py", line 43, in raise_for_operation_status raise ClaimOperationError(operation) -scitt_emulator.client.ClaimOperationError: Operation error: {'error': {'detail': '"\'did:web:example.com\' should not be valid under {\'enum\': [\'did:web:example.com\']}\\n\\nFailed validating \'not\' in schema[\'properties\'][\'issuer\']:\\n {\'not\': {\'enum\': [\'did:web:example.com\']}, \'type\': \'string\'}\\n\\nOn instance[\'issuer\']:\\n \'did:web:example.com\'\\n"', 'type': 'denied'}, 'operationId': '7bf1101b-ec10-409f-884a-a3747a270394', 'status': 'failed'} +scitt_emulator.client.ClaimOperationError: Operation error denied: 'did:web:example.com' is not one of ['did:web:example.org'] + +Failed validating 'enum' in schema['properties']['issuer']: + {'enum': ['did:web:example.org'], 'type': 'string'} + +On instance['issuer']: + 'did:web:example.com' + $ scitt-emulator client create-claim --issuer did:web:example.org --content-type application/json --payload '{"sun": "yellow"}' --out claim.cose Claim written to claim.cose $ scitt-emulator client submit-claim --claim claim.cose --out claim.receipt.cbor diff --git a/scitt_emulator/client.py b/scitt_emulator/client.py index 110ac9ac..40645bda 100644 --- a/scitt_emulator/client.py +++ b/scitt_emulator/client.py @@ -22,7 +22,13 @@ def __init__(self, operation): self.operation = operation def __str__(self): - return f"Operation error: {self.operation}" + error_type = self.operation.get("error", {}).get( + "type", "error.type not present", + ) + error_detail = self.operation.get("error", {}).get( + "detail", "error.detail not present", + ) + return f"Operation error {error_type}: {error_detail}" def raise_for_status(response: httpx.Response): From 3806008a2e6d9ec84516dfd49aec2d240446a053 Mon Sep 17 00:00:00 2001 From: John Andersen Date: Wed, 30 Aug 2023 00:14:26 +0000 Subject: [PATCH 13/13] tests: docs: Support for conda test execution via addition of PYTHONPATH To ensure scitt_emulator module can be found Signed-off-by: John Andersen --- tests/test_docs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_docs.py b/tests/test_docs.py index 61806de9..ea3d92d9 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -88,6 +88,10 @@ def poll_workspace(config, stop_event): env = { **os.environ, "SCHEMA_PATH": str(config["schema_path"].resolve()), + "PYTHONPATH": ":".join( + os.environ.get("PYTHONPATH", "").split(":") + + [str(pathlib.Path(__file__).parents[1].resolve())] + ), } exit_code = 0 try: