From b11473cb20c521ec928a4188686acea3ae4409e0 Mon Sep 17 00:00:00 2001 From: sklump Date: Mon, 6 Jul 2020 16:45:49 +0000 Subject: [PATCH] support names and attribute-value specifications in attribute restrictions within proof request for present-proof protocol Signed-off-by: sklump --- .../v1_0/tests/test_routes.py | 38 ++++++++++++ .../protocols/present_proof/v1_0/routes.py | 59 +++++++++++++++++-- .../present_proof/v1_0/tests/test_routes.py | 29 ++++++++- demo/runners/faber.py | 12 ++++ demo/runners/support/agent.py | 6 +- 5 files changed, 135 insertions(+), 9 deletions(-) diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py index f09223c0ef..5c4a533522 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py @@ -203,6 +203,44 @@ async def test_credential_exchange_create(self): mock_cred_ex_record.serialize.return_value ) + async def test_credential_exchange_create_x(self): + mock = async_mock.MagicMock() + mock.json = async_mock.CoroutineMock() + context = RequestContext(base_context=InjectionContext(enforce_typing=False)) + mock.app = { + "outbound_message_router": async_mock.CoroutineMock(), + "request_context": context, + } + mock.app["request_context"].settings = {} + + with async_mock.patch.object( + test_module, "ConnectionRecord", autospec=True + ) as mock_connection_record, async_mock.patch.object( + test_module, "CredentialManager", autospec=True + ) as mock_credential_manager, async_mock.patch.object( + test_module.CredentialPreview, "deserialize", autospec=True + ), async_mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_credential_manager.return_value.create_offer = ( + async_mock.CoroutineMock() + ) + + mock_credential_manager.return_value.create_offer.return_value = ( + async_mock.CoroutineMock(), + async_mock.CoroutineMock(), + ) + + mock_cred_ex_record = async_mock.MagicMock() + mock_cred_offer = async_mock.MagicMock() + + mock_credential_manager.return_value.prepare_send.side_effect = ( + test_module.StorageError() + ) + + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.credential_exchange_create(mock) + async def test_credential_exchange_create_no_proposal(self): conn_id = "connection-id" diff --git a/aries_cloudagent/protocols/present_proof/v1_0/routes.py b/aries_cloudagent/protocols/present_proof/v1_0/routes.py index 9d62789824..d9251f6c75 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/routes.py @@ -123,7 +123,7 @@ class V10PresentationProposalRequestSchema(AdminAPIMessageTracingSchema): ) -class IndyProofReqSpecRestrictionsSchema(Schema): +class IndyProofReqPredSpecRestrictionsSchema(Schema): """Schema for restrictions in attr or pred specifier indy proof request.""" schema_id = fields.String( @@ -185,15 +185,64 @@ class IndyProofReqAttrSpecSchema(Schema): """Schema for attribute specification in indy proof request.""" name = fields.String( - example="favouriteDrink", description="Attribute name", required=True + example="favouriteDrink", description="Attribute name", required=False + ) + names = fields.List( + fields.String(example="age"), + description="Attribute name group", + required=False, ) restrictions = fields.List( - fields.Nested(IndyProofReqSpecRestrictionsSchema()), - description="If present, credential must satisfy one of given restrictions", + fields.Dict( + keys=fields.Str( + validate=validate.Regexp( + "^schema_id|" + "schema_issuer_did|" + "schema_name|" + "schema_version|" + "issuer_did|" + "cred_def_id|" + "attr::.+::value$" # indy does not support attr::...::marker here + ), + example="cred_def_id", # marshmallow/apispec v3.0 ignores + ), + values=fields.Str(example=INDY_CRED_DEF_ID["example"]), + ), + description=( + "If present, credential must satisfy one of given restrictions: specify " + "schema_id, schema_issuer_did, schema_name, schema_version, " + "issuer_did, cred_def_id, and/or attr::::value " + "where represents a credential attribute name" + ), required=False, ) non_revoked = fields.Nested(IndyProofReqNonRevokedSchema(), required=False) + @validates_schema + def validate_fields(self, data, **kwargs): + """ + Validate schema fields. + + Data must have exactly one of name or names; if names then restrictions are + mandatory. + + Args: + data: The data to validate + + Raises: + ValidationError: if data has both or neither of name and names + + """ + if ("name" in data) == ("names" in data): + raise ValidationError( + "Attribute specification must have either name or names but not both" + ) + restrictions = data.get("restrictions") + if ("names" in data) and (not restrictions or all(not r for r in restrictions)): + raise ValidationError( + "Attribute specification on 'names' must have non-empty restrictions" + ) + class IndyProofReqPredSpecSchema(Schema): """Schema for predicate specification in indy proof request.""" @@ -206,7 +255,7 @@ class IndyProofReqPredSpecSchema(Schema): ) p_value = fields.Integer(description="Threshold value", required=True) restrictions = fields.List( - fields.Nested(IndyProofReqSpecRestrictionsSchema()), + fields.Nested(IndyProofReqPredSpecRestrictionsSchema()), description="If present, credential must satisfy one of given restrictions", required=False, ) diff --git a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py index bfdb4f31fe..b6ae5ba892 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py @@ -13,12 +13,35 @@ def setUp(self): async def test_validate_non_revoked(self): non_revo = test_module.IndyProofReqNonRevokedSchema() - non_revo.validate({"from": 1234567890}) - non_revo.validate({"to": 1234567890}) - non_revo.validate({"from": 1234567890, "to": 1234567890}) + non_revo.validate_fields({"from": 1234567890}) + non_revo.validate_fields({"to": 1234567890}) + non_revo.validate_fields({"from": 1234567890, "to": 1234567890}) with self.assertRaises(test_module.ValidationError): non_revo.validate_fields({}) + async def test_validate_proof_req_attr_spec(self): + aspec = test_module.IndyProofReqAttrSpecSchema() + aspec.validate_fields({"name": "attr0"}) + aspec.validate_fields( + { + "names": ["attr0", "attr1"], + "restrictions": [{"attr::attr1::value": "my-value"}], + } + ) + aspec.validate_fields( + {"name": "attr0", "restrictions": [{"schema_name": "preferences"}]} + ) + with self.assertRaises(test_module.ValidationError): + aspec.validate_fields({}) + with self.assertRaises(test_module.ValidationError): + aspec.validate_fields({"name": "attr0", "names": ["attr1", "attr2"]}) + with self.assertRaises(test_module.ValidationError): + aspec.validate_fields({"names": ["attr1", "attr2"]}) + with self.assertRaises(test_module.ValidationError): + aspec.validate_fields({"names": ["attr0", "attr1"], "restrictions": []}) + with self.assertRaises(test_module.ValidationError): + aspec.validate_fields({"names": ["attr0", "attr1"], "restrictions": [{}]}) + async def test_presentation_exchange_list(self): mock = async_mock.MagicMock() mock.query = { diff --git a/demo/runners/faber.py b/demo/runners/faber.py index d3e5cc377f..291ac5943e 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -321,6 +321,18 @@ async def main( for req_pred in req_preds }, } + # test with an attribute group with attribute value restrictions + # indy_proof_request["requested_attributes"] = { + # "n_group_attrs": { + # "names": ["name", "degree", "timestamp", "date"], + # "restrictions": [ + # { + # "issuer_did": agent.did, + # "attr::name::value": "Alice Smith" + # } + # ] + # } + # } if revocation: indy_proof_request["non_revoked"] = {"to": int(time.time())} proof_request_web_request = { diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index 46eb22b70b..2da0e31539 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -482,8 +482,12 @@ async def admin_request( async with self.client_session.request( method, self.admin_url + path, json=data, params=params ) as resp: - resp.raise_for_status() resp_text = await resp.text() + try: + resp.raise_for_status() + except Exception as e: + # try to retrieve and print text on error + raise Exception(f"Error: {resp_text}") from e if not resp_text and not text: return None if not text: