From 7ea17f123eb45c6325428af4303e6ca3ed462bcf Mon Sep 17 00:00:00 2001 From: Nicholas Rempel Date: Thu, 4 Jul 2019 14:15:05 -0700 Subject: [PATCH 1/5] Add initial authentication mechanism to admin server Signed-off-by: Nicholas Rempel --- aries_cloudagent/admin/server.py | 31 +- aries_cloudagent/ledger/indy.py | 5 +- .../actionmenu/messages/tests/test_menu.py | 8 +- .../messaging/connections/manager.py | 56 +- .../connections/messages/problem_report.py | 4 +- .../connections/tests/test_diddoc.py | 516 +++++++++--------- .../messaging/credentials/manager.py | 4 +- .../discovery/messages/tests/test_query.py | 8 +- .../messaging/presentations/manager.py | 3 +- .../messaging/problem_report/message.py | 4 +- .../messaging/trustping/messages/ping.py | 4 +- .../wallet/tests/test_basic_wallet.py | 4 +- demo/agent.py | 5 +- docs/conf.py | 94 ++-- 14 files changed, 378 insertions(+), 368 deletions(-) diff --git a/aries_cloudagent/admin/server.py b/aries_cloudagent/admin/server.py index 4c5331721c..1e60e19028 100644 --- a/aries_cloudagent/admin/server.py +++ b/aries_cloudagent/admin/server.py @@ -136,6 +136,32 @@ def __init__( async def make_application(self) -> web.Application: """Get the aiohttp application instance.""" + + api_key = self.context.settings.get("api_key") + admin_unsecured = self.context.settings.get("admin_unsecured") + + # admin-token and admin-token are mutually exclusive. Should be enforced + # during parameter parsing but to be sure, we check here. + assert admin_unsecured or api_key + assert not (admin_unsecured and api_key) + + # If api_key is None, then admin_unsecured must be set so + # we can safely enable the admin server with no security + if api_key: + + @web.middleware + async def check_token(request, handler): + header_api_key = request.headers.get("x-api-key") + if not header_api_key: + raise web.HTTPUnauthorized( + "An API key must be provided in the X-API-Key header" + ) + + if api_key == header_api_key: + return await handler(request) + else: + raise web.HTTPUnauthorized("Invalid api key provided") + middlewares = [] stats: Collector = await self.context.inject(Collector, required=False) if stats: @@ -190,10 +216,7 @@ async def collect_stats(request, handler): cors.add(route) setup_aiohttp_apispec( - app=app, - title="Aries Cloud Agent", - version="v1", - swagger_path="/api/doc", + app=app, title="Aries Cloud Agent", version="v1", swagger_path="/api/doc" ) app.on_startup.append(self.on_startup) return app diff --git a/aries_cloudagent/ledger/indy.py b/aries_cloudagent/ledger/indy.py index cb2dd7609a..42d038f2c7 100644 --- a/aries_cloudagent/ledger/indy.py +++ b/aries_cloudagent/ledger/indy.py @@ -177,10 +177,7 @@ async def _submit(self, request_json: str, sign=True) -> str: # HACK: If only there were a better way to identify this kind # of rejected request... - if ( - "can have one and only one SCHEMA with name" - in request_result_json - ): + if "can have one and only one SCHEMA with name" in request_result_json: raise DuplicateSchemaError() if operation in ("REQNACK", "REJECT"): diff --git a/aries_cloudagent/messaging/actionmenu/messages/tests/test_menu.py b/aries_cloudagent/messaging/actionmenu/messages/tests/test_menu.py index 8a2f23e40c..351d4a26bc 100644 --- a/aries_cloudagent/messaging/actionmenu/messages/tests/test_menu.py +++ b/aries_cloudagent/messaging/actionmenu/messages/tests/test_menu.py @@ -57,9 +57,7 @@ def test_type(self): """Test type.""" assert self.menu._type == MENU - @mock.patch( - "aries_cloudagent.messaging.actionmenu.messages.menu.MenuSchema.load" - ) + @mock.patch("aries_cloudagent.messaging.actionmenu.messages.menu.MenuSchema.load") def test_deserialize(self, mock_menu_schema_load): """ Test deserialization. @@ -71,9 +69,7 @@ def test_deserialize(self, mock_menu_schema_load): assert menu is mock_menu_schema_load.return_value - @mock.patch( - "aries_cloudagent.messaging.actionmenu.messages.menu.MenuSchema.dump" - ) + @mock.patch("aries_cloudagent.messaging.actionmenu.messages.menu.MenuSchema.dump") def test_serialize(self, mock_menu_schema_dump): """ Test serialization. diff --git a/aries_cloudagent/messaging/connections/manager.py b/aries_cloudagent/messaging/connections/manager.py index 57aba31c95..184ffa9e84 100644 --- a/aries_cloudagent/messaging/connections/manager.py +++ b/aries_cloudagent/messaging/connections/manager.py @@ -72,10 +72,7 @@ def context(self) -> InjectionContext: return self._context async def create_invitation( - self, - my_label: str = None, - my_endpoint: str = None, - their_role: str = None, + self, my_label: str = None, my_endpoint: str = None, their_role: str = None ) -> Tuple[ConnectionRecord, ConnectionInvitation]: """ Generate new connection invitation. @@ -169,9 +166,7 @@ async def send_invitation(self, invitation: ConnectionInvitation, endpoint: str) await session.get(endpoint, params={"invite": invite_b64}) async def receive_invitation( - self, - invitation: ConnectionInvitation, - their_role: str = None, + self, invitation: ConnectionInvitation, their_role: str = None ) -> ConnectionRecord: """ Create a new connection record to track a received invitation. @@ -204,10 +199,7 @@ async def receive_invitation( self._log_state( "Created new connection record", - { - "id": connection.connection_id, - "state": connection.state, - }, + {"id": connection.connection_id, "state": connection.state}, ) # Save the invitation for later processing @@ -344,10 +336,7 @@ async def receive_request( self._log_state( "Created new connection record", - { - "id": connection.connection_id, - "state": connection.state, - }, + {"id": connection.connection_id, "state": connection.state}, ) # Attach the connection request so it can be found and responded to @@ -360,9 +349,7 @@ async def receive_request( return connection async def create_response( - self, - connection: ConnectionRecord, - my_endpoint: str = None, + self, connection: ConnectionRecord, my_endpoint: str = None ) -> ConnectionResponse: """ Create a connection response for a received connection request. @@ -626,7 +613,7 @@ async def create_did_document( self, my_info: DIDInfo, inbound_connection_id: str = None, - my_endpoint: str = None + my_endpoint: str = None, ) -> DIDDoc: """Create our DID document for a given DID. @@ -855,10 +842,7 @@ def diddoc_connection_target( ) async def establish_inbound( - self, - connection: ConnectionRecord, - inbound_connection_id: str, - outbound_handler + self, connection: ConnectionRecord, inbound_connection_id: str, outbound_handler ) -> str: """Assign the inbound routing connection for a connection record. @@ -878,8 +862,7 @@ async def establish_inbound( try: router = await ConnectionRecord.retrieve_by_id( - self.context, - inbound_connection_id + self.context, inbound_connection_id ) except StorageNotFoundError: raise ConnectionManagerError( @@ -894,9 +877,7 @@ async def establish_inbound( route_mgr = RoutingManager(self.context) await route_mgr.send_create_route( - inbound_connection_id, - my_info.verkey, - outbound_handler + inbound_connection_id, my_info.verkey, outbound_handler ) connection.routing_state = ConnectionRecord.ROUTING_STATE_REQUEST await connection.save(self.context) @@ -904,10 +885,7 @@ async def establish_inbound( return connection.routing_state async def update_inbound( - self, - inbound_connection_id: str, - recip_verkey: str, - routing_state: str + self, inbound_connection_id: str, recip_verkey: str, routing_state: str ): """Activate connections once a route has been established. @@ -915,9 +893,7 @@ async def update_inbound( connection and marks the routing as complete. """ conns = await ConnectionRecord.query( - self.context, { - "inbound_connection_id": inbound_connection_id, - } + self.context, {"inbound_connection_id": inbound_connection_id} ) wallet: BaseWallet = await self.context.inject(BaseWallet) @@ -946,10 +922,7 @@ async def updated_record(self, connection: ConnectionRecord): """Call webhook when the record is updated.""" responder = await self._context.inject(BaseResponder, required=False) if responder: - await responder.send_webhook( - "connections", - connection.serialize() - ) + await responder.send_webhook("connections", connection.serialize()) cache: BaseCache = await self._context.inject(BaseCache, required=False) cache_key = f"connection_target::{connection.connection_id}" if cache: @@ -962,8 +935,5 @@ async def updated_activity(self, connection: ConnectionRecord): activity = await connection.fetch_activity(self._context) await responder.send_webhook( "connections_activity", - { - "connection_id": connection.connection_id, - "activity": activity, - } + {"connection_id": connection.connection_id, "activity": activity}, ) diff --git a/aries_cloudagent/messaging/connections/messages/problem_report.py b/aries_cloudagent/messaging/connections/messages/problem_report.py index 7a88cea8c8..0b8c62aab6 100644 --- a/aries_cloudagent/messaging/connections/messages/problem_report.py +++ b/aries_cloudagent/messaging/connections/messages/problem_report.py @@ -6,9 +6,7 @@ from ...agent_message import AgentMessage, AgentMessageSchema from ..message_types import PROBLEM_REPORT -HANDLER_CLASS = ( - "aries_cloudagent.messaging.problem_report.handler.ProblemReportHandler" -) +HANDLER_CLASS = "aries_cloudagent.messaging.problem_report.handler.ProblemReportHandler" class ProblemReportReason(Enum): diff --git a/aries_cloudagent/messaging/connections/tests/test_diddoc.py b/aries_cloudagent/messaging/connections/tests/test_diddoc.py index 1f1c196063..7d838926a8 100644 --- a/aries_cloudagent/messaging/connections/tests/test_diddoc.py +++ b/aries_cloudagent/messaging/connections/tests/test_diddoc.py @@ -29,380 +29,389 @@ async def test_basic(self): # One authn key by reference dd_in = { - '@context': 'https://w3id.org/did/v1', - 'id': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKey': [ - { - 'id': '3', - 'type': 'RsaVerificationKey2018', - 'controller': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKeyPem': '-----BEGIN PUBLIC X...' + "@context": "https://w3id.org/did/v1", + "id": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKey": [ + { + "id": "3", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC X...", }, { - 'id': '4', - 'type': 'RsaVerificationKey2018', - 'controller': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKeyPem': '-----BEGIN PUBLIC 9...' + "id": "4", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC 9...", }, { - 'id': '6', - 'type': 'RsaVerificationKey2018', - 'controller': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKeyPem': '-----BEGIN PUBLIC A...' - } + "id": "6", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A...", + }, ], - 'authentication': [ + "authentication": [ { - 'type': 'RsaSignatureAuthentication2018', - 'publicKey': 'did:sov:LjgpST2rjsoxYegQDRm7EL#4' + "type": "RsaSignatureAuthentication2018", + "publicKey": "did:sov:LjgpST2rjsoxYegQDRm7EL#4", } ], - 'service': [ + "service": [ { - 'id': '0', - 'type': 'Agency', - 'serviceEndpoint': 'did:sov:Q4zqM7aXqm7gDQkUVLng9h' + "id": "0", + "type": "Agency", + "serviceEndpoint": "did:sov:Q4zqM7aXqm7gDQkUVLng9h", } - ] + ], } dd = DIDDoc.deserialize(dd_in) - assert len(dd.pubkey) == len(dd_in['publicKey']) - assert len(dd.authnkey) == len(dd_in['authentication']) + assert len(dd.pubkey) == len(dd_in["publicKey"]) + assert len(dd.authnkey) == len(dd_in["authentication"]) dd_out = dd.serialize() - #print('\n\n== 1 == DID Doc {} on abbreviated identifiers: {}'.format(dd, ppjson(dd_out))) + # print('\n\n== 1 == DID Doc {} on abbreviated identifiers: {}'.format(dd, ppjson(dd_out))) # Exercise JSON, de/serialization dd_json = dd.to_json() dd_copy = dd.from_json(dd_json) assert dd_copy.did == dd.did - assert all(dd_copy.authnkey[k].to_dict() == dd.authnkey[k].to_dict() for k in dd_copy.authnkey) + assert all( + dd_copy.authnkey[k].to_dict() == dd.authnkey[k].to_dict() + for k in dd_copy.authnkey + ) assert {k for k in dd_copy.authnkey} == {k for k in dd.authnkey} - assert all(dd_copy.pubkey[k].to_dict() == dd.pubkey[k].to_dict() for k in dd_copy.pubkey) + assert all( + dd_copy.pubkey[k].to_dict() == dd.pubkey[k].to_dict() + for k in dd_copy.pubkey + ) assert {k for k in dd_copy.pubkey} == {k for k in dd.pubkey} - assert all(dd_copy.service[k].to_dict() == dd.service[k].to_dict() for k in dd_copy.service) + assert all( + dd_copy.service[k].to_dict() == dd.service[k].to_dict() + for k in dd_copy.service + ) assert {k for k in dd_copy.service} == {k for k in dd.service} # print('\n\n== 2 == DID Doc de/serialization operates OK:') # Exercise accessors - dd.did = dd_out['id'] - assert dd.did == canon_did(dd_out['id']) + dd.did = dd_out["id"] + assert dd.did == canon_did(dd_out["id"]) with self.assertRaises(ValueError): - dd.set(['neither a service', 'nor a public key']) + dd.set(["neither a service", "nor a public key"]) assert dd.service[[k for k in dd.service][0]].did == dd.did - #print('\n\n== 3 == DID Doc accessors operate OK') + # print('\n\n== 3 == DID Doc accessors operate OK') def test_embedded_authkey(self): # One authn key embedded, all possible refs canonical dd_in = { - '@context': 'https://w3id.org/did/v1', - 'id': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKey': [ - { - 'id': '3', - 'type': 'RsaVerificationKey2018', - 'controller': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKeyPem': '-----BEGIN PUBLIC X...' + "@context": "https://w3id.org/did/v1", + "id": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKey": [ + { + "id": "3", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC X...", }, { - 'id': 'did:sov:LjgpST2rjsoxYegQDRm7EL#4', - 'type': 'RsaVerificationKey2018', - 'controller': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKeyPem': '-----BEGIN PUBLIC 9...' - } + "id": "did:sov:LjgpST2rjsoxYegQDRm7EL#4", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC 9...", + }, ], - 'authentication': [ + "authentication": [ { - 'type': 'RsaSignatureAuthentication2018', - 'publicKey': 'did:sov:LjgpST2rjsoxYegQDRm7EL#4' + "type": "RsaSignatureAuthentication2018", + "publicKey": "did:sov:LjgpST2rjsoxYegQDRm7EL#4", }, { - 'id': 'did:sov:LjgpST2rjsoxYegQDRm7EL#6', - 'type': 'RsaVerificationKey2018', - 'controller': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKeyPem': '-----BEGIN PUBLIC A...' - } + "id": "did:sov:LjgpST2rjsoxYegQDRm7EL#6", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A...", + }, ], - 'service': [ + "service": [ { - 'id': 'did:sov:LjgpST2rjsoxYegQDRm7EL;0', - 'type': 'Agency', - 'serviceEndpoint': 'https://www.von.ca' + "id": "did:sov:LjgpST2rjsoxYegQDRm7EL;0", + "type": "Agency", + "serviceEndpoint": "https://www.von.ca", } - ] + ], } dd = DIDDoc.deserialize(dd_in) - assert len(dd.pubkey) == len(dd_in['publicKey']) + 1 - assert len(dd.authnkey) == len(dd_in['authentication']) + assert len(dd.pubkey) == len(dd_in["publicKey"]) + 1 + assert len(dd.authnkey) == len(dd_in["authentication"]) dd_out = dd.serialize() - #print('\n\n== 4 == DID Doc on mixed reference styles, embedded and ref style authn keys: {}'.format(ppjson(dd_out))) + # print('\n\n== 4 == DID Doc on mixed reference styles, embedded and ref style authn keys: {}'.format(ppjson(dd_out))) def test_reference_authkey(self): # All references canonical where possible; one authn key embedded and one by reference dd_in = { - '@context': 'https://w3id.org/did/v1', - 'id': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKey': [ - { - 'id': 'did:sov:LjgpST2rjsoxYegQDRm7EL#3', - 'type': 'RsaVerificationKey2018', - 'controller': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKeyPem': '-----BEGIN PUBLIC X...' + "@context": "https://w3id.org/did/v1", + "id": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKey": [ + { + "id": "did:sov:LjgpST2rjsoxYegQDRm7EL#3", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC X...", }, { - 'id': 'did:sov:LjgpST2rjsoxYegQDRm7EL#4', - 'type': 'RsaVerificationKey2018', - 'controller': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKeyPem': '-----BEGIN PUBLIC 9...' - } + "id": "did:sov:LjgpST2rjsoxYegQDRm7EL#4", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC 9...", + }, ], - 'authentication': [ + "authentication": [ { - 'type': 'RsaSignatureAuthentication2018', - 'publicKey': 'did:sov:LjgpST2rjsoxYegQDRm7EL#4' + "type": "RsaSignatureAuthentication2018", + "publicKey": "did:sov:LjgpST2rjsoxYegQDRm7EL#4", }, { - 'id': 'did:sov:LjgpST2rjsoxYegQDRm7EL#6', - 'type': 'RsaVerificationKey2018', - 'controller': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKeyPem': '-----BEGIN PUBLIC A...' - } + "id": "did:sov:LjgpST2rjsoxYegQDRm7EL#6", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A...", + }, ], - 'service': [ + "service": [ { - 'id': 'did:sov:LjgpST2rjsoxYegQDRm7EL;0', - 'type': 'DidMessaging', - 'serviceEndpoint': 'https://www.von.ca' + "id": "did:sov:LjgpST2rjsoxYegQDRm7EL;0", + "type": "DidMessaging", + "serviceEndpoint": "https://www.von.ca", } - ] + ], } dd = DIDDoc.deserialize(dd_in) - assert len(dd.pubkey) == len(dd_in['publicKey']) + 1 - assert len(dd.authnkey) == len(dd_in['authentication']) + assert len(dd.pubkey) == len(dd_in["publicKey"]) + 1 + assert len(dd.authnkey) == len(dd_in["authentication"]) dd_out = dd.serialize() - #print('\n\n== 5 == DID Doc on canonical refs: {}'.format(ppjson(dd_out))) + # print('\n\n== 5 == DID Doc on canonical refs: {}'.format(ppjson(dd_out))) def test_minimal(self): # Minimal as per indy-agent test suite without explicit identifiers dd_in = { - '@context': 'https://w3id.org/did/v1', - 'publicKey': [ + "@context": "https://w3id.org/did/v1", + "publicKey": [ { - 'id': 'LjgpST2rjsoxYegQDRm7EL#keys-1', - 'type': 'Ed25519VerificationKey2018', - 'controller': 'LjgpST2rjsoxYegQDRm7EL', - 'publicKeyBase58': '~XXXXXXXXXXXXXXXX' + "id": "LjgpST2rjsoxYegQDRm7EL#keys-1", + "type": "Ed25519VerificationKey2018", + "controller": "LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "~XXXXXXXXXXXXXXXX", } ], - 'service': [ + "service": [ { - 'type': 'DidMessaging', - 'recipientKeys': ['~XXXXXXXXXXXXXXXX'], - 'serviceEndpoint': 'https://www.von.ca' + "type": "DidMessaging", + "recipientKeys": ["~XXXXXXXXXXXXXXXX"], + "serviceEndpoint": "https://www.von.ca", } - ] + ], } dd = DIDDoc.deserialize(dd_in) - assert len(dd.pubkey) == len(dd_in['publicKey']) + assert len(dd.pubkey) == len(dd_in["publicKey"]) assert len(dd.authnkey) == 0 dd_out = dd.serialize() - #print('\n\n== 6 == DID Doc miminal style, implcit DID document identifier: {}'.format( + # print('\n\n== 6 == DID Doc miminal style, implcit DID document identifier: {}'.format( # ppjson(dd_out))) def test_minimal_ids(self): # Minimal + ids as per indy-agent test suite with explicit identifiers; novel service recipient key on raw base58 dd_in = { - '@context': 'https://w3id.org/did/v1', - 'id': 'LjgpST2rjsoxYegQDRm7EL', - 'publicKey': [ - { - 'id': 'LjgpST2rjsoxYegQDRm7EL#keys-1', - 'type': 'Ed25519VerificationKey2018', - 'controller': 'LjgpST2rjsoxYegQDRm7EL', - 'publicKeyBase58': '~XXXXXXXXXXXXXXXX' + "@context": "https://w3id.org/did/v1", + "id": "LjgpST2rjsoxYegQDRm7EL", + "publicKey": [ + { + "id": "LjgpST2rjsoxYegQDRm7EL#keys-1", + "type": "Ed25519VerificationKey2018", + "controller": "LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "~XXXXXXXXXXXXXXXX", } ], - 'service': [ + "service": [ { - 'id': 'LjgpST2rjsoxYegQDRm7EL;indy', - 'type': 'DidMessaging', - 'priority': 1, - 'recipientKeys': ['~YYYYYYYYYYYYYYYY'], - 'serviceEndpoint': 'https://www.von.ca' + "id": "LjgpST2rjsoxYegQDRm7EL;indy", + "type": "DidMessaging", + "priority": 1, + "recipientKeys": ["~YYYYYYYYYYYYYYYY"], + "serviceEndpoint": "https://www.von.ca", } - ] + ], } dd = DIDDoc.deserialize(dd_in) - assert len(dd.pubkey) == 1 + len(dd_in['publicKey']) + assert len(dd.pubkey) == 1 + len(dd_in["publicKey"]) assert len(dd.authnkey) == 0 dd_out = dd.serialize() - #print('\n\n== 7 == DID Doc miminal style plus explicit idents and novel raw base58 service recip key: {}'.format( + # print('\n\n== 7 == DID Doc miminal style plus explicit idents and novel raw base58 service recip key: {}'.format( # ppjson(dd_out))) def test_minimal_explicit(self): # Minimal + ids as per indy-agent test suite with explicit identifiers; novel service recipient key on raw base58 dd_in = { - '@context': 'https://w3id.org/did/v1', - 'id': 'LjgpST2rjsoxYegQDRm7EL', - 'publicKey': [ - { - 'id': 'LjgpST2rjsoxYegQDRm7EL#keys-1', - 'type': 'Ed25519VerificationKey2018', - 'controller': 'LjgpST2rjsoxYegQDRm7EL', - 'publicKeyBase58': '~XXXXXXXXXXXXXXXX' + "@context": "https://w3id.org/did/v1", + "id": "LjgpST2rjsoxYegQDRm7EL", + "publicKey": [ + { + "id": "LjgpST2rjsoxYegQDRm7EL#keys-1", + "type": "Ed25519VerificationKey2018", + "controller": "LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "~XXXXXXXXXXXXXXXX", }, { - 'id': 'LjgpST2rjsoxYegQDRm7EL#keys-2', - 'type': 'Ed25519VerificationKey2018', - 'controller': 'LjgpST2rjsoxYegQDRm7EL', - 'publicKeyBase58': '~YYYYYYYYYYYYYYYY' + "id": "LjgpST2rjsoxYegQDRm7EL#keys-2", + "type": "Ed25519VerificationKey2018", + "controller": "LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "~YYYYYYYYYYYYYYYY", }, { - 'id': 'LjgpST2rjsoxYegQDRm7EL#keys-3', - 'type': 'Secp256k1VerificationKey2018', - 'controller': 'LjgpST2rjsoxYegQDRm7EL', - 'publicKeyHex': '02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71' + "id": "LjgpST2rjsoxYegQDRm7EL#keys-3", + "type": "Secp256k1VerificationKey2018", + "controller": "LjgpST2rjsoxYegQDRm7EL", + "publicKeyHex": "02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71", }, { - 'id': 'did:sov:LjgpST2rjsoxYegQDRm7EL#keys-4', - 'type': 'RsaVerificationKey2018', - 'controller': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKeyPem': '-----BEGIN PUBLIC A...' - } + "id": "did:sov:LjgpST2rjsoxYegQDRm7EL#keys-4", + "type": "RsaVerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyPem": "-----BEGIN PUBLIC A...", + }, ], - 'service': [ + "service": [ { - 'id': 'LjgpST2rjsoxYegQDRm7EL;indy', - 'type': 'DidMessaging', - 'priority': 0, - 'recipientKeys': ['~ZZZZZZZZZZZZZZZZ'], - 'serviceEndpoint': 'did:sov:LjgpST2rjsoxYegQDRm7EL;1' + "id": "LjgpST2rjsoxYegQDRm7EL;indy", + "type": "DidMessaging", + "priority": 0, + "recipientKeys": ["~ZZZZZZZZZZZZZZZZ"], + "serviceEndpoint": "did:sov:LjgpST2rjsoxYegQDRm7EL;1", }, { - 'id': '1', - 'type': 'one', - 'priority': 1, - 'recipientKeys': [ - '~XXXXXXXXXXXXXXXX', - 'did:sov:LjgpST2rjsoxYegQDRm7EL#keys-1' - ], - 'routingKeys': [ - 'did:sov:LjgpST2rjsoxYegQDRm7EL#keys-4' + "id": "1", + "type": "one", + "priority": 1, + "recipientKeys": [ + "~XXXXXXXXXXXXXXXX", + "did:sov:LjgpST2rjsoxYegQDRm7EL#keys-1", ], - 'serviceEndpoint': 'LjgpST2rjsoxYegQDRm7EL;2' + "routingKeys": ["did:sov:LjgpST2rjsoxYegQDRm7EL#keys-4"], + "serviceEndpoint": "LjgpST2rjsoxYegQDRm7EL;2", }, { - 'id': '2', - 'type': 'two', - 'priority': 2, - 'recipientKeys': [ - '~XXXXXXXXXXXXXXXX', - 'did:sov:LjgpST2rjsoxYegQDRm7EL#keys-1' - ], - 'routingKeys': [ - 'did:sov:LjgpST2rjsoxYegQDRm7EL#keys-4' + "id": "2", + "type": "two", + "priority": 2, + "recipientKeys": [ + "~XXXXXXXXXXXXXXXX", + "did:sov:LjgpST2rjsoxYegQDRm7EL#keys-1", ], - 'serviceEndpoint': 'https://www.two.ca/two' - } - ] + "routingKeys": ["did:sov:LjgpST2rjsoxYegQDRm7EL#keys-4"], + "serviceEndpoint": "https://www.two.ca/two", + }, + ], } dd = DIDDoc.deserialize(dd_in) - assert len(dd.pubkey) == 1 + len(dd_in['publicKey']) + assert len(dd.pubkey) == 1 + len(dd_in["publicKey"]) assert len(dd.authnkey) == 0 assert {s.priority for s in dd.service.values()} == {0, 1, 2} assert len(dd.service) == 3 - assert all(len(dd.service[k].to_dict()['recipientKeys']) == 1 for k in dd.service) - assert 'routingKeys' not in dd.service['did:sov:LjgpST2rjsoxYegQDRm7EL;indy'].to_dict() - assert all(len(dd.service[k].to_dict()['routingKeys']) == 1 - for k in ('did:sov:LjgpST2rjsoxYegQDRm7EL;1', 'did:sov:LjgpST2rjsoxYegQDRm7EL;2')) + assert all( + len(dd.service[k].to_dict()["recipientKeys"]) == 1 for k in dd.service + ) + assert ( + "routingKeys" + not in dd.service["did:sov:LjgpST2rjsoxYegQDRm7EL;indy"].to_dict() + ) + assert all( + len(dd.service[k].to_dict()["routingKeys"]) == 1 + for k in ( + "did:sov:LjgpST2rjsoxYegQDRm7EL;1", + "did:sov:LjgpST2rjsoxYegQDRm7EL;2", + ) + ) dd_out = dd.serialize() - #print('\n\n== 8 == DID Doc on mixed service routing and recipient keys: {}'.format(ppjson(dd_out))) + # print('\n\n== 8 == DID Doc on mixed service routing and recipient keys: {}'.format(ppjson(dd_out))) pk = PublicKey( dd.did, - '99', - '~AAAAAAAAAAAAAAAA', + "99", + "~AAAAAAAAAAAAAAAA", PublicKeyType.ED25519_SIG_2018, dd.did, - True) + True, + ) dd.set(pk) - assert len(dd.pubkey) == 2 + len(dd_in['publicKey']) - assert canon_ref(dd.did, '99', '#') in dd.pubkey + assert len(dd.pubkey) == 2 + len(dd_in["publicKey"]) + assert canon_ref(dd.did, "99", "#") in dd.pubkey assert len(dd.authnkey) == 1 service = Service( - dd.did, - 'abc', - 'IndyAgent', - [pk], - [pk], - 'http://www.abc.ca/123' + dd.did, "abc", "IndyAgent", [pk], [pk], "http://www.abc.ca/123" ) dd.set(service) assert len(dd.service) == 4 - assert canon_ref(dd.did, 'abc', ';') in dd.service - #print('\n\n== 9 == DID Doc adds public key and service via set() OK') + assert canon_ref(dd.did, "abc", ";") in dd.service + # print('\n\n== 9 == DID Doc adds public key and service via set() OK') def test_missing_recipkey(self): # Exercise missing service recipient key dd_in = { - '@context': 'https://w3id.org/did/v1', - 'id': 'LjgpST2rjsoxYegQDRm7EL', - 'publicKey': [ - { - 'id': 'LjgpST2rjsoxYegQDRm7EL#keys-1', - 'type': 'Ed25519VerificationKey2018', - 'controller': 'LjgpST2rjsoxYegQDRm7EL', - 'publicKeyBase58': '~XXXXXXXXXXXXXXXX' + "@context": "https://w3id.org/did/v1", + "id": "LjgpST2rjsoxYegQDRm7EL", + "publicKey": [ + { + "id": "LjgpST2rjsoxYegQDRm7EL#keys-1", + "type": "Ed25519VerificationKey2018", + "controller": "LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "~XXXXXXXXXXXXXXXX", } ], - 'service': [ + "service": [ { - 'id': 'LjgpST2rjsoxYegQDRm7EL;indy', - 'type': 'DidMessaging', - 'priority': 1, - 'recipientKeys': [ - 'did:sov:LjgpST2rjsoxYegQDRm7EL#keys-3' - ], - 'serviceEndpoint': 'https://www.von.ca' + "id": "LjgpST2rjsoxYegQDRm7EL;indy", + "type": "DidMessaging", + "priority": 1, + "recipientKeys": ["did:sov:LjgpST2rjsoxYegQDRm7EL#keys-3"], + "serviceEndpoint": "https://www.von.ca", } - ] + ], } with self.assertRaises(ValueError): dd = DIDDoc.deserialize(dd_in) - #print('\n\n== 10 == DID Doc on underspecified service key fails as expected') + # print('\n\n== 10 == DID Doc on underspecified service key fails as expected') def test_w3c_minimal(self): # Minimal as per W3C Example 2, draft 0.12 dd_in = { - '@context': 'https://w3id.org/did/v1', - 'id': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'authentication': [ - { - 'id': 'LjgpST2rjsoxYegQDRm7EL#keys-1', - 'type': 'Ed25519VerificationKey2018', - 'controller': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKeyBase58': '~XXXXXXXXXXXXXXXX' + "@context": "https://w3id.org/did/v1", + "id": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "authentication": [ + { + "id": "LjgpST2rjsoxYegQDRm7EL#keys-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "~XXXXXXXXXXXXXXXX", } ], - 'service': [ + "service": [ { - 'type': 'DidMessaging', - 'serviceEndpoint': 'https://example.com/endpoint/8377464' + "type": "DidMessaging", + "serviceEndpoint": "https://example.com/endpoint/8377464", } - ] + ], } dd = DIDDoc.deserialize(dd_in) @@ -411,75 +420,76 @@ def test_w3c_minimal(self): assert len(dd.service) == 1 dd_out = dd.serialize() - #print('\n\n== 11 == Minimal DID Doc (no pubkey except authentication) as per W3C spec parses OK: {}'.format( + # print('\n\n== 11 == Minimal DID Doc (no pubkey except authentication) as per W3C spec parses OK: {}'.format( # ppjson(dd_out))) def test_no_ident(self): # Exercise no-identifier case dd_in = { - '@context': 'https://w3id.org/did/v1', - 'authentication': [ + "@context": "https://w3id.org/did/v1", + "authentication": [ { - 'type': 'Ed25519VerificationKey2018', - 'controller': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKeyBase58': '~XXXXXXXXXXXXXXXX' + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "~XXXXXXXXXXXXXXXX", } ], - 'service': [ + "service": [ { - 'type': 'DidMessaging', - 'serviceEndpoint': 'https://example.com/endpoint/8377464' + "type": "DidMessaging", + "serviceEndpoint": "https://example.com/endpoint/8377464", } - ] + ], } with self.assertRaises(ValueError): dd = DIDDoc.deserialize(dd_in) - #print('\n\n== 12 == DID Doc without identifier rejected as expected') + # print('\n\n== 12 == DID Doc without identifier rejected as expected') def test_canon_did(self): # Exercise reference canonicalization, including failure paths - valid_did = 'LjgpST2rjsoxYegQDRm7EL' + valid_did = "LjgpST2rjsoxYegQDRm7EL" with self.assertRaises(ValueError): - canon_ref('not-a-DID', ref=valid_did, delimiter='#') + canon_ref("not-a-DID", ref=valid_did, delimiter="#") with self.assertRaises(ValueError): - canon_ref(valid_did, ref='did:sov:not-a-DID', delimiter='#') - - urlref = 'https://www.clafouti-quasar.ca:8443/supply-management/fruit/index.html' + canon_ref(valid_did, ref="did:sov:not-a-DID", delimiter="#") + + urlref = ( + "https://www.clafouti-quasar.ca:8443/supply-management/fruit/index.html" + ) assert canon_ref(valid_did, ref=urlref) == urlref - #print('\n\n== 13 == Reference canonicalization operates as expected') + # print('\n\n== 13 == Reference canonicalization operates as expected') def test_pubkey_type(self): dd_in = { - '@context': 'https://w3id.org/did/v1', - 'id': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'authentication': [ - { - 'id': 'LjgpST2rjsoxYegQDRm7EL#keys-1', - 'type': 'Ed25519VerificationKey2018', - 'controller': 'did:sov:LjgpST2rjsoxYegQDRm7EL', - 'publicKeyBase58': '~XXXXXXXXXXXXXXXX' + "@context": "https://w3id.org/did/v1", + "id": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "authentication": [ + { + "id": "LjgpST2rjsoxYegQDRm7EL#keys-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:sov:LjgpST2rjsoxYegQDRm7EL", + "publicKeyBase58": "~XXXXXXXXXXXXXXXX", } ], - 'service': [ + "service": [ { - 'type': 'DidMessaging', - 'serviceEndpoint': 'https://example.com/endpoint/8377464' + "type": "DidMessaging", + "serviceEndpoint": "https://example.com/endpoint/8377464", } - ] + ], } dd = DIDDoc.deserialize(dd_in) - assert PublicKeyType.get('no-such-type') is None + assert PublicKeyType.get("no-such-type") is None pubkey0 = dd.pubkey[[k for k in dd.pubkey][0]] was_authn = pubkey0.authn pubkey0.authn = not was_authn assert pubkey0.authn != was_authn - #print('\n\n== 14 == Changed authentication setting for DIDDoc {} in public key {}, now {}'.format( + # print('\n\n== 14 == Changed authentication setting for DIDDoc {} in public key {}, now {}'.format( # pubkey0.did, # pubkey0.id, # repr(pubkey0))) - diff --git a/aries_cloudagent/messaging/credentials/manager.py b/aries_cloudagent/messaging/credentials/manager.py index b94beb9cd9..4025cbc5b8 100644 --- a/aries_cloudagent/messaging/credentials/manager.py +++ b/aries_cloudagent/messaging/credentials/manager.py @@ -282,7 +282,7 @@ async def create_request( if credential_exchange_record.credential_request: self._logger.warning( "create_request called multiple times for credential exchange: %s", - credential_exchange_record.credential_exchange_id + credential_exchange_record.credential_exchange_id, ) else: ledger: BaseLedger = await self.context.inject(BaseLedger) @@ -352,7 +352,7 @@ async def issue_credential(self, credential_exchange_record: CredentialExchange) if credential_exchange_record.credential: self._logger.warning( "issue_credential called multiple times for credential exchange: %s", - credential_exchange_record.credential_exchange_id + credential_exchange_record.credential_exchange_id, ) else: credential_offer = credential_exchange_record.credential_offer diff --git a/aries_cloudagent/messaging/discovery/messages/tests/test_query.py b/aries_cloudagent/messaging/discovery/messages/tests/test_query.py index 468549f3b7..c3d7abf021 100644 --- a/aries_cloudagent/messaging/discovery/messages/tests/test_query.py +++ b/aries_cloudagent/messaging/discovery/messages/tests/test_query.py @@ -17,9 +17,7 @@ def test_type(self): query = Query(query=self.test_query, comment=self.test_comment) assert query._type == QUERY - @mock.patch( - "aries_cloudagent.messaging.discovery.messages.query.QuerySchema.load" - ) + @mock.patch("aries_cloudagent.messaging.discovery.messages.query.QuerySchema.load") def test_deserialize(self, mock_query_schema_load): obj = {"obj": "obj"} @@ -28,9 +26,7 @@ def test_deserialize(self, mock_query_schema_load): assert query is mock_query_schema_load.return_value - @mock.patch( - "aries_cloudagent.messaging.discovery.messages.query.QuerySchema.dump" - ) + @mock.patch("aries_cloudagent.messaging.discovery.messages.query.QuerySchema.dump") def test_serialize(self, mock_query_schema_dump): query = Query(query=self.test_query, comment=self.test_comment) diff --git a/aries_cloudagent/messaging/presentations/manager.py b/aries_cloudagent/messaging/presentations/manager.py index aaa60d00a5..f884608654 100644 --- a/aries_cloudagent/messaging/presentations/manager.py +++ b/aries_cloudagent/messaging/presentations/manager.py @@ -281,6 +281,5 @@ async def updated_record(self, presentation_exchange: PresentationExchange): responder = await self._context.inject(BaseResponder, required=False) if responder: await responder.send_webhook( - "presentations", - presentation_exchange.serialize() + "presentations", presentation_exchange.serialize() ) diff --git a/aries_cloudagent/messaging/problem_report/message.py b/aries_cloudagent/messaging/problem_report/message.py index 9b598be80f..9e83d5274a 100644 --- a/aries_cloudagent/messaging/problem_report/message.py +++ b/aries_cloudagent/messaging/problem_report/message.py @@ -6,9 +6,7 @@ from ..agent_message import AgentMessage, AgentMessageSchema -HANDLER_CLASS = ( - "aries_cloudagent.messaging.problem_report.handler.ProblemReportHandler" -) +HANDLER_CLASS = "aries_cloudagent.messaging.problem_report.handler.ProblemReportHandler" MESSAGE_TYPE = "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/notification/1.0/problem-report" diff --git a/aries_cloudagent/messaging/trustping/messages/ping.py b/aries_cloudagent/messaging/trustping/messages/ping.py index cebb712578..c31de70eec 100644 --- a/aries_cloudagent/messaging/trustping/messages/ping.py +++ b/aries_cloudagent/messaging/trustping/messages/ping.py @@ -5,9 +5,7 @@ from ...agent_message import AgentMessage, AgentMessageSchema from ..message_types import PING -HANDLER_CLASS = ( - "aries_cloudagent.messaging.trustping.handlers.ping_handler.PingHandler" -) +HANDLER_CLASS = "aries_cloudagent.messaging.trustping.handlers.ping_handler.PingHandler" class Ping(AgentMessage): diff --git a/aries_cloudagent/wallet/tests/test_basic_wallet.py b/aries_cloudagent/wallet/tests/test_basic_wallet.py index 4156740a31..dba7f5dc3f 100644 --- a/aries_cloudagent/wallet/tests/test_basic_wallet.py +++ b/aries_cloudagent/wallet/tests/test_basic_wallet.py @@ -9,9 +9,7 @@ WalletNotFoundError, ) -from aries_cloudagent.messaging.decorators.signature_decorator import ( - SignatureDecorator, -) +from aries_cloudagent.messaging.decorators.signature_decorator import SignatureDecorator @pytest.fixture() diff --git a/demo/agent.py b/demo/agent.py index 0399bcaf4c..5ddb316c24 100644 --- a/demo/agent.py +++ b/demo/agent.py @@ -89,8 +89,9 @@ def __init__( ) self.storage_type = params.get("storage_type") self.wallet_type = params.get("wallet_type", "indy") - self.wallet_name = params.get("wallet_name") or \ - self.ident.lower().replace(' ', '') + rand_name + self.wallet_name = ( + params.get("wallet_name") or self.ident.lower().replace(" ", "") + rand_name + ) self.wallet_key = params.get("wallet_key") or self.ident + rand_name self.did = None diff --git a/docs/conf.py b/docs/conf.py index 2d6898dcd9..9f01f034d2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,9 +12,23 @@ # import os import sys -sys.path.insert(0, os.path.abspath('..')) -autodoc_mock_imports = ["setup", "pysodium", "indy", "aiohttp_cors", "aiohttp", "aiohttp_apispec", "marshmallow", "base58", "msgpack", "pytest", "asynctest", "aries_cloudagent.base_handler"] +sys.path.insert(0, os.path.abspath("..")) + +autodoc_mock_imports = [ + "setup", + "pysodium", + "indy", + "aiohttp_cors", + "aiohttp", + "aiohttp_apispec", + "marshmallow", + "base58", + "msgpack", + "pytest", + "asynctest", + "aries_cloudagent.base_handler", +] # -- Project information ----------------------------------------------------- @@ -23,9 +37,9 @@ author = "Province of British Columbia" # The short X.Y version -version = '' +version = "" # The full version, including alpha/beta/rc tags -release = '' +release = "" # with open(join(dirname(dirname(dirname(__file__))), 'VERSION.txt')) as fh_version: # release = fh_version.read().strip() # version = re.sub(r'([^\.]*\.[^\.]*).*', r'\1', release) @@ -36,26 +50,26 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.napoleon', - 'sphinx.ext.ifconfig', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.ifconfig", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # -source_suffix = ['.rst', '.md'] +source_suffix = [".rst", ".md"] # source_suffix = '.rst' # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -67,7 +81,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None @@ -78,12 +92,12 @@ # a list of builtin themes. # # pip install -U sphinx-rtd-theme -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -94,9 +108,9 @@ # 'searchbox.html']``. # html_sidebars = { - '**': [ - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', + "**": [ + "relations.html", # needs 'show_related': True theme option to display + "searchbox.html", ] } @@ -104,7 +118,7 @@ # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'AriesCloudAgentPythondoc' +htmlhelp_basename = "AriesCloudAgentPythondoc" # -- Options for LaTeX output ------------------------------------------------ @@ -113,15 +127,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -131,8 +142,13 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'AriesCloudAgentPython.tex', 'Aries Cloud Agent Python Documentation', - 'Nicholas Rempel, Andrew Whitehead', 'manual'), + ( + master_doc, + "AriesCloudAgentPython.tex", + "Aries Cloud Agent Python Documentation", + "Nicholas Rempel, Andrew Whitehead", + "manual", + ) ] @@ -141,8 +157,13 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'ariescloudagentpython', 'Aries Cloud Agent Python Documentation', - [author], 1) + ( + master_doc, + "ariescloudagentpython", + "Aries Cloud Agent Python Documentation", + [author], + 1, + ) ] @@ -152,10 +173,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'AriesCloudAgentPython', 'Aries CloudAgent Python Documentation', - author, 'AriesCloudAgentPython', - 'A Hyperledger Aries cloud agent implemented in Python and suitable for use in (almost) any non-mobile environment.', - 'Miscellaneous'), + ( + master_doc, + "AriesCloudAgentPython", + "Aries CloudAgent Python Documentation", + author, + "AriesCloudAgentPython", + "A Hyperledger Aries cloud agent implemented in Python and suitable for use in (almost) any non-mobile environment.", + "Miscellaneous", + ) ] @@ -174,10 +200,10 @@ # epub_uid = '' # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # -- Extension configuration ------------------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} \ No newline at end of file +intersphinx_mapping = {"https://docs.python.org/": None} From 7efda0620a03be9fc21aed29b31a1d1aaa3a823f Mon Sep 17 00:00:00 2001 From: Nicholas Rempel Date: Fri, 12 Jul 2019 12:42:37 -0700 Subject: [PATCH 2/5] Add api token checking middleware Signed-off-by: Nicholas Rempel --- aries_cloudagent/admin/server.py | 34 +++++++++++++++-------------- aries_cloudagent/config/argparse.py | 29 ++++++++++++++++++++++++ aries_cloudagent/config/error.py | 7 ++++++ 3 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 aries_cloudagent/config/error.py diff --git a/aries_cloudagent/admin/server.py b/aries_cloudagent/admin/server.py index 1e60e19028..c823e57446 100644 --- a/aries_cloudagent/admin/server.py +++ b/aries_cloudagent/admin/server.py @@ -137,32 +137,34 @@ def __init__( async def make_application(self) -> web.Application: """Get the aiohttp application instance.""" - api_key = self.context.settings.get("api_key") - admin_unsecured = self.context.settings.get("admin_unsecured") + middlewares = [] + + admin_api_key = self.context.settings.get("admin.admin_api_key") + admin_insecure_mode = self.context.settings.get("admin.admin_insecure_mode") - # admin-token and admin-token are mutually exclusive. Should be enforced - # during parameter parsing but to be sure, we check here. - assert admin_unsecured or api_key - assert not (admin_unsecured and api_key) + # admin-token and admin-token are mutually exclusive and required. + # This should be enforced during parameter parsing but to be sure, + # we check here. + assert admin_insecure_mode or admin_api_key + assert not (admin_insecure_mode and admin_api_key) - # If api_key is None, then admin_unsecured must be set so + # If admin_api_key is None, then admin_insecure_mode must be set so # we can safely enable the admin server with no security - if api_key: + if admin_api_key: @web.middleware async def check_token(request, handler): - header_api_key = request.headers.get("x-api-key") - if not header_api_key: - raise web.HTTPUnauthorized( - "An API key must be provided in the X-API-Key header" - ) + header_admin_api_key = request.headers.get("x-api-key") + if not header_admin_api_key: + raise web.HTTPUnauthorized() - if api_key == header_api_key: + if admin_api_key == header_admin_api_key: return await handler(request) else: - raise web.HTTPUnauthorized("Invalid api key provided") + raise web.HTTPUnauthorized() + + middlewares.append(check_token) - middlewares = [] stats: Collector = await self.context.inject(Collector, required=False) if stats: diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py index 1c32b8911f..7c6b6d173a 100644 --- a/aries_cloudagent/config/argparse.py +++ b/aries_cloudagent/config/argparse.py @@ -4,6 +4,7 @@ import argparse from typing import Sequence +from .error import ArgsParseError PARSER = argparse.ArgumentParser(description="Runs an Aries Cloud Agent.") @@ -153,6 +154,19 @@ help="Enable the administration API on a given host and port", ) +PARSER.add_argument( + "--admin-api-key", + type=str, + metavar="", + help="Set the api key for the admin API.", +) + +PARSER.add_argument( + "--admin-insecure-mode", + action="store_true", + help="Do not protect the admin API with token authentication.z", +) + PARSER.add_argument("--debug", action="store_true", help="Enable debugging features") PARSER.add_argument( @@ -304,6 +318,21 @@ def get_settings(args): settings["wallet.storage_creds"] = args.wallet_storage_creds if args.admin: + + admin_api_key = args.admin_api_key + admin_insecure_mode = args.admin_insecure_mode + + if (admin_api_key and admin_insecure_mode) or not ( + admin_api_key or admin_insecure_mode + ): + raise ArgsParseError( + "Either --admin-api-key or --admin-insecure-mode " + + "must be set but not both." + ) + + settings["admin.admin_api_key"] = admin_api_key + settings["admin.admin_insecure_mode"] = admin_insecure_mode + settings["admin.enabled"] = True settings["admin.host"] = args.admin[0] settings["admin.port"] = args.admin[1] diff --git a/aries_cloudagent/config/error.py b/aries_cloudagent/config/error.py new file mode 100644 index 0000000000..2fa2e5b392 --- /dev/null +++ b/aries_cloudagent/config/error.py @@ -0,0 +1,7 @@ +"""Errors for config modules.""" + +from ..error import BaseError + + +class ArgsParseError(BaseError): + """Error raised when there is a problem parsing the command-line arguments.""" From 76096c81debf4d851f63984821792078c3d97d05 Mon Sep 17 00:00:00 2001 From: Nicholas Rempel Date: Fri, 12 Jul 2019 14:29:18 -0700 Subject: [PATCH 3/5] Add new cli args to readme Signed-off-by: Nicholas Rempel --- DevReadMe.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DevReadMe.md b/DevReadMe.md index 887866c5af..c0f7be3ea0 100644 --- a/DevReadMe.md +++ b/DevReadMe.md @@ -87,6 +87,8 @@ Most configuration parameters are provided to the the agent at startup. Refer to | `--genesis-transactions` | `--genesis-transactions {"reqSignature":{},"txn":{"data":{"d... ` | Specifies the genesis transactions to use to connect to an Hyperledger Indy ledger. | `false` | | `--genesis-url` | `--genesis-url https://example.com/genesis` | Specifies the url from which to download the genesis transaction data. For example, the [Sovrin Network genesis transactions](https://raw.githubusercontent.com/sovrin-foundation/sovrin/master/sovrin/pool_transactions_live_genesis). | `false` | | `--admin` | `--admin 0.0.0.0 5050` | Specifies the host and port on which to run the administrative server. If not provided, no admin server is made available. | `false` | +| `--admin-insecure-mode` | `--admin-insecure-mode` | Instructs the agent to run the admin web server in insecure mode. The admin server will be publicly available to anyone who has access to the interface. | `false` | +| `--admin-api-key` | `--admin-api-key abc123` | Instructs the agent to protect all admin endpoints with the provided API key. The API must be pass in the header `X-API-Key: `. | `false` | | `--debug` | `--debug` | Enables a remote debugging service that can be accessed using [ptvsd](https://github.com/Microsoft/ptvsd). The framework will wait for the debugger to connect at start-up. | `false` | | `--debug-connections` | `--debug-connections` | Enables additional logging of connection information. | `false` | | `--accept-invites` | `--accept-invites` | Instructs the agent to automatically accept invites. | `false` | From 0ebe5722bb43bcc35732f09bd64d731019ef1fe9 Mon Sep 17 00:00:00 2001 From: Nicholas Rempel Date: Mon, 15 Jul 2019 15:37:59 -0700 Subject: [PATCH 4/5] Fix test Signed-off-by: Nicholas Rempel --- aries_cloudagent/admin/tests/test_admin_server.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/aries_cloudagent/admin/tests/test_admin_server.py b/aries_cloudagent/admin/tests/test_admin_server.py index 0a025fa2db..4aa7dfc1fe 100644 --- a/aries_cloudagent/admin/tests/test_admin_server.py +++ b/aries_cloudagent/admin/tests/test_admin_server.py @@ -19,11 +19,24 @@ def get_admin_server(self) -> AdminServer: context.injector.bind_provider( BaseOutboundMessageQueue, ClassProvider(BasicOutboundMessageQueue) ) + context.settings["admin.admin_insecure_mode"] = True server = AdminServer( "0.0.0.0", unused_port(), context, self.outbound_message_router ) return server + @unittest_run_loop + async def test_start_bad_settings(self): + server = self.get_admin_server() + server.context.settings["admin.admin_insecure_mode"] = None + + try: + await server.start() + except AssertionError: + return True + + raise Exception + @unittest_run_loop async def test_start_stop(self): server = self.get_admin_server() From ff9e923072a87ab7e93f37f646b4d898463d244b Mon Sep 17 00:00:00 2001 From: Nicholas Rempel Date: Tue, 16 Jul 2019 10:14:07 -0700 Subject: [PATCH 5/5] Run demo with admin insecure mode Signed-off-by: Nicholas Rempel --- demo/agent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/agent.py b/demo/agent.py index d0ceda7ab9..fe5fb21f56 100644 --- a/demo/agent.py +++ b/demo/agent.py @@ -148,6 +148,7 @@ def get_agent_args(self): ("--inbound-transport", "http", "0.0.0.0", str(self.http_port)), ("--outbound-transport", "http"), ("--admin", "0.0.0.0", str(self.admin_port)), + "--admin-insecure-mode", ("--wallet-type", self.wallet_type), ("--wallet-name", self.wallet_name), ("--wallet-key", self.wallet_key),