From a2e561122e00f5162f83f71cb104eea2d3252e7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2019 18:10:10 +0000 Subject: [PATCH 01/22] Bump flask from 0.12.1 to 1.0 Bumps [flask](https://github.com/pallets/flask) from 0.12.1 to 1.0. - [Release notes](https://github.com/pallets/flask/releases) - [Changelog](https://github.com/pallets/flask/blob/master/CHANGES.rst) - [Commits](https://github.com/pallets/flask/compare/0.12.1...1.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 58a2a5e..01e3080 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,5 @@ pytest responses==0.5.1 # Server dependencies -Flask==0.12.1 +Flask==1.0 gunicorn==19.7.1 diff --git a/setup.py b/setup.py index a43d7b9..2623376 100644 --- a/setup.py +++ b/setup.py @@ -68,7 +68,7 @@ 'validators==0.11.2', ], extras_require={ - 'server': ["Flask==0.12.1", 'gunicorn==19.7.1'], + 'server': ["Flask==1.0", 'gunicorn==19.7.1'], }, entry_points=""" [console_scripts] From d3c0ccd768b14f7808f237868c0a7d17dce9b379 Mon Sep 17 00:00:00 2001 From: Jeff Creed Date: Mon, 30 Nov 2020 16:13:20 -0800 Subject: [PATCH 02/22] use json() function of Response object for it's assumptions around json content-type encoding; prevents encoding errors / throws same error as json.loads --- openbadges/verifier/tasks/graph.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openbadges/verifier/tasks/graph.py b/openbadges/verifier/tasks/graph.py index b525086..f528747 100644 --- a/openbadges/verifier/tasks/graph.py +++ b/openbadges/verifier/tasks/graph.py @@ -41,7 +41,8 @@ def fetch_http_node(state, task_meta, **options): ) try: - json.loads(result.text) + json_body = result.json() + response_text_with_proper_encoding = json.dumps(json_body) except ValueError: content_type = result.headers.get('Content-Type', 'UNKNOWN') @@ -65,8 +66,8 @@ def fetch_http_node(state, task_meta, **options): True, 'Successfully fetched image from {}'.format(url), actions) actions = [ - store_original_resource(node_id=url, data=result.text), - add_task(INTAKE_JSON, data=result.text, node_id=url, + store_original_resource(node_id=url, data=response_text_with_proper_encoding), + add_task(INTAKE_JSON, data=response_text_with_proper_encoding, node_id=url, expected_class=task_meta.get('expected_class'), source_node_path=task_meta.get('source_node_path'))] return task_result(message="Successfully fetched JSON data from {}".format(url), actions=actions) From 217a6d7257b9edb7e3ef502fa14fa2bce8141c2e Mon Sep 17 00:00:00 2001 From: Jeff Creed Date: Fri, 4 Dec 2020 08:21:59 -0800 Subject: [PATCH 03/22] BP-4065 store max validation depth in options, pass depth through actions, check max depth before adding validations WIP --- openbadges/verifier/tasks/crypto.py | 16 +- openbadges/verifier/tasks/graph.py | 32 ++-- openbadges/verifier/tasks/input.py | 10 +- openbadges/verifier/tasks/validation.py | 189 ++++++++++++++---------- openbadges/verifier/verifier.py | 9 +- tests/test_graph.py | 6 +- tests/test_image_validation.py | 15 +- tests/test_input.py | 6 +- tests/test_signed_verification.py | 8 +- tests/test_validate_endorsements.py | 10 +- tests/test_validation.py | 51 ++++--- tests/test_verify.py | 4 +- 12 files changed, 215 insertions(+), 141 deletions(-) diff --git a/openbadges/verifier/tasks/crypto.py b/openbadges/verifier/tasks/crypto.py index 10d67fd..7c23842 100644 --- a/openbadges/verifier/tasks/crypto.py +++ b/openbadges/verifier/tasks/crypto.py @@ -26,10 +26,11 @@ def process_jws_input(state, task_meta, **options): node_json = jws.get_unverified_claims(data).decode('utf-8') node_data = json.loads(node_json) node_id = task_meta.get('node_id', node_data.get('id')) + depth = task_meta.get('depth') actions = [ - add_task(INTAKE_JSON, data=node_json, node_id=node_id), - add_task(VERIFY_JWS, node_id=node_id, data=data, prerequisites=SIGNING_KEY_FETCHED) + add_task(INTAKE_JSON, data=node_json, node_id=node_id, depth=depth), + add_task(VERIFY_JWS, node_id=node_id, data=data, prerequisites=SIGNING_KEY_FETCHED, depth=depth) ] if node_id: actions.append(set_validation_subject(node_id)) @@ -42,15 +43,16 @@ def verify_jws_signature(state, task_meta, **options): node_id = task_meta['node_id'] key_node = get_node_by_path(state, [node_id, 'verification', 'creator']) public_pem = key_node['publicKeyPem'] + depth = task_meta['depth'] except (KeyError, IndexError,): raise TaskPrerequisitesError() actions = [ - add_task(VERIFY_KEY_OWNERSHIP, node_id=node_id), + add_task(VERIFY_KEY_OWNERSHIP, node_id=node_id, depth=depth), add_task( VALIDATE_PROPERTY, node_path=[node_id, 'badge', 'issuer'], prop_name='revocationList', prop_type=ValueTypes.ID, expected_class=OBClasses.RevocationList, fetch=True, required=False, - prerequisites=[ISSUER_PROPERTY_DEPENDENCIES] + prerequisites=[ISSUER_PROPERTY_DEPENDENCIES], depth=depth ), ] @@ -73,13 +75,17 @@ def verify_key_ownership(state, task_meta, **options): issuer_node = get_node_by_path(state, [node_id, 'badge', 'issuer']) key_node = get_node_by_path(state, [node_id, 'verification', 'creator']) key_id = key_node['id'] + depth = task_meta['depth'] except (KeyError, IndexError,): raise TaskPrerequisitesError() actions = [] if issuer_node.get('revocationList'): actions.append(add_task( - VERIFY_SIGNED_ASSERTION_NOT_REVOKED, node_id=node_id, prerequisites=[VALIDATE_REVOCATIONLIST_ENTRIES] + VERIFY_SIGNED_ASSERTION_NOT_REVOKED, + node_id=node_id, + prerequisites=[VALIDATE_REVOCATIONLIST_ENTRIES], + depth=depth )) issuer_keys = list_of(issuer_node.get('publicKey')) diff --git a/openbadges/verifier/tasks/graph.py b/openbadges/verifier/tasks/graph.py index f528747..3ea2179 100644 --- a/openbadges/verifier/tasks/graph.py +++ b/openbadges/verifier/tasks/graph.py @@ -29,6 +29,7 @@ def fetch_http_node(state, task_meta, **options): url = task_meta['url'] + depth = task_meta['depth'] if options.get('cache_backend'): session = requests_cache.CachedSession( @@ -60,16 +61,21 @@ def fetch_http_node(state, task_meta, **options): b64content = b''.join([b'data:', parsed_type.encode(), b';base64,', base64.b64encode(result.content)]) actions = [store_original_resource(node_id=url, data=b64content)] if task_meta.get('is_potential_baked_input', False): - actions += [add_task(PROCESS_BAKED_RESOURCE, node_id=url)] + actions += [add_task(PROCESS_BAKED_RESOURCE, node_id=url, depth=depth)] return task_result( True, 'Successfully fetched image from {}'.format(url), actions) actions = [ store_original_resource(node_id=url, data=response_text_with_proper_encoding), - add_task(INTAKE_JSON, data=response_text_with_proper_encoding, node_id=url, + add_task(INTAKE_JSON, + data=response_text_with_proper_encoding, + node_id=url, expected_class=task_meta.get('expected_class'), - source_node_path=task_meta.get('source_node_path'))] + source_node_path=task_meta.get('source_node_path'), + depth=depth + ) + ] return task_result(message="Successfully fetched JSON data from {}".format(url), actions=actions) @@ -92,6 +98,7 @@ def intake_json(state, task_meta, **options): input_data = task_meta['data'] node_id = task_meta.get('node_id') expected_class = task_meta.get('expected_class') + depth = task_meta.get('depth') openbadges_version = None actions = [] @@ -105,8 +112,13 @@ def intake_json(state, task_meta, **options): if openbadges_version in ['1.1', '2.0']: compact_action = add_task( - JSONLD_COMPACT_DATA, node_id=node_id, openbadges_version=openbadges_version, - expected_class=expected_class, data=input_data, source_node_path=task_meta.get('source_node_path') + JSONLD_COMPACT_DATA, + node_id=node_id, + openbadges_version=openbadges_version, + expected_class=expected_class, + data=input_data, + source_node_path=task_meta.get('source_node_path'), + depth=depth ) actions.append(compact_action) @@ -170,6 +182,7 @@ def jsonld_compact_data(state, task_meta, **options): data = data.decode() input_data = json.loads(data) expected_class = task_meta.get('expected_class') + depth = task_meta.get('depth') except TypeError: return task_result(False, "Could not load data") @@ -185,7 +198,7 @@ def jsonld_compact_data(state, task_meta, **options): # Handle mismatch between URL node source and declared ID. if result.get('id') and task_meta.get('node_id') and result['id'] != task_meta['node_id']: - refetch_action = add_task(FETCH_HTTP_NODE, url=result['id']) + refetch_action = add_task(FETCH_HTTP_NODE, url=result['id'], depth=depth) if expected_class: refetch_action['expected_class'] = expected_class actions = [ @@ -214,10 +227,10 @@ def jsonld_compact_data(state, task_meta, **options): if expected_class: actions.append( add_task(VALIDATE_EXPECTED_NODE_CLASS, node_id=node_id, - expected_class=expected_class) + expected_class=expected_class, depth=depth) ) elif task_meta.get('detectAndValidateClass', True): - actions.append(add_task(DETECT_AND_VALIDATE_NODE_CLASS, node_id=node_id)) + actions.append(add_task(DETECT_AND_VALIDATE_NODE_CLASS, node_id=node_id, depth=depth)) return task_result( True, @@ -232,6 +245,7 @@ def flatten_refetch_embedded_resource(state, task_meta, **options): node = get_node_by_id(state, node_id) prop_name = task_meta['prop_name'] node_class = task_meta['node_class'] + depth = task_meta['depth'] except (IndexError, KeyError): raise TaskPrerequisitesError() @@ -285,6 +299,6 @@ def flatten_refetch_embedded_resource(state, task_meta, **options): if not node_match_exists(state, embedded_node_id) and not filter_tasks( state, node_id=embedded_node_id, task_type=FETCH_HTTP_NODE): # fetch - actions.append(add_task(FETCH_HTTP_NODE, url=embedded_node_id)) + actions.append(add_task(FETCH_HTTP_NODE, url=embedded_node_id, depth=depth)) return task_result(True, "Embedded {} node in {} queued for storage and/or refetching as needed", actions) diff --git a/openbadges/verifier/tasks/input.py b/openbadges/verifier/tasks/input.py index 58f5e8e..ae31926 100644 --- a/openbadges/verifier/tasks/input.py +++ b/openbadges/verifier/tasks/input.py @@ -55,6 +55,7 @@ def detect_input_type(state, task_meta, **options): Detects what data format user has provided and saves to the store. """ input_value = state.get('input').get('value') + depth = task_meta.get('depth') detected_type = None new_actions = [] @@ -62,7 +63,7 @@ def detect_input_type(state, task_meta, **options): detected_type = 'url' new_actions.append(set_input_type(detected_type)) new_actions.append(add_task( - FETCH_HTTP_NODE, url=input_value, is_potential_baked_input=task_meta.get('is_potential_baked_input', True) + FETCH_HTTP_NODE, url=input_value, is_potential_baked_input=task_meta.get('is_potential_baked_input', True), depth=depth )) new_actions.append(set_validation_subject(input_value)) elif input_is_json(input_value): @@ -82,12 +83,12 @@ def detect_input_type(state, task_meta, **options): message_level=MESSAGE_LEVEL_ERROR )) elif detected_type == 'url': - new_actions.append(add_task(FETCH_HTTP_NODE, url=id_url, is_potential_baked_input=False)) + new_actions.append(add_task(FETCH_HTTP_NODE, url=id_url, is_potential_baked_input=False, depth=depth)) new_actions.append(set_validation_subject(id_url)) elif input_is_jws(input_value): detected_type = 'jws' new_actions.append(set_input_type(detected_type)) - new_actions.append(add_task(PROCESS_JWS_INPUT, data=input_value)) + new_actions.append(add_task(PROCESS_JWS_INPUT, data=input_value, depth=depth)) else: raise NotImplementedError("only URL, JSON, or JWS input implemented so far") @@ -101,6 +102,7 @@ def process_baked_resource(state, task_meta, **options): try: node_id = task_meta['node_id'] resource_b64 = state['input']['original_json'][node_id] + depth = task_meta['depth'] except KeyError: raise TaskPrerequisitesError() try: @@ -121,7 +123,7 @@ def process_baked_resource(state, task_meta, **options): if assertion_data: actions = [ store_input(assertion_data), - add_task(DETECT_INPUT_TYPE, is_potential_baked_input=False) + add_task(DETECT_INPUT_TYPE, is_potential_baked_input=False, depth=depth) ] return task_result(True, "Retrieved baked data from image resource {}".format(abv(node_id)), actions) else: diff --git a/openbadges/verifier/tasks/validation.py b/openbadges/verifier/tasks/validation.py index 6f55b37..89fa21b 100644 --- a/openbadges/verifier/tasks/validation.py +++ b/openbadges/verifier/tasks/validation.py @@ -23,9 +23,10 @@ VERIFY_RECIPIENT_IDENTIFIER) from .utils import (abbreviate_value as abv, abbreviate_node_id as abv_node, - is_blank_node_id, is_data_uri, is_empty_list, is_null_list, is_iri, is_url, task_result,) + is_blank_node_id, is_data_uri, is_empty_list, is_null_list, is_iri, is_url, task_result, ) from future.standard_library import install_aliases + install_aliases() from urllib.parse import urlparse @@ -104,6 +105,7 @@ class PrimitiveValueValidator(object): PrimitiveValueValidator(ValueTypes.TEXT)("test value") > True """ + def __init__(self, value_type): value_check_functions = { ValueTypes.BOOLEAN: self._validate_boolean, @@ -164,10 +166,9 @@ def _validate_datetime(value): # we also require tzinfo specification on our datetime strings # NOTE -- does not catch minus-sign (non-ascii char) tzinfo delimiter return (isinstance(value, six.string_types) and - (value[-1:]=='Z' or + (value[-1:] == 'Z' or bool(re.match(r'.*[+-](?:\d{4}|\d{2}|\d{2}:\d{2})$', value)))) - @staticmethod def _validate_email(value): return bool(re.match(r'(^[^@\s]+@[^@\s]+$)', value)) @@ -203,7 +204,7 @@ def _validate_markdown_text(cls, value): @classmethod def _validate_rdf_type(cls, value): try: - if not(isinstance(value, six.string_types)): + if not (isinstance(value, six.string_types)): raise ValidationError( 'RDF_TYPE entry {} must be a string value'.format(abv(value))) @@ -269,6 +270,7 @@ def validate_property(state, task_meta, **options): prop_type = task_meta.get('prop_type') required = bool(task_meta.get('required')) allow_many = task_meta.get('many') + depth = task_meta.get('depth') actions = [] @@ -283,7 +285,7 @@ def validate_property(state, task_meta, **options): return task_result( False, "Required property {} not present in {} {}".format( prop_name, node_class, abv_node(node_id, node_path)) - ) + ) if not isinstance(prop_value, (list, tuple)): values_to_test = [prop_value] @@ -325,7 +327,10 @@ def validate_property(state, task_meta, **options): actions.append( add_task(VALIDATE_EXPECTED_NODE_CLASS, node_path=value_to_test_path, expected_class=task_meta.get('expected_class'), - full_validate=task_meta.get('full_validate', True))) + full_validate=task_meta.get('full_validate', True), + depth=depth + ) + ) continue elif task_meta.get('allow_data_uri') and not PrimitiveValueValidator(ValueTypes.DATA_URI_OR_URL)(val): raise ValidationError("ID-type property {} had value `{}` that isn't URI or DATA URI in {}.".format( @@ -356,12 +361,16 @@ def validate_property(state, task_meta, **options): actions.append( add_task(FETCH_HTTP_NODE, url=val, expected_class=task_meta.get('expected_class'), - source_node_path=value_to_test_path + source_node_path=value_to_test_path, + depth=depth )) else: actions.append( add_task(VALIDATE_EXPECTED_NODE_CLASS, node_id=val, - expected_class=task_meta.get('expected_class'))) + expected_class=task_meta.get('expected_class'), + depth=depth + ) + ) except ValidationError as e: return task_result(False, e.message) @@ -416,48 +425,48 @@ def __init__(self, class_name): self.validators = [ {'prop_name': 'id', 'prop_type': ValueTypes.IRI, 'required': True}, {'prop_name': 'type', 'prop_type': ValueTypes.RDF_TYPE, 'required': True, - 'many': True, 'must_contain_one': ['Assertion']}, + 'many': True, 'must_contain_one': ['Assertion']}, {'prop_name': 'recipient', 'prop_type': ValueTypes.ID, - 'expected_class': OBClasses.IdentityObject, 'required': True}, + 'expected_class': OBClasses.IdentityObject, 'required': True}, {'prop_name': 'badge', 'prop_type': ValueTypes.ID, 'prerequisites': 'ASN_FLATTEN_BC', - 'expected_class': OBClasses.BadgeClass, 'fetch': True, 'required': True}, + 'expected_class': OBClasses.BadgeClass, 'fetch': True, 'required': True}, {'prop_name': 'verification', 'prop_type': ValueTypes.ID, - 'expected_class': OBClasses.VerificationObjectAssertion, 'required': True}, + 'expected_class': OBClasses.VerificationObjectAssertion, 'required': True}, {'prop_name': 'issuedOn', 'prop_type': ValueTypes.DATETIME, 'required': True}, {'prop_name': 'expires', 'prop_type': ValueTypes.DATETIME, 'required': False}, {'prop_name': 'image', 'prop_type': ValueTypes.ID, 'required': False, 'allow_remote_url': True, 'expected_class': OBClasses.Image, 'fetch': False, 'allow_data_uri': False}, {'prop_name': 'narrative', 'prop_type': ValueTypes.MARKDOWN_TEXT, 'required': False}, {'prop_name': 'evidence', 'prop_type': ValueTypes.ID, 'allow_remote_url': True, - 'expected_class': OBClasses.Evidence, 'many': True, 'fetch': False, 'required': False}, + 'expected_class': OBClasses.Evidence, 'many': True, 'fetch': False, 'required': False}, {'task_type': ASSERTION_VERIFICATION_DEPENDENCIES, 'prerequisites': ISSUER_PROPERTY_DEPENDENCIES}, {'task_type': ASSERTION_TIMESTAMP_CHECKS}, {'task_type': IMAGE_VALIDATION, 'prop_name': 'image', 'node_class': OBClasses.Assertion, - 'required': False, 'many': False, 'allow_data_uri': False}, + 'required': False, 'many': False, 'allow_data_uri': False}, {'task_type': FLATTEN_EMBEDDED_RESOURCE, 'prop_name': 'badge', 'node_class': OBClasses.Assertion, - 'task_key': 'ASN_FLATTEN_BC'} + 'task_key': 'ASN_FLATTEN_BC'} ] elif class_name == OBClasses.BadgeClass: self.validators = [ {'prop_name': 'id', 'prop_type': ValueTypes.IRI, 'required': True}, {'prop_name': 'type', 'prop_type': ValueTypes.RDF_TYPE, 'required': True, - 'many': True, 'must_contain_one': ['BadgeClass']}, + 'many': True, 'must_contain_one': ['BadgeClass']}, {'prop_name': 'issuer', 'prop_type': ValueTypes.ID, 'prerequisites': 'BC_FLATTEN_ISS', - 'expected_class': OBClasses.Profile, 'fetch': True, 'required': True}, + 'expected_class': OBClasses.Profile, 'fetch': True, 'required': True}, {'prop_name': 'name', 'prop_type': ValueTypes.TEXT, 'required': True}, {'prop_name': 'description', 'prop_type': ValueTypes.TEXT, 'required': True}, {'prop_name': 'image', 'prop_type': ValueTypes.ID, 'required': False, 'allow_remote_url': True, 'expected_class': OBClasses.Image, 'fetch': False, 'allow_data_uri': True}, {'prop_name': 'criteria', 'prop_type': ValueTypes.ID, - 'expected_class': OBClasses.Criteria, 'fetch': False, - 'required': True, 'allow_remote_url': True}, + 'expected_class': OBClasses.Criteria, 'fetch': False, + 'required': True, 'allow_remote_url': True}, {'prop_name': 'alignment', 'prop_type': ValueTypes.ID, - 'expected_class': OBClasses.AlignmentObject, 'many': True, 'fetch': False, 'required': False}, + 'expected_class': OBClasses.AlignmentObject, 'many': True, 'fetch': False, 'required': False}, {'prop_name': 'tags', 'prop_type': ValueTypes.TEXT, 'many': True, 'required': False}, {'task_type': IMAGE_VALIDATION, 'prop_name': 'image', 'node_class': OBClasses.BadgeClass, - 'required': True, 'many': False, 'allow_data_uri': True}, + 'required': True, 'many': False, 'allow_data_uri': True}, {'task_type': FLATTEN_EMBEDDED_RESOURCE, 'prop_name': 'issuer', 'node_class': OBClasses.BadgeClass, - 'task_key': 'BC_FLATTEN_ISS'} + 'task_key': 'BC_FLATTEN_ISS'} ] elif class_name == OBClasses.CryptographicKey: self.validators = [ @@ -473,7 +482,7 @@ def __init__(self, class_name): self.validators = [ {'prop_name': 'id', 'prop_type': ValueTypes.IRI, 'required': True}, {'prop_name': 'type', 'prop_type': ValueTypes.RDF_TYPE, 'required': True, - 'many': True, 'must_contain_one': ['Issuer', 'Profile']}, + 'many': True, 'must_contain_one': ['Issuer', 'Profile']}, {'prop_name': 'name', 'prop_type': ValueTypes.TEXT, 'required': True}, {'prop_name': 'description', 'prop_type': ValueTypes.TEXT, 'required': False}, {'prop_name': 'image', 'prop_type': ValueTypes.ID, 'required': False, 'allow_remote_url': True, @@ -482,7 +491,7 @@ def __init__(self, class_name): {'prop_name': 'email', 'prop_type': ValueTypes.EMAIL, 'required': True}, {'prop_name': 'telephone', 'prop_type': ValueTypes.TELEPHONE, 'required': False}, {'prop_name': 'publicKey', 'prop_type': ValueTypes.ID, - 'expected_class': OBClasses.CryptographicKey, 'fetch': True, 'required': False}, + 'expected_class': OBClasses.CryptographicKey, 'fetch': True, 'required': False}, {'prop_name': 'verification', 'prop_type': ValueTypes.ID, 'expected_class': OBClasses.VerificationObjectIssuer, 'fetch': False, 'required': False}, {'task_type': ISSUER_PROPERTY_DEPENDENCIES, 'messageLevel': MESSAGE_LEVEL_WARNING} @@ -501,7 +510,7 @@ def __init__(self, class_name): {'prop_name': 'email', 'prop_type': ValueTypes.EMAIL, 'required': False, 'many': True}, {'prop_name': 'telephone', 'prop_type': ValueTypes.TELEPHONE, 'required': False, 'many': True}, {'prop_name': 'publicKey', 'prop_type': ValueTypes.ID, 'many': True, - 'expected_class': OBClasses.CryptographicKey, 'fetch': False, 'required': False}, + 'expected_class': OBClasses.CryptographicKey, 'fetch': False, 'required': False}, {'prop_name': 'verification', 'prop_type': ValueTypes.ID, 'expected_class': OBClasses.VerificationObjectIssuer, 'fetch': False, 'required': False}, {'task_type': VERIFY_RECIPIENT_IDENTIFIER, 'prerequisites': ASSERTION_VERIFICATION_DEPENDENCIES} @@ -509,7 +518,7 @@ def __init__(self, class_name): elif class_name == OBClasses.AlignmentObject: self.validators = [ {'prop_name': 'type', 'prop_type': ValueTypes.RDF_TYPE, - 'many': True, 'required': False, 'default': OBClasses.AlignmentObject}, + 'many': True, 'required': False, 'default': OBClasses.AlignmentObject}, {'prop_name': 'targetName', 'prop_type': ValueTypes.TEXT, 'required': True}, {'prop_name': 'targetUrl', 'prop_type': ValueTypes.URL, 'required': True}, {'prop_name': 'description', 'prop_type': ValueTypes.TEXT, 'required': False}, @@ -519,7 +528,7 @@ def __init__(self, class_name): elif class_name == OBClasses.Criteria: self.validators = [ {'prop_name': 'type', 'prop_type': ValueTypes.RDF_TYPE, - 'many': True, 'required': False, 'default': OBClasses.Criteria}, + 'many': True, 'required': False, 'default': OBClasses.Criteria}, {'prop_name': 'id', 'prop_type': ValueTypes.IRI, 'required': False}, {'prop_name': 'narrative', 'prop_type': ValueTypes.MARKDOWN_TEXT, 'required': False}, {'task_type': CRITERIA_PROPERTY_DEPENDENCIES} @@ -527,7 +536,7 @@ def __init__(self, class_name): elif class_name == OBClasses.IdentityObject: self.validators = [ {'prop_name': 'type', 'prop_type': ValueTypes.RDF_TYPE, 'required': True, 'many': False, - 'must_contain_one': ['id', 'email', 'url', 'telephone']}, + 'must_contain_one': ['id', 'email', 'url', 'telephone']}, {'prop_name': 'identity', 'prop_type': ValueTypes.IDENTITY_HASH, 'required': True}, {'prop_name': 'hashed', 'prop_type': ValueTypes.BOOLEAN, 'required': True}, {'prop_name': 'salt', 'prop_type': ValueTypes.TEXT, 'required': False}, @@ -536,7 +545,7 @@ def __init__(self, class_name): elif class_name == OBClasses.Evidence: self.validators = [ {'prop_name': 'type', 'prop_type': ValueTypes.RDF_TYPE, 'many': True, - 'required': False, 'default': 'Evidence'}, + 'required': False, 'default': 'Evidence'}, {'prop_name': 'id', 'prop_type': ValueTypes.IRI, 'required': False}, {'prop_name': 'narrative', 'prop_type': ValueTypes.MARKDOWN_TEXT, 'required': False}, {'prop_name': 'name', 'prop_type': ValueTypes.TEXT, 'required': False}, @@ -547,7 +556,7 @@ def __init__(self, class_name): elif class_name == OBClasses.Image: self.validators = [ {'prop_name': 'type', 'prop_type': ValueTypes.RDF_TYPE, 'many': True, - 'required': False, 'default': 'schema:ImageObject'}, + 'required': False, 'default': 'schema:ImageObject'}, {'prop_name': 'id', 'prop_type': ValueTypes.DATA_URI_OR_URL, 'required': True}, {'prop_name': 'caption', 'prop_type': ValueTypes.TEXT, 'required': False}, {'prop_name': 'author', 'prop_type': ValueTypes.IRI, 'required': False}, @@ -556,15 +565,15 @@ def __init__(self, class_name): elif class_name == OBClasses.VerificationObjectAssertion: self.validators = [ {'prop_name': 'type', 'prop_type': ValueTypes.RDF_TYPE, 'required': True, 'many': False, - 'must_contain_one': ['HostedBadge', 'SignedBadge']}, + 'must_contain_one': ['HostedBadge', 'SignedBadge']}, {'prop_name': 'creator', 'prop_type': ValueTypes.ID, - 'expected_class': OBClasses.CryptographicKey, 'fetch': True, 'required': False, - 'prerequisites': ASSERTION_VERIFICATION_DEPENDENCIES}, + 'expected_class': OBClasses.CryptographicKey, 'fetch': True, 'required': False, + 'prerequisites': ASSERTION_VERIFICATION_DEPENDENCIES}, ] elif class_name == OBClasses.VerificationObjectIssuer: self.validators = [ {'prop_name': 'type', 'prop_type': ValueTypes.RDF_TYPE, 'required': False, 'many': True, - 'default': 'VerificationObject'}, + 'default': 'VerificationObject'}, {'prop_name': 'verificationProperty', 'prop_type': ValueTypes.COMPACT_IRI, 'required': False}, {'prop_name': 'startsWith', 'prop_type': ValueTypes.URL, 'required': False}, {'prop_name': 'allowedOrigins', 'prop_type': ValueTypes.URL_AUTHORITY, 'required': False, @@ -613,9 +622,10 @@ def __init__(self, class_name): ] -def _get_validation_actions(node_class, node_id=None, node_path=None): +def _get_validation_actions(node_class, depth, node_id=None, node_path=None): validators = ClassValidators(node_class).validators actions = [] + print(f'{" ":<{depth * 4}}{node_class}') for validator in validators: if validator.get('prop_type') == ValueTypes.RDF_TYPE: action = add_task(VALIDATE_RDF_TYPE_PROPERTY, **validator) @@ -631,6 +641,7 @@ def _get_validation_actions(node_class, node_id=None, node_path=None): else: action['node_path'] = node_path + action['depth'] = depth + 1 actions.append(action) return actions @@ -638,66 +649,82 @@ def _get_validation_actions(node_class, node_id=None, node_path=None): def detect_and_validate_node_class(state, task_meta, **options): node_id = task_meta.get('node_id') node_path = task_meta.get('node_path') + depth = task_meta.get('depth') + max_depth = options.get('max_validation_depth') + actions = [] - if node_id: - node = get_node_by_id(state, node_id) - else: - node = get_node_by_path(state, node_path) + if depth <= max_depth: + if node_id: + node = get_node_by_id(state, node_id) + else: + node = get_node_by_path(state, node_path) - declared_node_type = node.get('type') - node_class = None + declared_node_type = node.get('type') + node_class = None - for ob_class in OBClasses.ALL_CLASSES: - if declared_node_type == ob_class: - node_class = OBClasses.default_for(ob_class) - break + for ob_class in OBClasses.ALL_CLASSES: + if declared_node_type == ob_class: + node_class = OBClasses.default_for(ob_class) + break - actions = _get_validation_actions(node_class, node_id, node_path) + actions += _get_validation_actions(node_class, depth, node_id, node_path) - # Filter list for related nodes down to props that exist and 'id' - if task_meta.get('full_validate', True) is False: - actions = [a for a in actions - if a.get('prop_name') == 'id' - or a.get('prop_name') is not None and node.get(a['prop_name']) is not None] + # Filter list for related nodes down to props that exist and 'id' + if task_meta.get('full_validate', True) is False: + actions = [a for a in actions + if a.get('prop_name') == 'id' + or a.get('prop_name') is not None and node.get(a['prop_name']) is not None] - return task_result( - True, "Declared type on node {} is {}".format(abv_node(node_id, node_path), declared_node_type), - actions - ) + return task_result( + True, "Declared type on node {} is {}".format(abv_node(node_id, node_path), declared_node_type), + actions + ) + else: + return task_result( + True, "Reached max validation depth", actions + ) def validate_expected_node_class(state, task_meta, **options): node_id = task_meta.get('node_id') node_path = task_meta.get('node_path') - - node_classes = [OBClasses.default_for(c) for c in list_of(task_meta['expected_class'])] + depth = task_meta.get('depth') + max_depth = options.get('max_validation_depth') actions = [] - for node_class in node_classes: - actions += _get_validation_actions(node_class, node_id, node_path) - # Filter list for related nodes down to props that exist and 'id' - if task_meta.get('full_validate', True) is False: - if node_id: - node = get_node_by_id(state, node_id) - else: - node = get_node_by_path(state, node_path) - actions = [a for a in actions - if a.get('prop_name', 'UNKNOWN') in ['id', 'endorsementComment'] - or a.get('prop_name') is not None and node.get(a['prop_name']) is not None - or a.get('prop_name') is None and a.get('run_non_core') is True] + if depth <= max_depth: + node_classes = [OBClasses.default_for(c) for c in list_of(task_meta['expected_class'])] + for node_class in node_classes: + actions += _get_validation_actions(node_class, depth, node_id, node_path) + # Filter list for related nodes down to props that exist and 'id' + if task_meta.get('full_validate', True) is False: + if node_id: + node = get_node_by_id(state, node_id) + else: + node = get_node_by_path(state, node_path) + actions = [a for a in actions + if a.get('prop_name', 'UNKNOWN') in ['id', 'endorsementComment'] + or a.get('prop_name') is not None and node.get(a['prop_name']) is not None + or a.get('prop_name') is None and a.get('run_non_core') is True] - return task_result( - True, "Queued property validations for class {} instance {}".format( - node_class, abv_node(node_id, node_path)), - actions - ) + return task_result( + True, "Queued property validations for class {} instance {}".format( + node_class, abv_node(node_id, node_path)), + actions + ) + else: + return task_result( + True, "Reached max validation depth", actions + ) """ Class Validation Tasks """ + + def identity_object_property_dependencies(state, task_meta, **options): node_id = task_meta.get('node_id') node_path = task_meta.get('node_path') @@ -741,9 +768,9 @@ def criteria_property_dependencies(state, task_meta, **options): is_blank_id_node = bool(re.match(r'_:b\d+$', node_id)) if is_blank_id_node and not node.get('narrative'): return task_result(False, - "Criteria node {} has no narrative. Either external id or narrative is required.".format( - abv_node(node_id, node_path)) - ) + "Criteria node {} has no narrative. Either external id or narrative is required.".format( + abv_node(node_id, node_path)) + ) elif is_blank_id_node: return task_result( True, "Criteria node {} is a narrative-based piece of evidence.".format( @@ -809,7 +836,7 @@ def assertion_timestamp_checks(state, task_meta, **options): return task_result( False, "Assertion {} expiration is prior to issue date.".format(node_id)) - if expires < now : + if expires < now: return task_result( False, "Assertion {} expired on {}".format(node_id, assertion['expires']) ) @@ -828,7 +855,7 @@ def issuer_property_dependencies(state, task_meta, **options): if not bool(re.match('^http(s)?://', node_id)): return task_result( False, "Issuer Profile {} not hosted with HTTP-based identifier.".format(node_id) + - "Many platforms can only handle HTTP(s)-hosted issuers.") + "Many platforms can only handle HTTP(s)-hosted issuers.") return task_result(True, "Issuer profile id meets expectations.") @@ -867,8 +894,8 @@ def validate_revocationlist_entries(state, task_meta, **options): )) else: return task_result(False, "RevocationList {} has entry with id {} not in IRI format".format( - node_id, entry - )) + node_id, entry + )) # Node flattening system will insert all entries into graph. return task_result(True) diff --git a/openbadges/verifier/verifier.py b/openbadges/verifier/verifier.py index e07ca0a..9c80db6 100644 --- a/openbadges/verifier/verifier.py +++ b/openbadges/verifier/verifier.py @@ -23,7 +23,8 @@ 'use_cache': True, 'cache_backend': 'memory', 'cache_expire_after': 300, - 'jsonld_options': jsonld_use_cache + 'jsonld_options': jsonld_use_cache, + 'max_validation_depth': 3 } @@ -106,7 +107,7 @@ def verification_store(badge_input, recipient_profile=None, store=None, options= store.dispatch(resolve_task(task.get('task_id'), success=False, result=e.message)) else: store.dispatch(store_input(badge_data)) - store.dispatch(add_task(tasks.DETECT_INPUT_TYPE)) + store.dispatch(add_task(tasks.DETECT_INPUT_TYPE, depth=0)) if recipient_profile: profile_id = recipient_profile.get('id') @@ -114,7 +115,9 @@ def verification_store(badge_input, recipient_profile=None, store=None, options= task = add_task( JSONLD_COMPACT_DATA, data=json.dumps(recipient_profile), - expected_class=OBClasses.ExpectedRecipientProfile) + expected_class=OBClasses.ExpectedRecipientProfile, + depth=0 + ) if profile_id: task['node_id'] = profile_id store.dispatch(task) diff --git a/tests/test_graph.py b/tests/test_graph.py index e013a5a..982a9d4 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -34,7 +34,7 @@ def test_basic_http_fetch_task(self): body=test_components['2_0_basic_assertion'], status=200, content_type='application/ld+json' ) - task = add_task(FETCH_HTTP_NODE, url=url) + task = add_task(FETCH_HTTP_NODE, url=url, depth=0) success, message, actions = fetch_http_node({}, task) @@ -53,7 +53,7 @@ def test_svg_fetch_with_complex_mimetype(self): body=svg_circle, status=200, content_type='image/svg+xml; charset=utf-8' ) - task = add_task(FETCH_HTTP_NODE, url=url) + task = add_task(FETCH_HTTP_NODE, url=url, depth=0) success, message, actions = fetch_http_node({}, task) @@ -229,7 +229,7 @@ def test_node_has_id_different_from_fetch_url(self): responses.add(responses.GET, first_url, json=node_data) responses.add(responses.GET, second_url, json=node_data) - task = add_task(FETCH_HTTP_NODE, url=second_url) + task = add_task(FETCH_HTTP_NODE, url=second_url, depth=0) state = {'graph': []} result, message, actions = run_task(state, task) diff --git a/tests/test_image_validation.py b/tests/test_image_validation.py index 9932b76..b225811 100644 --- a/tests/test_image_validation.py +++ b/tests/test_image_validation.py @@ -19,7 +19,8 @@ def test_validate_badgeclass_image_formats(self): session = CachedSession(backend='memory', expire_after=100000) loader = CachableDocumentLoader(use_cache=True, session=session) options = { - 'jsonld_options': {'documentLoader': loader} + 'jsonld_options': {'documentLoader': loader}, + 'max_validation_depth': 3 } image_url = 'http://example.org/awesomebadge.png' badgeclass = { @@ -35,7 +36,7 @@ def test_validate_badgeclass_image_formats(self): self.assertEqual(response.status_code, 200) task_meta = add_task( - VALIDATE_EXPECTED_NODE_CLASS, node_id=badgeclass['id'], expected_class=OBClasses.BadgeClass) + VALIDATE_EXPECTED_NODE_CLASS, node_id=badgeclass['id'], expected_class=OBClasses.BadgeClass, depth=0) result, message, actions = task_named(VALIDATE_EXPECTED_NODE_CLASS)(state, task_meta, **options) self.assertTrue(result) @@ -88,7 +89,8 @@ def test_base64_data_uri_in_badgeclass(self): session = CachedSession(backend='memory', expire_after=100000) loader = CachableDocumentLoader(use_cache=True, session=session) options = { - 'jsonld_options': {'documentLoader': loader} + 'jsonld_options': {'documentLoader': loader}, + 'max_validation_depth': 3 } badgeclass = { 'id': 'http://example.org/badgeclass', @@ -98,7 +100,7 @@ def test_base64_data_uri_in_badgeclass(self): state = {'graph': [badgeclass]} task_meta = add_task( - VALIDATE_EXPECTED_NODE_CLASS, node_id=badgeclass['id'], expected_class=OBClasses.BadgeClass) + VALIDATE_EXPECTED_NODE_CLASS, node_id=badgeclass['id'], expected_class=OBClasses.BadgeClass, depth=0) result, message, actions = task_named(VALIDATE_EXPECTED_NODE_CLASS)(state, task_meta, **options) self.assertTrue(result) @@ -144,7 +146,8 @@ def test_base64_data_uri_in_assertion(self): session = CachedSession(backend='memory', expire_after=100000) loader = CachableDocumentLoader(use_cache=True, session=session) options = { - 'jsonld_options': {'documentLoader': loader} + 'jsonld_options': {'documentLoader': loader}, + 'max_validation_depth': 3 } assertion = { 'id': 'http://example.org/assertion', @@ -153,7 +156,7 @@ def test_base64_data_uri_in_assertion(self): state = {'graph': [assertion]} task_meta = add_task( - VALIDATE_EXPECTED_NODE_CLASS, node_id=assertion['id'], expected_class=OBClasses.Assertion) + VALIDATE_EXPECTED_NODE_CLASS, node_id=assertion['id'], expected_class=OBClasses.Assertion, depth=0) result, message, actions = task_named(VALIDATE_EXPECTED_NODE_CLASS)(state, task_meta, **options) self.assertTrue(result) diff --git a/tests/test_input.py b/tests/test_input.py index 5268d4c..4eaf60f 100644 --- a/tests/test_input.py +++ b/tests/test_input.py @@ -223,7 +223,7 @@ def test_fetch_task_handles_potential_baked_input(self): responses.add(responses.GET, image_url, body=baked_file.read(), status=200, content_type='image/png') - task = add_task(FETCH_HTTP_NODE, url=image_url, is_potential_baked_input=True) + task = add_task(FETCH_HTTP_NODE, url=image_url, is_potential_baked_input=True, depth=0) result, message, actions = run_task({}, task) self.assertTrue(result) @@ -233,7 +233,7 @@ def test_fetch_task_handles_potential_baked_input(self): self.assertEqual(store_resource_action.get('node_id'), image_url) self.assertEqual(process_baked_input_action.get('node_id'), image_url) - task = add_task(FETCH_HTTP_NODE, url=image_url, is_potential_baked_input=False) + task = add_task(FETCH_HTTP_NODE, url=image_url, is_potential_baked_input=False, depth=0) result, message, actions = run_task({}, task) self.assertTrue(result) @@ -248,7 +248,7 @@ def test_process_baked_resource(self): } } } - task_meta = add_task(PROCESS_BAKED_RESOURCE, node_id=image_url) + task_meta = add_task(PROCESS_BAKED_RESOURCE, node_id=image_url, depth=0) result, message, actions = process_baked_resource(state, task_meta) self.assertTrue(result) self.assertEqual(len(actions), 2) diff --git a/tests/test_signed_verification.py b/tests/test_signed_verification.py index 75ead4d..6761e4a 100644 --- a/tests/test_signed_verification.py +++ b/tests/test_signed_verification.py @@ -68,7 +68,7 @@ def setUp(self): } def test_can_process_jws_input(self): - task_meta = add_task(PROCESS_JWS_INPUT, data=self.signature) + task_meta = add_task(PROCESS_JWS_INPUT, data=self.signature, depth=0) state = {} success, message, actions = process_jws_input(state, task_meta) @@ -77,7 +77,7 @@ def test_can_process_jws_input(self): def test_can_verify_jws(self): task_meta = add_task(VERIFY_JWS, data=self.signature, - node_id=self.assertion_data['id']) + node_id=self.assertion_data['id'], depth=0) success, message, actions = verify_jws_signature(self.state, task_meta) self.assertTrue(success) @@ -99,7 +99,7 @@ def test_can_verify_jws(self): self.signed_assertion = encoded_separator.join((original_header, encoded_payload, original_signature)) task_meta = add_task(VERIFY_JWS, data=self.signed_assertion, - node_id=self.assertion_data['id']) + node_id=self.assertion_data['id'], depth=0) success, message, actions = verify_jws_signature(self.state, task_meta) self.assertFalse(success) @@ -107,7 +107,7 @@ def test_can_verify_jws(self): def test_can_verify_key_ownership(self): state = self.state - task_meta = add_task(VERIFY_KEY_OWNERSHIP, node_id=self.assertion_data['id']) + task_meta = add_task(VERIFY_KEY_OWNERSHIP, node_id=self.assertion_data['id'], depth=0) result, message, actions = verify_key_ownership(state, task_meta) self.assertTrue(result) diff --git a/tests/test_validate_endorsements.py b/tests/test_validate_endorsements.py index e0fd937..3a61bc7 100644 --- a/tests/test_validate_endorsements.py +++ b/tests/test_validate_endorsements.py @@ -7,6 +7,7 @@ from openbadges.verifier.tasks.task_types import VALIDATE_EXPECTED_NODE_CLASS from openbadges.verifier.tasks.validation import OBClasses from openbadges import verify +from openbadges.verifier.tasks import task_named from .utils import set_up_context_mock, set_up_image_mock @@ -122,17 +123,20 @@ def test_validate_endorsement_as_input(self): def test_claim_property_validation(self): self.set_up_resources() + options = {'max_validation_depth': 3} + state = {'graph': [self.endorsement]} task_meta = add_task( VALIDATE_EXPECTED_NODE_CLASS, node_id=self.endorsement['id'], prop_name='claim', - expected_class=OBClasses.Endorsement + expected_class=OBClasses.Endorsement, + depth=0 ) - result, message, actions = run_task(state, task_meta) + result, message, actions = task_named(VALIDATE_EXPECTED_NODE_CLASS)(state, task_meta, **options) self.assertTrue(result) claim_action = [a for a in actions if a.get('prop_name') == 'claim'][0] - result, message, actions = run_task(state, claim_action) + result, message, actions = task_named(VALIDATE_EXPECTED_NODE_CLASS)(state, claim_action, **options) self.assertTrue(result) self.assertEqual(len(actions), 1) diff --git a/tests/test_validation.py b/tests/test_validation.py index d3a92de..418f992 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -757,18 +757,19 @@ def test_many_nested_validation_for_id_property(self): ] } state = {'graph': [first_node]} + options = {'max_validation_depth': 3} required = True task = add_task( VALIDATE_PROPERTY, node_id=first_node['id'], node_class=OBClasses.BadgeClass, prop_name='alignment', prop_type=ValueTypes.ID, required=required, many=True, fetch=False, - expected_class=OBClasses.AlignmentObject, allow_remote_url=False + expected_class=OBClasses.AlignmentObject, allow_remote_url=False, depth=0 ) result, message, actions = validate_property(state, task) self.assertTrue(result, "Task the queues up individual class validators is successful") self.assertEqual(len(actions), 1) - result, message, actions = task_named(actions[0]['name'])(state, actions[0]) + result, message, actions = task_named(actions[0]['name'])(state, actions[0], **options) self.assertTrue(result) for a in actions: self.assertEqual(a['node_path'], [first_node['id'], 'alignment', 0]) @@ -803,6 +804,7 @@ def test_validate_identity_object_property_dependencies(self): } } state = {'graph': [first_node]} + options = {'max_validation_depth': 3} task = add_task( VALIDATE_PROPERTY, @@ -810,7 +812,8 @@ def test_validate_identity_object_property_dependencies(self): prop_name="recipient", required=True, prop_type=ValueTypes.ID, - expected_class=OBClasses.IdentityObject + expected_class=OBClasses.IdentityObject, + depth=0 ) def run(cur_state, cur_task, expected_result, msg=''): @@ -820,7 +823,7 @@ def run(cur_state, cur_task, expected_result, msg=''): self.assertEqual(actions[0]['expected_class'], OBClasses.IdentityObject) cur_task = actions[0] - result, message, actions = task_named(cur_task['name'])(cur_state, cur_task) + result, message, actions = task_named(cur_task['name'])(cur_state, cur_task, **options) self.assertTrue(result, "IdentityObject validation task discovery should succeed.") for cur_task in [a for a in actions if a.get('type') == ADD_TASK]: @@ -889,20 +892,22 @@ def test_run_criteria_task_discovery_and_validation(self): {'id': 'http://example.com/b', 'name': 'Another property outside of Criteria class scope'}, ] } + options = {'max_validation_depth': 3} actions = [add_task( VALIDATE_PROPERTY, node_id=badgeclass_node['id'], prop_name="criteria", required=False, prop_type=ValueTypes.ID, - expected_class=OBClasses.Criteria + expected_class=OBClasses.Criteria, + depth=0 )] badgeclass_node['criteria'] = 'http://example.com/a' for task in actions: if not task.get('type') == 'ADD_TASK': continue - result, message, new_actions = task_named(task['name'])(state, task) + result, message, new_actions = task_named(task['name'])(state, task, **options) if new_actions: actions.extend(new_actions) self.assertTrue(result) @@ -914,13 +919,15 @@ def test_run_criteria_task_discovery_and_validation_embedded(self): state = { 'graph': [badgeclass_node] } + options = {'max_validation_depth': 3} actions = [add_task( VALIDATE_PROPERTY, node_id=badgeclass_node['id'], prop_name="criteria", required=False, prop_type=ValueTypes.ID, - expected_class=OBClasses.Criteria + expected_class=OBClasses.Criteria, + depth=0 )] badgeclass_node['criteria'] = { 'narrative': 'Do the important things.' @@ -929,7 +936,7 @@ def test_run_criteria_task_discovery_and_validation_embedded(self): for task in actions: if not task.get('type') == 'ADD_TASK': continue - result, message, new_actions = task_named(task['name'])(state, task) + result, message, new_actions = task_named(task['name'])(state, task, **options) if new_actions: actions.extend(new_actions) self.assertTrue(result) @@ -939,6 +946,7 @@ def test_run_criteria_task_discovery_and_validation_embedded(self): def test_many_criteria_disallowed(self): badgeclass_node = {'id': 'http://example.com/badgeclass', 'type': 'BadgeClass'} state = {'graph': [badgeclass_node]} + options = {'max_validation_depth': 3} actions = [add_task( VALIDATE_PROPERTY, node_id=badgeclass_node['id'], @@ -950,7 +958,7 @@ def test_many_criteria_disallowed(self): badgeclass_node['criteria'] = ['http://example.com/a', 'http://example.com/b'] task = actions[0] - result, message, new_actions = task_named(task['name'])(state, task) + result, message, new_actions = task_named(task['name'])(state, task, **options) self.assertFalse(result, "Validation should reject multiple criteria entries") self.assertTrue('has more than the single allowed value' in message) @@ -985,16 +993,17 @@ def _setUpEvidenceState(self): ]} def _run(self, task_meta, expected_result, msg='', test_task='UNKNOWN'): + options = {'max_validation_depth': 3} result, message, actions = validate_property(self.state, task_meta) self.assertTrue(result, "Property validation task should succeed.") self.assertEqual(len(actions), 1) task_meta = actions[0] - result, message, actions = task_named(task_meta['name'])(self.state, task_meta) + result, message, actions = task_named(task_meta['name'])(self.state, task_meta, **options) self.assertTrue(result, "Class validation task discovery should succeed.") for task_meta in [a for a in actions if a.get('type') == ADD_TASK]: - val_result, val_message, val_actions = task_named(task_meta['name'])(self.state, task_meta) + val_result, val_message, val_actions = task_named(task_meta['name'])(self.state, task_meta, **options) if not task_meta['name'] == test_task: self.assertTrue(val_result, "Test {} should pass".format(task_meta['name'])) elif task_meta['name'] == test_task: @@ -1013,7 +1022,8 @@ def test_evidence_class_validation(self): prop_name="evidence", required=False, prop_type=ValueTypes.ID, - expected_class=OBClasses.Evidence + expected_class=OBClasses.Evidence, + depth=0 ) self._run(task, True, 'Single embedded complete evidence node passes') @@ -1073,7 +1083,8 @@ def test_alignment_object_validation(self): prop_name="alignment", required=False, prop_type=ValueTypes.ID, - expected_class=OBClasses.AlignmentObject + expected_class=OBClasses.AlignmentObject, + depth=0 ) self._run(task, True, 'Single embedded complete alignment node passes', test_task=None) @@ -1095,7 +1106,7 @@ def test_basic_badgeclass_validation(self): second_node = {'id': '_:b0', 'narrative': 'Do the important learning.'} state = {'graph': [first_node, second_node]} - actions = _get_validation_actions(OBClasses.BadgeClass, first_node['id']) + actions = _get_validation_actions(OBClasses.BadgeClass, 0, first_node['id']) results = [] for action in actions: if action['type'] == 'ADD_TASK': @@ -1393,15 +1404,16 @@ def test_both_issuer_and_profile_queue_class_validation(self): } state = {'graph': [issuer]} + options = {'max_validation_depth': 3} task_meta = add_task(VALIDATE_EXPECTED_NODE_CLASS, node_id=issuer['id'], - expected_class=issuer['type']) + expected_class=issuer['type'], depth=0) - result, message, actions = task_named(task_meta['name'])(state, task_meta) + result, message, actions = task_named(task_meta['name'])(state, task_meta, **options) self.assertTrue(result) - task_meta = add_task(DETECT_AND_VALIDATE_NODE_CLASS, node_id=issuer['id']) - result, message, actions = task_named(task_meta['name'])(state, task_meta) + task_meta = add_task(DETECT_AND_VALIDATE_NODE_CLASS, node_id=issuer['id'], depth=0) + result, message, actions = task_named(task_meta['name'])(state, task_meta, **options) self.assertTrue(result) def test_issuer_warn_on_non_https_id(self): @@ -1412,10 +1424,11 @@ def test_issuer_warn_on_non_https_id(self): 'url': 'http://example.com' } state = {'graph': [issuer]} + options = {'max_validation_depth': 3} task_meta = add_task(ISSUER_PROPERTY_DEPENDENCIES, node_id=issuer['id'], messageLevel=MESSAGE_LEVEL_WARNING) - result, message, actions = task_named(task_meta['name'])(state, task_meta) + result, message, actions = task_named(task_meta['name'])(state, task_meta, **options) self.assertFalse(result) self.assertIn('HTTP', message) diff --git a/tests/test_verify.py b/tests/test_verify.py index d897f14..23fb401 100644 --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -88,10 +88,12 @@ def test_verify_of_baked_image(self): content_type='application/ld+json' ) + options = {'max_validation_depth': 3} + with open(png_badge, 'rb') as image: baked_image = bake(image, test_components['2_0_basic_assertion']) responses.add(responses.GET, 'https://example.org/baked', body=baked_image.read(), content_type='image/png') - results = verify(baked_image) + results = verify(baked_image, None, **options) # verify gets the JSON out of the baked image, and then detect_input_type # will reach out to the assertion URL to fetch the canonical assertion (thus, From fde364cdc7e0036f31018260ec4bc587933fad51 Mon Sep 17 00:00:00 2001 From: Jeff Creed Date: Tue, 8 Dec 2020 07:23:08 -0800 Subject: [PATCH 04/22] provide options to tests for max validation depth --- openbadges/verifier/tasks/validation.py | 5 ++--- tests/test_validate_endorsements.py | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/openbadges/verifier/tasks/validation.py b/openbadges/verifier/tasks/validation.py index 89fa21b..7649209 100644 --- a/openbadges/verifier/tasks/validation.py +++ b/openbadges/verifier/tasks/validation.py @@ -651,7 +651,6 @@ def detect_and_validate_node_class(state, task_meta, **options): node_path = task_meta.get('node_path') depth = task_meta.get('depth') max_depth = options.get('max_validation_depth') - actions = [] if depth <= max_depth: if node_id: @@ -667,7 +666,7 @@ def detect_and_validate_node_class(state, task_meta, **options): node_class = OBClasses.default_for(ob_class) break - actions += _get_validation_actions(node_class, depth, node_id, node_path) + actions = _get_validation_actions(node_class, depth, node_id, node_path) # Filter list for related nodes down to props that exist and 'id' if task_meta.get('full_validate', True) is False: @@ -681,7 +680,7 @@ def detect_and_validate_node_class(state, task_meta, **options): ) else: return task_result( - True, "Reached max validation depth", actions + True, "Reached max validation depth", [] ) diff --git a/tests/test_validate_endorsements.py b/tests/test_validate_endorsements.py index 3a61bc7..a5ebe28 100644 --- a/tests/test_validate_endorsements.py +++ b/tests/test_validate_endorsements.py @@ -4,7 +4,7 @@ from openbadges.verifier.actions.tasks import add_task from openbadges.verifier.openbadges_context import OPENBADGES_CONTEXT_V2_URI from openbadges.verifier.tasks import run_task -from openbadges.verifier.tasks.task_types import VALIDATE_EXPECTED_NODE_CLASS +from openbadges.verifier.tasks.task_types import VALIDATE_EXPECTED_NODE_CLASS, VALIDATE_PROPERTY from openbadges.verifier.tasks.validation import OBClasses from openbadges import verify from openbadges.verifier.tasks import task_named @@ -136,10 +136,10 @@ def test_claim_property_validation(self): self.assertTrue(result) claim_action = [a for a in actions if a.get('prop_name') == 'claim'][0] - result, message, actions = task_named(VALIDATE_EXPECTED_NODE_CLASS)(state, claim_action, **options) + result, message, actions = task_named(claim_action['name'])(state, claim_action, **options) self.assertTrue(result) self.assertEqual(len(actions), 1) - result, message, actions = run_task(state, actions[0]) + result, message, actions = task_named(actions[0]['name'])(state, actions[0], **options) self.assertTrue(result) self.assertEqual(len(actions), 3) From 982f566d7eb8d7a3256643b52dc0b392d9e97510 Mon Sep 17 00:00:00 2001 From: Jeff Creed Date: Tue, 8 Dec 2020 07:47:49 -0800 Subject: [PATCH 05/22] add depth to validate_related tests --- tests/test_validate_related.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/test_validate_related.py b/tests/test_validate_related.py index 33712be..48f3c8b 100644 --- a/tests/test_validate_related.py +++ b/tests/test_validate_related.py @@ -1,12 +1,14 @@ import unittest from openbadges.verifier.actions.tasks import add_task -from openbadges.verifier.tasks import run_task +from openbadges.verifier.tasks import run_task, task_named from openbadges.verifier.tasks.task_types import DETECT_AND_VALIDATE_NODE_CLASS class RelatedObjectTests(unittest.TestCase): def test_validate_related_language(self): + options = {'max_validation_depth': 3} + assertion = { 'type': 'Assertion', 'id': 'http://example.com/assertion', @@ -27,8 +29,8 @@ def test_validate_related_language(self): 'name': 'Insignia Pronto' } state = {'graph': [assertion, badgeclass]} - task_meta = add_task(DETECT_AND_VALIDATE_NODE_CLASS, node_id=badgeclass['id']) - result, message, actions = run_task(state, task_meta) + task_meta = add_task(DETECT_AND_VALIDATE_NODE_CLASS, node_id=badgeclass['id'], depth=0) + result, message, actions = task_named(DETECT_AND_VALIDATE_NODE_CLASS)(state, task_meta, **options) self.assertTrue(result) language_task = [t for t in actions if t.get('prop_name') == '@language'][0] @@ -36,9 +38,9 @@ def test_validate_related_language(self): self.assertTrue(r, "The BadgeClass's language property is valid.") related_task = [t for t in actions if t.get('prop_name') == 'related'][0] - result, message, actions = run_task(state, related_task) + result, message, actions = task_named(related_task['name'])(state, related_task, **options) self.assertTrue(result, "The related property is valid and queues up task discovery for embedded node") - result, message, actions = run_task(state, actions[0]) + result, message, actions = task_named(actions[0]['name'])(state, actions[0], **options) self.assertTrue(result, "Some tasks are discovered to validate the related node.") self.assertEqual(len(actions), 2, "There are only tasks for 'id' and '@language'.") @@ -47,6 +49,8 @@ def test_validate_related_language(self): self.assertTrue(r, "Related node property validation is successful.") def test_validate_multiple_related_languages(self): + options = {'max_validation_depth': 3} + assertion = { 'type': 'Assertion', 'id': 'http://example.com/assertion', @@ -71,15 +75,15 @@ def test_validate_multiple_related_languages(self): 'name': 'Insignia Pronto' } state = {'graph': [assertion, badgeclass]} - task_meta = add_task(DETECT_AND_VALIDATE_NODE_CLASS, node_id=badgeclass['id']) - result, message, actions = run_task(state, task_meta) + task_meta = add_task(DETECT_AND_VALIDATE_NODE_CLASS, node_id=badgeclass['id'], depth=0) + result, message, actions = task_named(DETECT_AND_VALIDATE_NODE_CLASS)(state, task_meta, **options) self.assertTrue(result) language_task = [t for t in actions if t.get('prop_name') == '@language'][0] - r, _, __ = run_task(state, language_task) + r, _, __ = task_named(language_task['name'])(state, language_task, **options) self.assertTrue(r, "The BadgeClass's language property is valid.") related_task = [t for t in actions if t.get('prop_name') == 'related'][0] - result, message, actions = run_task(state, related_task) + result, message, actions = task_named(related_task['name'])(state, related_task, **options) self.assertTrue(result, "The related property is valid and queues up task discovery for embedded node") self.assertEqual(len(actions), 2, "It has now discovered two nodes to test.") From 1040a8d8c8592ea6226036d4491598442adbf3cd Mon Sep 17 00:00:00 2001 From: Jeff Creed Date: Tue, 8 Dec 2020 07:51:30 -0800 Subject: [PATCH 06/22] make language test runnable, add depth --- tests/validate_language.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/validate_language.py b/tests/validate_language.py index 6cbcab8..5599ce9 100644 --- a/tests/validate_language.py +++ b/tests/validate_language.py @@ -1,25 +1,27 @@ import unittest from openbadges.verifier.actions.tasks import add_task -from openbadges.verifier.tasks import run_task +from openbadges.verifier.tasks import task_named from openbadges.verifier.tasks.task_types import VALIDATE_EXPECTED_NODE_CLASS from openbadges.verifier.tasks.validation import OBClasses class ValidateLanguagePropertyTests(unittest.TestCase): - def validate_language_prop_basic(self): + def test_validate_language_prop_basic(self): + options = {'max_validation_depth': 3} + badgeclass = { 'id': 'http://example.org/badgeclass', '@language': 'en-US' } state = {'graph': [badgeclass]} task = add_task(VALIDATE_EXPECTED_NODE_CLASS, node_id=badgeclass['id'], - expected_class=OBClasses.BadgeClass) - result, message, actions = run_task(state, task) + expected_class=OBClasses.BadgeClass, depth=0) + result, message, actions = task_named(VALIDATE_EXPECTED_NODE_CLASS)(state, task, **options) self.assertTrue(result) l_actions = [a for a in actions if a.get('prop_name') == '@language'] self.assertEqual(len(l_actions), 1) - result, message, actions = run_task(state, l_actions[0]) + result, message, actions = task_named(l_actions[0]['name'])(state, l_actions[0], **options) self.assertTrue(result) From 56e833a68e6c7fea1286b7ccad5b2acb5b3a05ad Mon Sep 17 00:00:00 2001 From: Nate Otto Date: Tue, 8 Dec 2020 13:29:49 -0800 Subject: [PATCH 07/22] Relax specific version requirements to better handle new pip dependency version conflict resolution strictness --- requirements.txt | 8 ++++---- setup.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2075009..ca5f1f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,17 @@ # Duplicates from setup.py aniso8601>=1.2.0 -future==0.16.0 +future>=0.16.0 jsonschema==2.6.0 language-tags==0.4.3 -openbadges_bakery==1.1.0 +openbadges_bakery>=1.1.0 pycryptodome==3.6.6 pydux==0.2.2 PyLD==0.7.1 python-jose==3.0.1 python-mimeparse==1.6.0 pytz==2017.2 -requests >= 2.13 -requests_cache >= 0.4.13 +requests>=2.13 +requests_cache>=0.4.13 rfc3986==0.4.1 validators==0.11.2 diff --git a/setup.py b/setup.py index 78d3d38..1f883db 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ install_requires=[ 'aniso8601>=1.2.0', 'Click == 6.7', - 'future==0.16.0', + 'future>=0.16.0', 'jsonschema==2.6.0', 'language-tags==0.4.3', 'openbadges-bakery>=1.1.0', @@ -63,7 +63,7 @@ 'python-mimeparse==1.6.0', 'pytz==2017.2', 'requests >= 2.13', - 'requests_cache==0.4.13', + 'requests_cache>=0.4.13', 'rfc3986==0.4.1', 'validators==0.11.2', ], From 52bcfeb19d5e2e5316c99e29b6ee068152be6d5d Mon Sep 17 00:00:00 2001 From: Jeff Creed Date: Wed, 9 Dec 2020 07:18:16 -0800 Subject: [PATCH 08/22] fix format string tuple index error --- openbadges/verifier/tasks/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbadges/verifier/tasks/graph.py b/openbadges/verifier/tasks/graph.py index 3ea2179..71de467 100644 --- a/openbadges/verifier/tasks/graph.py +++ b/openbadges/verifier/tasks/graph.py @@ -252,7 +252,7 @@ def flatten_refetch_embedded_resource(state, task_meta, **options): actions = [] value = node.get(prop_name) if value is None: - return task_result(True, "Expected property {} was missing in node {}".format(node_id)) + return task_result(True, "Expected property {} was missing in node {}".format(prop_name, node_id)) if isinstance(value, six.string_types): return task_result( From 8d51a559841813b32ab8508d17d80fca5c827cef Mon Sep 17 00:00:00 2001 From: Jeff Creed Date: Wed, 9 Dec 2020 07:22:16 -0800 Subject: [PATCH 09/22] update equality assertions to have expected result first (for correct error reporting) --- tests/test_validate_endorsements.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_validate_endorsements.py b/tests/test_validate_endorsements.py index a5ebe28..bd4a11b 100644 --- a/tests/test_validate_endorsements.py +++ b/tests/test_validate_endorsements.py @@ -92,7 +92,7 @@ def test_validate_linked_endorsement(self): results = verify(self.assertion['id']) self.assertTrue(results['report']['valid']) - self.assertEqual(len(results['graph']), 5, "The graph now contains all five resources.") + self.assertEqual(5, len(results['graph']), "The graph now contains all five resources.") @responses.activate def test_validate_linked_endorsement_array(self): @@ -105,7 +105,7 @@ def test_validate_linked_endorsement_array(self): results = verify(self.assertion['id']) self.assertTrue(results['report']['valid']) - self.assertEqual(len(results['graph']), 6, "The graph now contains all six resources including both endorsements.") + self.assertEqual(6, len(results['graph']), "The graph now contains all six resources including both endorsements.") @responses.activate def test_validate_endorsement_as_input(self): @@ -118,7 +118,7 @@ def test_validate_endorsement_as_input(self): results = verify(self.endorsement['id']) self.assertTrue(results['report']['valid']) - self.assertTrue(len(results['graph']), 2) + self.assertEqual(2, len(results['graph'])) def test_claim_property_validation(self): self.set_up_resources() @@ -138,8 +138,8 @@ def test_claim_property_validation(self): result, message, actions = task_named(claim_action['name'])(state, claim_action, **options) self.assertTrue(result) - self.assertEqual(len(actions), 1) + self.assertEqual(1, len(actions)) result, message, actions = task_named(actions[0]['name'])(state, actions[0], **options) self.assertTrue(result) - self.assertEqual(len(actions), 3) + self.assertEqual(3, len(actions)) From 058da6a550161ca2000502cdb5949bd5cd6fc817 Mon Sep 17 00:00:00 2001 From: Jeff Creed Date: Wed, 9 Dec 2020 08:17:31 -0800 Subject: [PATCH 10/22] BP-4065 add flattening to endorsement validators, allow flattening to handle arrays as well as objects --- openbadges/verifier/tasks/graph.py | 67 +++++++++++++------------ openbadges/verifier/tasks/validation.py | 5 +- tests/test_validate_endorsements.py | 15 +++++- 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/openbadges/verifier/tasks/graph.py b/openbadges/verifier/tasks/graph.py index 71de467..87c8fce 100644 --- a/openbadges/verifier/tasks/graph.py +++ b/openbadges/verifier/tasks/graph.py @@ -254,51 +254,56 @@ def flatten_refetch_embedded_resource(state, task_meta, **options): if value is None: return task_result(True, "Expected property {} was missing in node {}".format(prop_name, node_id)) - if isinstance(value, six.string_types): + if isinstance(value, six.string_types) or (isinstance(value, list) and all(isinstance(s, six.string_types) for s in value)): return task_result( True, "Property {} referenced from {} is not embedded in need of flattening".format( prop_name, abv_node(node_id=node_id) )) if not isinstance(value, dict): - return task_result( - False, "Property {} referenced from {} is not a JSON object or string as expected".format( - prop_name, abv_node(node_id=node_id) - )) - embedded_node_id = value.get('id') - - if embedded_node_id is None: - new_node = value.copy() - embedded_node_id = '_:{}'.format(uuid.uuid4()) - new_node['id'] = embedded_node_id - new_node['@context'] = OPENBADGES_CONTEXT_V2_URI - actions.append(add_node(embedded_node_id, data=new_node)) - actions.append(patch_node(node_id, {prop_name: embedded_node_id})) - actions.append(report_message( - 'Node id missing at {}. A blank node ID has been assigned'.format( - abv_node(node_path=[node_id, prop_name], length=64) - ), message_level=MESSAGE_LEVEL_WARNING) - ) - elif not isinstance(embedded_node_id, six.string_types) or not is_iri(embedded_node_id): - return task_result(False, "Embedded JSON object at {} has no proper assigned id.".format( - abv_node(node_path=[node_id, prop_name]))) + if not isinstance(value, list): + return task_result( + False, "Property {} referenced from {} is not a JSON object, JSON object list, or string as expected".format( + prop_name, abv_node(node_id=node_id) + )) + else: + value = [value] + + for node in value: + embedded_node_id = node.get('id') - elif node_class == OBClasses.Assertion and not is_url(embedded_node_id): + if embedded_node_id is None: + new_node = node.copy() + embedded_node_id = '_:{}'.format(uuid.uuid4()) + new_node['id'] = embedded_node_id + new_node['@context'] = OPENBADGES_CONTEXT_V2_URI + actions.append(add_node(embedded_node_id, data=new_node)) + actions.append(patch_node(node_id, {prop_name: embedded_node_id})) + actions.append(report_message( + 'Node id missing at {}. A blank node ID has been assigned'.format( + abv_node(node_path=[node_id, prop_name], length=64) + ), message_level=MESSAGE_LEVEL_WARNING) + ) + elif not isinstance(embedded_node_id, six.string_types) or not is_iri(embedded_node_id): + return task_result(False, "Embedded JSON object at {} has no proper assigned id.".format( + abv_node(node_path=[node_id, prop_name]))) + + elif node_class == OBClasses.Assertion and not is_url(embedded_node_id): if not re.match(URN_REGEX, embedded_node_id, re.IGNORECASE): actions.append(report_message( 'ID format for {} at {} not in an expected HTTP or URN:UUID scheme'.format( embedded_node_id, abv_node(node_path=[node_id, prop_name]) ))) - new_node = value.copy() + new_node = node.copy() new_node['@context'] = OPENBADGES_CONTEXT_V2_URI - actions.append(add_node(embedded_node_id, data=value)) + actions.append(add_node(embedded_node_id, data=node)) actions.append(patch_node(node_id, {prop_name: embedded_node_id})) - else: - actions.append(patch_node(node_id, {prop_name: embedded_node_id})) - if not node_match_exists(state, embedded_node_id) and not filter_tasks( + else: + actions.append(patch_node(node_id, {prop_name: embedded_node_id})) + if not node_match_exists(state, embedded_node_id) and not filter_tasks( state, node_id=embedded_node_id, task_type=FETCH_HTTP_NODE): - # fetch - actions.append(add_task(FETCH_HTTP_NODE, url=embedded_node_id, depth=depth)) + # fetch + actions.append(add_task(FETCH_HTTP_NODE, url=embedded_node_id, depth=depth)) - return task_result(True, "Embedded {} node in {} queued for storage and/or refetching as needed", actions) + return task_result(True, "Embedded {} node in {} queued for storage and/or refetching as needed", actions) \ No newline at end of file diff --git a/openbadges/verifier/tasks/validation.py b/openbadges/verifier/tasks/validation.py index 7649209..959fd79 100644 --- a/openbadges/verifier/tasks/validation.py +++ b/openbadges/verifier/tasks/validation.py @@ -618,7 +618,10 @@ def __init__(self, class_name): 'fetch': False, 'allow_data_uri': False, 'expected_class': class_name, 'full_validate': False, 'many': True}, {'prop_name': 'endorsement', 'prop_type': ValueTypes.ID, 'required': False, 'allow_remote_url': True, - 'fetch': True, 'allow_data_uri': False, 'expected_class': OBClasses.Endorsement, 'many': True} + 'fetch': True, 'allow_data_uri': False, 'expected_class': OBClasses.Endorsement, 'many': True, + 'prerequisites': ['PRI_FLATTEN_ENDRS']}, + {'task_type': FLATTEN_EMBEDDED_RESOURCE, 'prop_name': 'endorsement', 'node_class': OBClasses.Endorsement, + 'task_key': 'PRI_FLATTEN_ENDRS'} ] diff --git a/tests/test_validate_endorsements.py b/tests/test_validate_endorsements.py index bd4a11b..bd6f76c 100644 --- a/tests/test_validate_endorsements.py +++ b/tests/test_validate_endorsements.py @@ -31,7 +31,20 @@ def set_up_resources(self): 'id': 'http://example.com/badgeclass', 'type': 'BadgeClass', 'issuer': 'http://example.com/issuer', - 'endorsement': ['http://example.org/endorsement'], + 'endorsement': { + '@context': OPENBADGES_CONTEXT_V2_URI, + 'id': 'http://example.org/endorsement', + 'type': 'Endorsement', + 'claim': { + 'id': 'http://example.com/badgeclass', + 'endorsementComment': 'Pretty good' + }, + 'issuedOn': '2017-10-01T00:00Z', + 'issuer': 'http://example.org/issuer', + 'verification': { + 'type': "HostedBadge" + } + }, 'name': 'Best Badge', 'description': 'An achievement that is good.', 'image': 'http://example.com/badgeimage', From c2c1149833c7f1a3e878113cce5f1c7bb6478dc2 Mon Sep 17 00:00:00 2001 From: Jeff Creed Date: Wed, 9 Dec 2020 09:38:43 -0800 Subject: [PATCH 11/22] BP-4065 add validation depth test, remove debug output --- openbadges/verifier/tasks/validation.py | 1 - tests/test_validate_endorsements.py | 3 +- tests/test_validation.py | 109 +++++++++++++++++++++++- 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/openbadges/verifier/tasks/validation.py b/openbadges/verifier/tasks/validation.py index 959fd79..7f3ae24 100644 --- a/openbadges/verifier/tasks/validation.py +++ b/openbadges/verifier/tasks/validation.py @@ -628,7 +628,6 @@ def __init__(self, class_name): def _get_validation_actions(node_class, depth, node_id=None, node_path=None): validators = ClassValidators(node_class).validators actions = [] - print(f'{" ":<{depth * 4}}{node_class}') for validator in validators: if validator.get('prop_type') == ValueTypes.RDF_TYPE: action = add_task(VALIDATE_RDF_TYPE_PROPERTY, **validator) diff --git a/tests/test_validate_endorsements.py b/tests/test_validate_endorsements.py index bd6f76c..5a33057 100644 --- a/tests/test_validate_endorsements.py +++ b/tests/test_validate_endorsements.py @@ -3,8 +3,7 @@ from openbadges.verifier.actions.tasks import add_task from openbadges.verifier.openbadges_context import OPENBADGES_CONTEXT_V2_URI -from openbadges.verifier.tasks import run_task -from openbadges.verifier.tasks.task_types import VALIDATE_EXPECTED_NODE_CLASS, VALIDATE_PROPERTY +from openbadges.verifier.tasks.task_types import VALIDATE_EXPECTED_NODE_CLASS from openbadges.verifier.tasks.validation import OBClasses from openbadges import verify from openbadges.verifier.tasks import task_named diff --git a/tests/test_validation.py b/tests/test_validation.py index 418f992..4e681a6 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -10,7 +10,7 @@ from openbadges.verifier.actions.action_types import ADD_TASK, PATCH_NODE from openbadges.verifier.actions.graph import add_node, patch_node from openbadges.verifier.actions.tasks import add_task -from openbadges.verifier.openbadges_context import OPENBADGES_CONTEXT_V2_DICT +from openbadges.verifier.openbadges_context import OPENBADGES_CONTEXT_V2_DICT, OPENBADGES_CONTEXT_V2_URI from openbadges.verifier.reducers import main_reducer from openbadges.verifier.state import filter_active_tasks, INITIAL_STATE from openbadges.verifier.tasks import task_named, run_task @@ -1436,3 +1436,110 @@ def test_issuer_warn_on_non_https_id(self): task_meta['node_id'] = issuer['id'] result, message, actions = task_named(task_meta['name'])(state, task_meta) self.assertTrue(result) + +class ValidationDepthTests(unittest.TestCase): + assertion = { + '@context': OPENBADGES_CONTEXT_V2_URI, + 'type': 'Assertion', + 'id': 'http://example.com/assertion', + 'badge': 'http://example.com/badgeclass', + 'recipient': { + 'id': '_:b0', + 'identity': 'two@example.com', + 'type': 'email', + 'hashed': False + }, + "verification": { + "type": "HostedBadge" + }, + 'issuedOn': (datetime.now(utc) - timedelta(hours=1)).isoformat() + } + badgeclass = { + '@context': OPENBADGES_CONTEXT_V2_URI, + 'id': 'http://example.com/badgeclass', + 'type': 'BadgeClass', + 'name': 'Example Badge', + 'description': 'An example', + 'criteria': 'http://example.com/criteria', + 'issuer': 'http://example.com/issuer', + 'image': 'http://example.com/image1', + } + endorsement = { + '@context': OPENBADGES_CONTEXT_V2_URI, + 'id': 'http://example2.com/endorsement', + 'type': 'Endorsement', + 'claim': { + 'id': 'http://example.com/issuer', + 'endorsementComment': 'Pretty good issuer' + }, + 'issuedOn': '2017-10-01T00:00Z', + 'issuer': 'http://example2.com/issuer', + 'verification': { + 'type': "hosted" + } + } + issuer = { + '@context': OPENBADGES_CONTEXT_V2_URI, + 'id': 'http://example.com/issuer', + 'type': 'Issuer', + 'name': 'Example Issuer', + 'email': 'me@example.com', + 'url': 'http://example.com', + 'endorsement': endorsement['id'] + } + endorsement_issuer_endorsement = { + '@context': OPENBADGES_CONTEXT_V2_URI, + 'id': 'http://example3.com/endorsement', + 'type': 'Endorsement', + 'claim': { + 'id': 'http://example2.com/issuer', + 'endorsementComment': 'Pretty good endorsement issuer' + }, + 'issuedOn': '2017-10-01T00:00Z', + 'issuer': 'http://example3.com/issuer', + 'verification': { + 'type': "hosted" + } + } + endorsement_issuer = { + '@context': OPENBADGES_CONTEXT_V2_URI, + 'id': 'http://example2.com/issuer', + 'type': 'Issuer', + 'name': 'Example Issuer 2', + 'email': 'me@example2.com', + 'url': 'http://example2.com', + 'endorsement': endorsement_issuer_endorsement['id'] + } + endorsement_issuer_endorsement_issuer = { + '@context': OPENBADGES_CONTEXT_V2_URI, + 'id': 'http://example3.com/issuer', + 'type': 'Issuer', + 'name': 'Example Issuer 3', + 'email': 'me@example3.com', + 'url': 'http://example3.com' + } + + @responses.activate + def test_validation_depth(self): + set_up_context_mock() + + for resource in [self.assertion, self.badgeclass, self.issuer, self.endorsement, self.endorsement_issuer]: + responses.add(responses.GET, resource['id'], json=resource) + set_up_image_mock(self.badgeclass['image']) + + results = verify(self.assertion['id']) + self.assertTrue(results['report']['valid']) + self.assertEqual(5, len(results['graph']), "The graph does not contain endorsement of issuer of endorsement.") + + @responses.activate + def test_validation_depth_5(self): + options = {'max_validation_depth': 5} + set_up_context_mock() + + for resource in [self.assertion, self.badgeclass, self.issuer, self.endorsement, self.endorsement_issuer, self.endorsement_issuer_endorsement, self.endorsement_issuer_endorsement_issuer]: + responses.add(responses.GET, resource['id'], json=resource) + set_up_image_mock(self.badgeclass['image']) + + results = verify(self.assertion['id'], **options) + self.assertTrue(results['report']['valid']) + self.assertEqual(7, len(results['graph']), "The graph now contains all objects.") From 4a858150a5088d12f5ec448b6e626fcf0aff8c42 Mon Sep 17 00:00:00 2001 From: Jeff Creed Date: Thu, 14 Jan 2021 13:56:17 -0800 Subject: [PATCH 12/22] ++ version --- openbadges/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbadges/version.py b/openbadges/version.py index 60222ef..5e8d792 100644 --- a/openbadges/version.py +++ b/openbadges/version.py @@ -1 +1 @@ -VERSION = (1, 1, 2) +VERSION = (1, 1, 3) From 4be3dfeec8f2952930e96ccfb4feea1260710718 Mon Sep 17 00:00:00 2001 From: Francisco Gray Date: Tue, 9 Feb 2021 16:25:19 -0800 Subject: [PATCH 13/22] ++version --- openbadges/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbadges/version.py b/openbadges/version.py index 5e8d792..bb57259 100644 --- a/openbadges/version.py +++ b/openbadges/version.py @@ -1 +1 @@ -VERSION = (1, 1, 3) +VERSION = (1, 1, 5) From e90ba991df7ac39d896324d9eeb60997c4e9cf9e Mon Sep 17 00:00:00 2001 From: Jay Kufner Date: Wed, 17 Mar 2021 15:15:15 -0700 Subject: [PATCH 14/22] BP-5506 protects against task_meta.depth being set to None --- openbadges/verifier/tasks/validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbadges/verifier/tasks/validation.py b/openbadges/verifier/tasks/validation.py index 7f3ae24..6ffd8ae 100644 --- a/openbadges/verifier/tasks/validation.py +++ b/openbadges/verifier/tasks/validation.py @@ -689,7 +689,7 @@ def detect_and_validate_node_class(state, task_meta, **options): def validate_expected_node_class(state, task_meta, **options): node_id = task_meta.get('node_id') node_path = task_meta.get('node_path') - depth = task_meta.get('depth') + depth = int(task_meta.get('depth') or 0) max_depth = options.get('max_validation_depth') actions = [] From be08cc55ba6869eb04d4c3a64e78aff71d6dfc79 Mon Sep 17 00:00:00 2001 From: Nate Otto Date: Thu, 18 Mar 2021 15:57:03 -0700 Subject: [PATCH 15/22] Better protect depth check in validate_expected_node_class --- openbadges/verifier/tasks/validation.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/openbadges/verifier/tasks/validation.py b/openbadges/verifier/tasks/validation.py index 6ffd8ae..4af153e 100644 --- a/openbadges/verifier/tasks/validation.py +++ b/openbadges/verifier/tasks/validation.py @@ -99,9 +99,9 @@ class ValueTypes(object): class PrimitiveValueValidator(object): """ - A callable validator for primitive Open Badges value types. - - Example usage: + A callable validator for primitive Open Badges value types. + + Example usage: PrimitiveValueValidator(ValueTypes.TEXT)("test value") > True """ @@ -187,7 +187,7 @@ def _validate_iri(cls, value): """ Checks if a string matches an acceptable IRI format and scheme. For now, only accepts a few schemes, 'http', 'https', blank node identifiers, and 'urn:uuid' - :param value: six.string_types + :param value: six.string_types :return: bool """ return is_iri(value) @@ -689,7 +689,12 @@ def detect_and_validate_node_class(state, task_meta, **options): def validate_expected_node_class(state, task_meta, **options): node_id = task_meta.get('node_id') node_path = task_meta.get('node_path') - depth = int(task_meta.get('depth') or 0) + + try: + depth = int(task_meta.get('depth', 0)) + except (TypeError, ValueError): + depth = 0 + max_depth = options.get('max_validation_depth') actions = [] From 820f265a93123cb40f227b54bf36003c1da44cd8 Mon Sep 17 00:00:00 2001 From: Nate Otto Date: Thu, 18 Mar 2021 15:59:53 -0700 Subject: [PATCH 16/22] v1.1.6 --- openbadges/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbadges/version.py b/openbadges/version.py index bb57259..1a7c46a 100644 --- a/openbadges/version.py +++ b/openbadges/version.py @@ -1 +1 @@ -VERSION = (1, 1, 5) +VERSION = (1, 1, 6) From 69770495f6750ad33c61bfc28e9cf544b67f495c Mon Sep 17 00:00:00 2001 From: Jay Kufner Date: Wed, 21 Apr 2021 13:33:44 -0700 Subject: [PATCH 17/22] Reports a badge as invalid if badge or badge class images are not supported mime types. --- openbadges/verifier/tasks/images.py | 42 +++++++++- requirements.txt | 1 + tests/test_image_validation.py | 78 +++++++++++++++++++ tests/testfiles/public_domain_heart_jpeg.jpg | Bin 0 -> 719 bytes 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 tests/testfiles/public_domain_heart_jpeg.jpg diff --git a/openbadges/verifier/tasks/images.py b/openbadges/verifier/tasks/images.py index fa4da40..4d83d19 100644 --- a/openbadges/verifier/tasks/images.py +++ b/openbadges/verifier/tasks/images.py @@ -1,4 +1,5 @@ import base64 +import puremagic import re import requests import requests_cache @@ -14,6 +15,9 @@ abbreviate_node_id as abv_node, is_data_uri) +SVG_MIME_TYPE = 'image/svg+xml' +PNG_MIME_TYPE = 'image/png' + def validate_image(state, task_meta, **options): try: node_id = task_meta.get('node_id') @@ -74,13 +78,49 @@ def validate_image(state, task_meta, **options): url, headers={'Accept': 'application/ld+json, application/json, image/png, image/svg+xml'} ) result.raise_for_status() + validate_image_mime_type(result.content) content_type = result.headers['content-type'] encoded_body = base64.b64encode(result.content) data_uri = "data:{};base64,{}".format(content_type, encoded_body) - except (requests.ConnectionError, requests.HTTPError, KeyError): + except (requests.ConnectionError, + requests.HTTPError, + puremagic.PureError, + KeyError): return task_result(False, "Could not fetch image at {}".format(url)) + except ValueError as e: + return task_result(False, "The Image at {} is of an unsupported type: {}".format(url, e)) else: actions.append(store_original_resource(url, data_uri)) return task_result(True, "Validated image for node {}".format(abv_node(node_id, node_path)), actions) + + +def validate_image_mime_type(content): + allowed_mime_types = [SVG_MIME_TYPE, PNG_MIME_TYPE] + magic_strings = puremagic.magic_string(content) + if magic_strings: + derived_mime_type = None + derived_ext = None + + for magic_string in magic_strings: + if getattr(magic_string, 'mime_type', None) in allowed_mime_types: + derived_mime_type = getattr(magic_string, 'mime_type', None) + derived_ext = getattr(magic_string, 'extension', None) + break + + if not derived_mime_type and re.search(b'': + derived_mime_type = SVG_MIME_TYPE + derived_ext = '.svg' + + if derived_mime_type not in allowed_mime_types: + magic_string_info = max(magic_strings, key=lambda ms: ms.confidence and ms.extension and ms.mime_type) + raise ValueError("{} {}".format( + getattr(magic_string_info, 'mime_type', 'Unknown'), + getattr(magic_string_info, 'extension', 'Unknown') + )) + + if not derived_ext or not derived_mime_type: + raise ValueError("Unknown file extension.") + else: + raise ValueError("Unable to determine file type.") diff --git a/requirements.txt b/requirements.txt index ca5f1f0..9ca711c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ future>=0.16.0 jsonschema==2.6.0 language-tags==0.4.3 openbadges_bakery>=1.1.0 +puremagic==1.6 pycryptodome==3.6.6 pydux==0.2.2 PyLD==0.7.1 diff --git a/tests/test_image_validation.py b/tests/test_image_validation.py index b225811..3d29d93 100644 --- a/tests/test_image_validation.py +++ b/tests/test_image_validation.py @@ -81,6 +81,84 @@ def test_validate_badgeclass_image_formats(self): new_state = input_reducer({}, next_task) self.assertTrue(new_state['original_json'][image_url].startswith('data:'), "Data is stored in the expected spot.") + + @responses.activate + def test_badgeclass_with_unsupported_image_formats(self): + session = CachedSession(backend='memory', expire_after=100000) + loader = CachableDocumentLoader(use_cache=True, session=session) + options = { + 'jsonld_options': {'documentLoader': loader}, + 'max_validation_depth': 3 + } + image_url = 'http://example.org/awesomebadge.png' + badgeclass = { + 'id': 'http://example.org/badgeclass', + 'name': 'Awesome badge', + 'image': image_url + } + state = {'graph': [badgeclass]} + + with open(os.path.join(os.path.dirname(__file__), 'testfiles', 'public_domain_heart_jpeg.jpg'), 'rb') as f: + responses.add(responses.GET, badgeclass['image'], body=f.read(), content_type='image/png') + response = session.get(badgeclass['image']) + self.assertEqual(response.status_code, 200) + + task_meta = add_task( + VALIDATE_EXPECTED_NODE_CLASS, node_id=badgeclass['id'], expected_class=OBClasses.BadgeClass, depth=0) + + result, message, actions = task_named(VALIDATE_EXPECTED_NODE_CLASS)(state, task_meta, **options) + self.assertTrue(result) + + image_task = [a for a in actions if a.get('prop_name') == 'image'][0] + class_image_validation_task = [a for a in actions if a.get('name') == IMAGE_VALIDATION][0] + result, message, actions = task_named(image_task['name'])(state, image_task, **options) + self.assertTrue(result) + self.assertEqual(len(actions), 0) + + result, message, actions = task_named(class_image_validation_task['name'])( + state, class_image_validation_task, **options) + self.assertFalse(result) + self.assertEqual(len(actions), 0) + + # Case 2: Embedded image document + badgeclass['image'] = { + 'id': 'http://example.org/awesomebadge.png', + 'author': 'http://someoneelse.org/1', + 'caption': 'A hexagon with attitude' + } + + # Validate BadgeClass, queuing the image node validation task + result, message, actions = task_named(image_task['name'])(state, image_task, **options) + self.assertTrue(result) + self.assertEqual(len(actions), 1, "Image node validation task queued") + + # Run image node task discovery + next_task = actions[0] + result, message, actions = task_named(next_task['name'])(state, next_task, **options) + self.assertTrue(result) + + # Run validation task for the Image node + next_task = [a for a in actions if a.get('name') == IMAGE_VALIDATION][0] + result, message, actions = task_named(next_task['name'])(state, next_task, **options) + self.assertFalse(result) + self.assertEqual(len(actions), 0) + + + def test_validate_image_mime_type(self): + from openbadges.verifier.tasks.images import validate_image_mime_type + heart_png = os.path.join(os.path.dirname(__file__), 'testfiles', 'public_domain_heart.png') + heart_jpeg = os.path.join(os.path.dirname(__file__), 'testfiles', 'public_domain_heart_jpeg.jpg') + + # supported image type + with open(heart_png, 'rb') as f: + validate_image_mime_type(f.read()) + + # unsupported image type + with open(heart_jpeg, 'rb') as f: + self.assertRaises(ValueError, validate_image_mime_type, f.read()) + + + def test_base64_data_uri_in_badgeclass(self): data_uri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOUMyqsBwACeQFChxl' \ 'ltgAAAABJRU5ErkJggg==' diff --git a/tests/testfiles/public_domain_heart_jpeg.jpg b/tests/testfiles/public_domain_heart_jpeg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c495b39f8cc7891d13a1ce5a9b88a535fce5d56a GIT binary patch literal 719 zcmex=ma3|jiIItm zOAI4iKMQ#V{6EAX$iX1YAj-_B#K0uT$SlbC{|JKw0|PV2CkVj8#?Ha|{}uxWM3}|e zKiW1o%c{E`s9KB>2oOqvELJ8quv#VoAm_Bk)c*`vm9euxlre*pRal-n4^*Wpz`)4N z%)-RP3^S3DiJ66!O^88I(U4tO$x*;42I3}_f6~MW9lV+d^UVrAOC02@m>}j{AOpyAaPI-r3($iMf`$SjPLM!j1bS>E?~G?DMG+^e>^5nAzh)Aj zpx4fEZ)1Zhr;MFF&=5sI21XV}W>#1jF)%PO0z*lVflX0BNXaM>Y!EOC?HQKOcQx8N zQ-|4iyFjh4ux{bGH9Ff4UryO2>L(@me2=fA_&bx6Gq!5DoMdS!>e!<{-T(hh04;*T AJ^%m! literal 0 HcmV?d00001 From 7a6a256c76ae541e578ae0341994031beab3d152 Mon Sep 17 00:00:00 2001 From: Jay Kufner Date: Thu, 22 Apr 2021 12:30:45 -0700 Subject: [PATCH 18/22] BP-6100 Simplifies error messaging. Renames mimetype validation function. --- openbadges/verifier/tasks/images.py | 17 +++++++++-------- tests/test_image_validation.py | 8 ++++---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/openbadges/verifier/tasks/images.py b/openbadges/verifier/tasks/images.py index 4d83d19..bc458b2 100644 --- a/openbadges/verifier/tasks/images.py +++ b/openbadges/verifier/tasks/images.py @@ -78,25 +78,25 @@ def validate_image(state, task_meta, **options): url, headers={'Accept': 'application/ld+json, application/json, image/png, image/svg+xml'} ) result.raise_for_status() - validate_image_mime_type(result.content) + validate_image_mime_type_for_node_class(result.content, node_class) content_type = result.headers['content-type'] encoded_body = base64.b64encode(result.content) data_uri = "data:{};base64,{}".format(content_type, encoded_body) except (requests.ConnectionError, requests.HTTPError, - puremagic.PureError, KeyError): return task_result(False, "Could not fetch image at {}".format(url)) - except ValueError as e: - return task_result(False, "The Image at {} is of an unsupported type: {}".format(url, e)) + except (ValueError, + puremagic.PureError) as e: + return task_result(False, "{}".format(e)) else: actions.append(store_original_resource(url, data_uri)) return task_result(True, "Validated image for node {}".format(abv_node(node_id, node_path)), actions) -def validate_image_mime_type(content): +def validate_image_mime_type_for_node_class(content, node_class): allowed_mime_types = [SVG_MIME_TYPE, PNG_MIME_TYPE] magic_strings = puremagic.magic_string(content) if magic_strings: @@ -115,12 +115,13 @@ def validate_image_mime_type(content): if derived_mime_type not in allowed_mime_types: magic_string_info = max(magic_strings, key=lambda ms: ms.confidence and ms.extension and ms.mime_type) - raise ValueError("{} {}".format( + raise ValueError("{} image of type '{} {}' is unsupported".format( + node_class, getattr(magic_string_info, 'mime_type', 'Unknown'), getattr(magic_string_info, 'extension', 'Unknown') )) if not derived_ext or not derived_mime_type: - raise ValueError("Unknown file extension.") + raise ValueError("{} image is an unknown file type").format(node_class) else: - raise ValueError("Unable to determine file type.") + raise ValueError("Unable to determine file type for {} image").format(node_class) diff --git a/tests/test_image_validation.py b/tests/test_image_validation.py index 3d29d93..c68a9de 100644 --- a/tests/test_image_validation.py +++ b/tests/test_image_validation.py @@ -144,18 +144,18 @@ def test_badgeclass_with_unsupported_image_formats(self): self.assertEqual(len(actions), 0) - def test_validate_image_mime_type(self): - from openbadges.verifier.tasks.images import validate_image_mime_type + def test_validate_image_mime_type_for_node_class(self): + from openbadges.verifier.tasks.images import validate_image_mime_type_for_node_class heart_png = os.path.join(os.path.dirname(__file__), 'testfiles', 'public_domain_heart.png') heart_jpeg = os.path.join(os.path.dirname(__file__), 'testfiles', 'public_domain_heart_jpeg.jpg') # supported image type with open(heart_png, 'rb') as f: - validate_image_mime_type(f.read()) + validate_image_mime_type_for_node_class(f.read(), OBClasses.BadgeClass) # unsupported image type with open(heart_jpeg, 'rb') as f: - self.assertRaises(ValueError, validate_image_mime_type, f.read()) + self.assertRaises(ValueError, validate_image_mime_type_for_node_class, f.read(), OBClasses.BadgeClass) From 4e3164ed363892dd2d5471ca814a4b554ff9e7cb Mon Sep 17 00:00:00 2001 From: Louis Byers Date: Wed, 28 Apr 2021 12:32:19 -0700 Subject: [PATCH 19/22] v1.1.7 --- openbadges/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbadges/version.py b/openbadges/version.py index 1a7c46a..b6d81f5 100644 --- a/openbadges/version.py +++ b/openbadges/version.py @@ -1 +1 @@ -VERSION = (1, 1, 6) +VERSION = (1, 1, 7) From a13cfd0595adb9e64efcf12423a3aa33bc8c5d88 Mon Sep 17 00:00:00 2001 From: Louis Byers Date: Wed, 5 May 2021 14:00:49 -0700 Subject: [PATCH 20/22] v1.1.8 added puremagic to setup.py install requirements --- openbadges/version.py | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openbadges/version.py b/openbadges/version.py index b6d81f5..c0bb6cb 100644 --- a/openbadges/version.py +++ b/openbadges/version.py @@ -1 +1 @@ -VERSION = (1, 1, 7) +VERSION = (1, 1, 8) diff --git a/setup.py b/setup.py index 8d265df..3826a53 100644 --- a/setup.py +++ b/setup.py @@ -56,6 +56,7 @@ 'jsonschema==2.6.0', 'language-tags==0.4.3', 'openbadges-bakery>=1.1.0', + 'puremagic==1.6', 'pycryptodome==3.6.6', 'pydux==0.2.2', 'PyLD==0.7.1', From 8dcc8657569c0b72da227770b8b4dc3780b31882 Mon Sep 17 00:00:00 2001 From: Nate Otto Date: Fri, 18 Jun 2021 16:53:20 -0700 Subject: [PATCH 21/22] Bump Bakery Version. This library now only officially supports python3 environments --- openbadges/version.py | 2 +- setup.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openbadges/version.py b/openbadges/version.py index c0bb6cb..2dff7f8 100644 --- a/openbadges/version.py +++ b/openbadges/version.py @@ -1 +1 @@ -VERSION = (1, 1, 8) +VERSION = (1, 2, 0) diff --git a/setup.py b/setup.py index 3826a53..d7c8bab 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ setup( - name='openbadges', + name='badgecheck', version=".".join(map(str, VERSION)), packages=find_packages(exclude=['tests', 'tests.*']), include_package_data=True, @@ -40,10 +40,9 @@ 'License :: OSI Approved :: Apache Software License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Education', 'Topic :: Utilities', @@ -55,7 +54,7 @@ 'future>=0.16.0', 'jsonschema==2.6.0', 'language-tags==0.4.3', - 'openbadges-bakery>=1.1.0', + 'openbadges-bakery>=2.0.0', 'puremagic==1.6', 'pycryptodome==3.6.6', 'pydux==0.2.2', From 4f2b6184c0deeded66ee40c0dab963ede4a47111 Mon Sep 17 00:00:00 2001 From: Nate Otto Date: Tue, 8 Feb 2022 11:58:44 -0800 Subject: [PATCH 22/22] Fix a bunch of test dependencies and process 410 http status as a revocation specially --- .gitignore | 1 + openbadges/verifier/actions/utils.py | 16 ++++ openbadges/verifier/tasks/__init__.py | 4 +- openbadges/verifier/tasks/graph.py | 37 +++++++- openbadges/verifier/tasks/task_types.py | 1 + tests/test_graph.py | 8 +- tests/test_hosted_revocation.py | 121 ++++++++++++++++++++++++ tests/test_legacy_versions.py | 6 +- tests/test_loader.py | 4 +- tests/test_recipient.py | 4 +- tests/test_signed_verification.py | 2 +- tests/test_store.py | 2 +- tests/test_tasks.py | 11 +++ tests/test_testfiles.py | 2 +- tests/test_validate_endorsements.py | 6 +- tests/test_validation.py | 4 +- tests/test_verification_report.py | 6 +- tests/testfiles/test_components.py | 16 +++- 18 files changed, 226 insertions(+), 25 deletions(-) create mode 100644 tests/test_hosted_revocation.py diff --git a/.gitignore b/.gitignore index d395331..5197145 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ build/ .cache/ README.rst .vscode/launch.json +*.iml diff --git a/openbadges/verifier/actions/utils.py b/openbadges/verifier/actions/utils.py index 3b417a6..510b9ad 100644 --- a/openbadges/verifier/actions/utils.py +++ b/openbadges/verifier/actions/utils.py @@ -1,5 +1,21 @@ +import hashlib +import six import uuid def generate_task_key(): return uuid.uuid4().hex + + +def generate_task_signature(task_name, *args): + content = b'' + for arg in args: + try: + content += six.ensure_binary(str(arg)) + except TypeError: + pass + + return b'__'.join([ + six.ensure_binary(task_name), + six.ensure_binary(hashlib.md5(content).hexdigest()) + ]).decode('utf-8') diff --git a/openbadges/verifier/tasks/__init__.py b/openbadges/verifier/tasks/__init__.py index ad32320..18c2f53 100644 --- a/openbadges/verifier/tasks/__init__.py +++ b/openbadges/verifier/tasks/__init__.py @@ -3,7 +3,8 @@ from .extensions import validate_extension_node, validate_single_extension from .images import validate_image from .input import detect_input_type, process_baked_resource -from .graph import fetch_http_node, flatten_refetch_embedded_resource, intake_json, jsonld_compact_data +from .graph import (fetch_http_node, flatten_refetch_embedded_resource, intake_json, jsonld_compact_data, + process_410_gone) from .object_upgrades import upgrade_0_5_node, upgrade_1_0_node, upgrade_1_1_node from .validation import (assertion_timestamp_checks, assertion_verification_dependencies, criteria_property_dependencies, detect_and_validate_node_class, @@ -28,6 +29,7 @@ IDENTITY_OBJECT_PROPERTY_DEPENDENCIES: identity_object_property_dependencies, INTAKE_JSON: intake_json, ISSUER_PROPERTY_DEPENDENCIES: issuer_property_dependencies, + PROCESS_410_GONE: process_410_gone, PROCESS_BAKED_RESOURCE: process_baked_resource, PROCESS_JWS_INPUT: process_jws_input, UPGRADE_0_5_NODE: upgrade_0_5_node, diff --git a/openbadges/verifier/tasks/graph.py b/openbadges/verifier/tasks/graph.py index 87c8fce..d1c0ee4 100644 --- a/openbadges/verifier/tasks/graph.py +++ b/openbadges/verifier/tasks/graph.py @@ -13,6 +13,7 @@ from ..actions.input import store_original_resource from ..actions.validation_report import set_validation_subject from ..actions.tasks import add_task, delete_outdated_node_tasks, report_message +from ..actions.utils import generate_task_signature from ..actions.validation_report import set_openbadges_version from ..exceptions import TaskPrerequisitesError from ..openbadges_context import OPENBADGES_CONTEXT_V2_URI @@ -21,7 +22,7 @@ from ..utils import list_of, jsonld_use_cache,make_string_from_bytes, MESSAGE_LEVEL_WARNING from .task_types import (DETECT_AND_VALIDATE_NODE_CLASS, FETCH_HTTP_NODE, INTAKE_JSON, JSONLD_COMPACT_DATA, - PROCESS_BAKED_RESOURCE, UPGRADE_0_5_NODE, UPGRADE_1_0_NODE, UPGRADE_1_1_NODE, + PROCESS_410_GONE, PROCESS_BAKED_RESOURCE, UPGRADE_0_5_NODE, UPGRADE_1_0_NODE, UPGRADE_1_1_NODE, VALIDATE_EXPECTED_NODE_CLASS, VALIDATE_EXTENSION_NODE) from .utils import abbreviate_node_id as abv_node, filter_tasks, is_iri, is_url, task_result, URN_REGEX from .validation import OBClasses @@ -40,7 +41,6 @@ def fetch_http_node(state, task_meta, **options): result = session.get( url, headers={'Accept': 'application/ld+json, application/json, image/png, image/svg+xml'} ) - try: json_body = result.json() response_text_with_proper_encoding = json.dumps(json_body) @@ -76,6 +76,12 @@ def fetch_http_node(state, task_meta, **options): depth=depth ) ] + + if result.status_code == 410: + actions += [add_task( + PROCESS_410_GONE, node_id=url, prerequisites=[generate_task_signature(JSONLD_COMPACT_DATA, url)] + )] + return task_result(message="Successfully fetched JSON data from {}".format(url), actions=actions) @@ -113,6 +119,7 @@ def intake_json(state, task_meta, **options): if openbadges_version in ['1.1', '2.0']: compact_action = add_task( JSONLD_COMPACT_DATA, + task_key=generate_task_signature(JSONLD_COMPACT_DATA, node_id), node_id=node_id, openbadges_version=openbadges_version, expected_class=expected_class, @@ -306,4 +313,28 @@ def flatten_refetch_embedded_resource(state, task_meta, **options): # fetch actions.append(add_task(FETCH_HTTP_NODE, url=embedded_node_id, depth=depth)) - return task_result(True, "Embedded {} node in {} queued for storage and/or refetching as needed", actions) \ No newline at end of file + return task_result(True, "Embedded {} node in {} queued for storage and/or refetching as needed", actions) + + +def process_410_gone(state, task_meta, **options): + try: + node_id = task_meta['node_id'] + node = get_node_by_id(state, node_id) + except (IndexError, KeyError): + raise TaskPrerequisitesError() + + node_type = node.get('type') + if 'Assertion' not in list_of(node_type): + return task_result( + True, "Fetched resource returned 410 Gone status, but was not an Assertion, so not counted as revoked." + ) + + validation_subject = state['report'].get('validationSubject') + if validation_subject != node_id: + return task_result(True, "Fetched resource returned 410 Gone status, but was not the validation subject.") + + return task_result( + False, + "Hosted Assertion is revoked, as hosted response status was 410 Gone. {}".format(abv_node(node_id=node_id)) + ) + diff --git a/openbadges/verifier/tasks/task_types.py b/openbadges/verifier/tasks/task_types.py index 1bd6498..b0dec9e 100644 --- a/openbadges/verifier/tasks/task_types.py +++ b/openbadges/verifier/tasks/task_types.py @@ -13,6 +13,7 @@ INTAKE_JSON = 'INTAKE_JSON' JSONLD_COMPACT_DATA = 'JSONLD_COMPACT_DATA' PROCESS_JWS_INPUT = 'PROCESS_JWS_INPUT' +PROCESS_410_GONE = 'PROCESS_410_GONE' """ diff --git a/tests/test_graph.py b/tests/test_graph.py index 982a9d4..dcbe53a 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -15,13 +15,13 @@ from openbadges.verifier.utils import MESSAGE_LEVEL_WARNING from openbadges.verifier.verifier import verify -from .utils import set_up_context_mock, set_up_image_mock - try: + from .utils import set_up_context_mock, set_up_image_mock from .testfiles.test_components import test_components except (ImportError, SystemError): - from .testfiles.test_components import test_components + from tests.utils import set_up_context_mock, set_up_image_mock + from tests.testfiles.test_components import test_components class HttpFetchingTests(unittest.TestCase): @@ -95,7 +95,7 @@ def test_store_nested(self): def test_store_node_inaccurate_id_value(self): """ Due to redirects, we may not have the canonical id for a node. - If there's a conflict due to the id and the node[id] in add_node(id, node), + If there's a conflict due to the id and the node[id] in add_node(id, node), what should we do? """ pass diff --git a/tests/test_hosted_revocation.py b/tests/test_hosted_revocation.py new file mode 100644 index 0000000..d705888 --- /dev/null +++ b/tests/test_hosted_revocation.py @@ -0,0 +1,121 @@ +import json +import responses +import unittest + +from openbadges.verifier.actions.action_types import STORE_ORIGINAL_RESOURCE +from openbadges.verifier.actions.tasks import add_task +from openbadges.verifier.actions.utils import generate_task_signature +from openbadges.verifier.tasks.graph import fetch_http_node +from openbadges.verifier.tasks.task_types import FETCH_HTTP_NODE, INTAKE_JSON, JSONLD_COMPACT_DATA, PROCESS_410_GONE +from openbadges.verifier.verifier import verify + +try: + from .testfiles.test_components import test_components +except (ImportError, SystemError): + from tests.testfiles.test_components import test_components + +from tests.utils import set_up_image_mock + + +class HttpFetchingTests(unittest.TestCase): + + @responses.activate + def test_revoked_410_assertion_invalid(self): + """ + The specification states "If either the 410 Gone status or a response body declaring revoked true is returned, + the Assertion should be treated as revoked and thus invalid." This test checks the 410 state + """ + url = 'http://example.com/assertionmaybe' + # The issuer has revoked via just the status code, not body. Ideally they do both 410 and "revoked": true + responses.add( + responses.GET, url, + body=test_components['2_0_basic_assertion'], + status=410, content_type='application/ld+json' + ) + task = add_task(FETCH_HTTP_NODE, url=url, depth=0) + + success, message, actions = fetch_http_node({}, task) + + self.assertTrue(success) + self.assertEqual(len(actions), 3) + self.assertEqual(actions[0]['type'], STORE_ORIGINAL_RESOURCE) + self.assertEqual(actions[1]['name'], INTAKE_JSON) + self.assertEqual(actions[2]['name'], PROCESS_410_GONE) + self.assertEqual(actions[2]['prerequisites'][0], generate_task_signature(JSONLD_COMPACT_DATA, url)) + + @responses.activate + def test_verify_of_410_assertion(self): + url = 'https://example.org/beths-robotics-badge.json' + responses.add( + responses.GET, url, body=test_components['2_0_basic_assertion'], status=410, + content_type='application/ld+json' + ) + set_up_image_mock('https://example.org/beths-robot-badge.png') + responses.add( + responses.GET, 'https://w3id.org/openbadges/v2', + body=test_components['openbadges_context'], status=200, + content_type='application/ld+json' + ) + responses.add( + responses.GET, 'https://example.org/robotics-badge.json', + body=test_components['2_0_basic_badgeclass'], status=200, + content_type='application/ld+json' + ) + set_up_image_mock(u'https://example.org/robotics-badge.png') + responses.add( + responses.GET, 'https://example.org/organization.json', + body=test_components['2_0_basic_issuer'], status=200, + content_type='application/ld+json' + ) + + results = verify(url) + self.assertEqual(results.get('input').get('value'), url) + self.assertEqual(results.get('input').get('input_type'), 'url') + + self.assertEqual( + len(results['report']['messages']), 1, "The 410 status results in an error.") + + @responses.activate + def test_verify_of_410_endorsement_no_disruption(self): + url = 'https://example.org/beths-robotics-badge.json' + assertion_body = json.loads(test_components['2_0_basic_assertion']) + assertion_body['endorsement'] = 'https://example.org/beths-robotics-endorsement.json' + assertion_body['related'] = 'https://example.org/beths-robotics-badge2.json' + assertion_body = json.dumps(assertion_body) + responses.add( + responses.GET, url, body=assertion_body, status=200, + content_type='application/ld+json' + ) + set_up_image_mock('https://example.org/beths-robot-badge.png') + responses.add( + responses.GET, 'https://example.org/beths-robotics-badge2.json', body=assertion_body, status=410, + content_type='application/ld+json' + ) + responses.add( + responses.GET, 'https://w3id.org/openbadges/v2', + body=test_components['openbadges_context'], status=200, + content_type='application/ld+json' + ) + responses.add( + responses.GET, 'https://example.org/robotics-badge.json', + body=test_components['2_0_basic_badgeclass'], status=200, + content_type='application/ld+json' + ) + set_up_image_mock(u'https://example.org/robotics-badge.png') + responses.add( + responses.GET, 'https://example.org/organization.json', + body=test_components['2_0_basic_issuer'], status=200, + content_type='application/ld+json' + ) + responses.add( + responses.GET, 'https://example.org/beths-robotics-endorsement.json', + body=test_components['2_0_basic_endorsement'], status=410, + content_type='application/ld+json' + ) + + results = verify(url) + self.assertEqual(results.get('input').get('value'), url) + self.assertEqual(results.get('input').get('input_type'), 'url') + + self.assertEqual( + len(results['report']['messages']), 0, "The 410 status of the endorsement doesn't affect validity.") diff --git a/tests/test_legacy_versions.py b/tests/test_legacy_versions.py index 695eb30..ddab129 100644 --- a/tests/test_legacy_versions.py +++ b/tests/test_legacy_versions.py @@ -16,8 +16,10 @@ from openbadges.verifier.tasks.validation import OBClasses from openbadges.verifier.verifier import generate_report, verification_store, verify -from .testfiles.test_components import test_components - +try: + from .testfiles.test_components import test_components +except (ImportError, SystemError): + from tests.testfiles.test_components import test_components def setUpContextCache(): v2_data = test_components['openbadges_context'] diff --git a/tests/test_loader.py b/tests/test_loader.py index ecacff9..2cb8a7e 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -7,11 +7,11 @@ from openbadges.verifier.utils import CachableDocumentLoader try: - from .testfiles.test_components import test_components + from tests.testfiles.test_components import test_components from tests.utils import set_up_context_mock except (ImportError, SystemError): from .testfiles.test_components import test_components - from .testutils import set_up_context_mock + from .utils import set_up_context_mock class DocumentLoaderTests(unittest.TestCase): diff --git a/tests/test_recipient.py b/tests/test_recipient.py index 964e830..0b263e3 100644 --- a/tests/test_recipient.py +++ b/tests/test_recipient.py @@ -14,11 +14,11 @@ from openbadges.verifier.verifier import verification_store try: - from .testfiles.test_components import test_components + from tests.testfiles.test_components import test_components from tests.utils import set_up_image_mock except (ImportError, SystemError): from .testfiles.test_components import test_components - from tests.utils import set_up_image_mock + from .utils import set_up_image_mock class RecipientProfileVerificationTests(unittest.TestCase): diff --git a/tests/test_signed_verification.py b/tests/test_signed_verification.py index 6761e4a..b2aef75 100644 --- a/tests/test_signed_verification.py +++ b/tests/test_signed_verification.py @@ -19,7 +19,7 @@ from openbadges.verifier.utils import make_string_from_bytes try: - from .testfiles.test_components import test_components + from tests.testfiles.test_components import test_components from tests.utils import set_up_context_mock, set_up_image_mock except (ImportError, SystemError): from .testfiles.test_components import test_components diff --git a/tests/test_store.py b/tests/test_store.py index 8b1bc14..771896a 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -9,7 +9,7 @@ get_node_by_path,) try: - from .testfiles.test_components import test_components + from tests.testfiles.test_components import test_components except (ImportError, SystemError): from .testfiles.test_components import test_components diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 970d330..53c2cb5 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -4,6 +4,7 @@ from openbadges.verifier.reducers import main_reducer from openbadges.verifier.actions.tasks import add_task, resolve_task +from openbadges.verifier.actions.utils import generate_task_signature from openbadges.verifier.reducers.tasks import _new_state_with_updated_item from openbadges.verifier.tasks.utils import abbreviate_value from openbadges.verifier.state import INITIAL_STATE, filter_active_tasks @@ -61,3 +62,13 @@ def chars(length): self.assertEqual(abbreviate_value(['interesting']), 'interesting') self.assertEqual(abbreviate_value(['interesting', 1]), 'interesting, 1') self.assertEqual(abbreviate_value([{'a': 'b'}, 'interesting']), "{'a': 'b'}, interesting") + + def test_generate_task_signature(self): + task_key = generate_task_signature('DETECT_INPUT_TYPE', 123) + self.assertEqual(task_key, 'DETECT_INPUT_TYPE__202cb962ac59075b964b07152d234b70') + + task_key = generate_task_signature('JSONLD_COMPACT_DATA', 'https://example.com/123') + self.assertEqual(task_key, 'JSONLD_COMPACT_DATA__d974a4cbd98f9538d3210974dc8a7972') + + task_key = generate_task_signature('UNKNOWN_SOLDIER', 'important key info!') + self.assertEqual(task_key, 'UNKNOWN_SOLDIER__e9729d01fa8a2b37aed4a4dceeea464c') diff --git a/tests/test_testfiles.py b/tests/test_testfiles.py index a748085..8653b1c 100644 --- a/tests/test_testfiles.py +++ b/tests/test_testfiles.py @@ -2,7 +2,7 @@ import unittest try: - from .testfiles.test_components import test_components + from tests.testfiles.test_components import test_components except (ImportError, SystemError): from .testfiles.test_components import test_components diff --git a/tests/test_validate_endorsements.py b/tests/test_validate_endorsements.py index 5a33057..2a9db0e 100644 --- a/tests/test_validate_endorsements.py +++ b/tests/test_validate_endorsements.py @@ -8,8 +8,10 @@ from openbadges import verify from openbadges.verifier.tasks import task_named - -from .utils import set_up_context_mock, set_up_image_mock +try: + from tests.utils import set_up_context_mock, set_up_image_mock +except (ImportError, SystemError): + from .utils import set_up_context_mock, set_up_image_mock class EndorsementTests(unittest.TestCase): diff --git a/tests/test_validation.py b/tests/test_validation.py index 4e681a6..7df6d23 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -26,11 +26,11 @@ from openbadges.verifier.verifier import call_task, verify try: - from .testfiles.test_components import test_components + from tests.testfiles.test_components import test_components from tests.utils import set_up_context_mock, set_up_image_mock except (ImportError, SystemError): from .testfiles.test_components import test_components - from tests.utils import set_up_context_mock, set_up_image_mock + from .utils import set_up_context_mock, set_up_image_mock class PropertyValidationTests(unittest.TestCase): diff --git a/tests/test_verification_report.py b/tests/test_verification_report.py index 89d6da7..4de782f 100644 --- a/tests/test_verification_report.py +++ b/tests/test_verification_report.py @@ -10,11 +10,11 @@ from openbadges.verifier.verifier import generate_report, verification_store try: - from .testfiles.test_components import test_components + from tests.testfiles.test_components import test_components from tests.utils import set_up_context_mock except (ImportError, SystemError): from .testfiles.test_components import test_components - from .testutils import set_up_context_mock + from .utils import set_up_context_mock class VerificationReportTests(unittest.TestCase): @@ -86,6 +86,6 @@ def test_validation_version(self): def test_messages_warnings_counts(self): """ The validationReport contains the properties of messages, warningCount, errorCount, valid - :return: + :return: """ pass diff --git a/tests/testfiles/test_components.py b/tests/testfiles/test_components.py index d59e3da..7d433fd 100644 --- a/tests/testfiles/test_components.py +++ b/tests/testfiles/test_components.py @@ -136,6 +136,20 @@ "url": "https://example.org", "email": "contact@example.org" }""", +'2_0_basic_endorsement': """{ + "@context": "https://w3id.org/openbadges/v2", + "type": "Endorsement", + "id": "https://example.org/beths-robotics-endorsement.json", + "claim": { + "id": "https://example.org/beths-robot-badge.png", + "endorsementComment": "A great robot." + }, + "issuer": "https://example.org/organization.json", + "issuedOn": "2016-12-31T23:59:59Z", + "verification": { + "type": "hosted" + } +}""", 'openbadges_context': """ {"@context": {"issuedOn": {"@id": "obi:issueDate", "@type": "xsd:dateTime"}, "AlignmentObject": "schema:AlignmentObject", "uid": {"@id": "obi:uid"}, "claim": {"@id": "cred:claim", "@type": "@id"}, "targetCode": {"@id": "obi:targetCode"}, "image": {"@id": "schema:image", "@type": "@id"}, "Endorsement": "cred:Credential", "Assertion": "obi:Assertion", "related": {"@id": "dc:relation", "@type": "@id"}, "evidence": {"@id": "obi:evidence", "@type": "@id"}, "sec": "https://w3id.org/security#", "Criteria": "obi:Criteria", "owner": {"@id": "sec:owner", "@type": "@id"}, "revocationList": {"@id": "obi:revocationList", "@type": "@id"}, "targetName": {"@id": "schema:targetName"}, "id": "@id", "alignment": {"@id": "obi:alignment", "@type": "@id"}, "allowedOrigins": {"@id": "obi:allowedOrigins"}, "Profile": "obi:Profile", "startsWith": {"@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"}, "author": {"@id": "schema:author", "@type": "@id"}, "FrameValidation": "obi:FrameValidation", "validationFrame": "obi:validationFrame", "creator": {"@id": "dc:creator", "@type": "@id"}, "validationSchema": "obi:validationSchema", "validatesType": "obi:validatesType", "version": {"@id": "schema:version"}, "BadgeClass": "obi:BadgeClass", "endorsement": {"@id": "cred:credential", "@type": "@id"}, "revocationReason": {"@id": "obi:revocationReason"}, "RevocationList": "obi:RevocationList", "issuer": {"@id": "obi:issuer", "@type": "@id"}, "type": "@type", "email": {"@id": "schema:email"}, "targetDescription": {"@id": "schema:targetDescription"}, "schema": "http://schema.org/", "targetUrl": {"@id": "schema:targetUrl"}, "criteria": {"@id": "obi:criteria", "@type": "@id"}, "verificationProperty": {"@id": "obi:verificationProperty"}, "description": {"@id": "schema:description"}, "Extension": "obi:Extension", "tags": {"@id": "schema:keywords"}, "CryptographicKey": "sec:Key", "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, "hosted": "obi:HostedBadge", "dc": "http://purl.org/dc/terms/", "telephone": {"@id": "schema:telephone"}, "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, "badge": {"@id": "obi:badge", "@type": "@id"}, "endorsementComment": {"@id": "obi:endorsementComment"}, "genre": {"@id": "schema:genre"}, "hashed": {"@id": "obi:hashed", "@type": "xsd:boolean"}, "recipient": {"@id": "obi:recipient", "@type": "@id"}, "HostedBadge": "obi:HostedBadge", "identity": {"@id": "obi:identityHash"}, "revoked": {"@id": "obi:revoked", "@type": "xsd:boolean"}, "verify": "verification", "VerificationObject": "obi:VerificationObject", "name": {"@id": "schema:name"}, "publicKeyPem": {"@id": "sec:publicKeyPem"}, "obi": "https://w3id.org/openbadges#", "url": {"@id": "schema:url", "@type": "@id"}, "cred": "https://w3id.org/credentials#", "Image": "obi:Image", "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, "IdentityObject": "obi:IdentityObject", "signed": "obi:SignedBadge", "Evidence": "obi:Evidence", "narrative": {"@id": "obi:narrative"}, "caption": {"@id": "schema:caption"}, "audience": {"@id": "obi:audience"}, "extensions": "https://w3id.org/openbadges/extensions#", "verification": {"@id": "obi:verify", "@type": "@id"}, "xsd": "http://www.w3.org/2001/XMLSchema#", "TypeValidation": "obi:TypeValidation", "revokedAssertions": {"@id": "obi:revoked"}, "SignedBadge": "obi:SignedBadge", "validation": "obi:validation", "salt": {"@id": "obi:salt"}, "targetFramework": {"@id": "schema:targetFramework"}, "Issuer": "obi:Issuer"}} """, @@ -246,7 +260,7 @@ "description": { "@id": "schema:description" }, "url": { "@id": "schema:url", "@type": "@id" }, "image": { "@id": "schema:image", "@type": "@id" }, - + "uid": { "@id": "obi:uid" }, "recipient": { "@id": "obi:recipient", "@type": "@id" }, "hashed": { "@id": "obi:hashed", "@type": "xsd:boolean" },