Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support names and attribute-value specifications in attribute restric… #587

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
59 changes: 54 additions & 5 deletions aries_cloudagent/protocols/present_proof/v1_0/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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::<attribute-name>::value "
"where <attribute-name> 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."""
Expand All @@ -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,
)
Expand Down
29 changes: 26 additions & 3 deletions aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
12 changes: 12 additions & 0 deletions demo/runners/faber.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
6 changes: 5 additions & 1 deletion demo/runners/support/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down