From 790d061a3e9c1fdd93e51f7c4051df69d10c104f Mon Sep 17 00:00:00 2001 From: Akiff Manji Date: Thu, 7 Sep 2023 16:40:57 +0000 Subject: [PATCH 01/13] feat: devcontainer configuraton for vscode Signed-off-by: Akiff Manji --- .devcontainer/Dockerfile | 15 ++++++++++++++ .devcontainer/devcontainer.json | 28 +++++++++++++++++++++++++ .devcontainer/docker-compose.yml | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yml diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..5c8f9e58f --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/devcontainers/python:1-3.8-bookworm + +ENV PYTHONUNBUFFERED 1 + +# [Optional] If your requirements rarely change, uncomment this section to add them to the image. +# COPY requirements.txt /tmp/pip-tmp/ +# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ +# && rm -rf /tmp/pip-tmp + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + + + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..dd57cbfbf --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/postgres +{ + "name": "Python 3 & PostgreSQL", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "features": { + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, + "ghcr.io/itsmechlark/features/postgresql:1": {} + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // This can be used to network with other containers or the host. + // "forwardPorts": [5000, 5432], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip install --user -r requirements.txt", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..f2e9705b0 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,35 @@ +version: '3.8' + +services: + app: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + + volumes: + - ../..:/workspaces:cached + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + + # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. + network_mode: service:db + + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + + db: + image: postgres:latest + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: postgres + POSTGRES_DB: postgres + POSTGRES_PASSWORD: postgres + + # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + +volumes: + postgres-data: From 09f109e4e206116351f77d5474eedc90ae092330 Mon Sep 17 00:00:00 2001 From: Akiff Manji Date: Thu, 7 Sep 2023 16:41:41 +0000 Subject: [PATCH 02/13] feat: hard code digital business card schema Signed-off-by: Akiff Manji --- .../legal_api/services/digital_credentials.py | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/legal-api/src/legal_api/services/digital_credentials.py b/legal-api/src/legal_api/services/digital_credentials.py index f606fc881..6f2339db7 100644 --- a/legal-api/src/legal_api/services/digital_credentials.py +++ b/legal-api/src/legal_api/services/digital_credentials.py @@ -27,17 +27,34 @@ class DigitalCredentialsService: """Provides services to do digital credentials using aca-py agent.""" - business_schema = { + # business_schema = { + # 'attributes': [ + # 'legalName', + # 'foundingDate', + # 'taxId', + # 'homeJurisdiction', + # 'legalType', + # 'identifier' + # ], + # 'schema_name': 'business_schema', # do not change schema name. this is the name registered in aca-py agent + # 'schema_version': '1.0.0' # if attributes changes update schema_version to re-register + # } + + digital_business_card_schema = { 'attributes': [ - 'legalName', - 'foundingDate', - 'taxId', - 'homeJurisdiction', - 'legalType', - 'identifier' + 'business_name', + 'company_status', + 'credential_id', + 'identifier', + 'registered_on_dateint', + 'role', + 'cra_business_number', + 'family_name', + 'business_type', + 'given_names', ], - 'schema_name': 'business_schema', # do not change schema name. this is the name registered in aca-py agent - 'schema_version': '1.0.0' # if attributes changes update schema_version to re-register + 'schema_name': 'digital_business_card', + 'schema_version': '1.0.0' } def __init__(self): @@ -46,6 +63,7 @@ def __init__(self): self.api_url = None self.api_key = None + self.api_token = None self.entity_did = None def init_app(self, app): @@ -54,6 +72,7 @@ def init_app(self, app): self.api_url = app.config.get('ACA_PY_ADMIN_API_URL') self.api_key = app.config.get('ACA_PY_ADMIN_API_KEY') + self.api_token = app.config.get('ACA_PY_ADMIN_API_TOKEN') self.entity_did = app.config.get('ACA_PY_ENTITY_DID') with suppress(Exception): self._register_business() @@ -63,22 +82,22 @@ def _register_business(self): # check for the current schema definition. definition = DCDefinition.find_by( credential_type=DCDefinition.CredentialType.business, - schema_name=self.business_schema['schema_name'], - schema_version=self.business_schema['schema_version'] + schema_name=self.digital_business_card_schema['schema_name'], + schema_version=self.digital_business_card_schema['schema_version'] ) if definition: if definition.is_deleted: - raise Exception('Digital Credentials: business_schema is marked as delete, fix it.') # noqa: E501; pylint: disable=broad-exception-raised, line-too-long + raise Exception('Digital Credentials: digital_business_card_schema is marked as delete, fix it.') # noqa: E501; pylint: disable=broad-exception-raised, line-too-long else: # deactivate any existing schema definition before registering new one DCDefinition.deactivate(DCDefinition.CredentialType.business) - schema_id = self._register_schema(self.business_schema) + schema_id = self._register_schema(self.digital_business_card_schema) definition = DCDefinition( credential_type=DCDefinition.CredentialType.business, - schema_name=self.business_schema['schema_name'], - schema_version=self.business_schema['schema_version'], + schema_name=self.digital_business_card_schema['schema_name'], + schema_version=self.digital_business_card_schema['schema_version'], schema_id=schema_id ) definition.save() @@ -110,7 +129,7 @@ def _register_credential_definitions(self, schema_id: str) -> Optional[str]: 'revocation_registry_size': 1000, 'schema_id': schema_id, 'support_revocation': True, - 'tag': 'business_schema' + 'tag': 'Digital Business Card' })) response.raise_for_status() return response.json()['credential_definition_id'] @@ -165,5 +184,5 @@ def issue_credential(self, def _get_headers(self) -> dict: return { 'Content-Type': 'application/json', - 'X-API-KEY': self.api_key + 'Authorization': f'Bearer {self.api_token}' } From 671fc7eb5918de0d1830284f1b2256370c70a296 Mon Sep 17 00:00:00 2001 From: Akiff Manji Date: Thu, 7 Sep 2023 16:41:41 +0000 Subject: [PATCH 03/13] feat: hard code digital business card schema Signed-off-by: Akiff Manji --- legal-api/src/legal_api/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/legal-api/src/legal_api/config.py b/legal-api/src/legal_api/config.py index 283dc81b8..caab458b6 100644 --- a/legal-api/src/legal_api/config.py +++ b/legal-api/src/legal_api/config.py @@ -146,6 +146,7 @@ class _Config(): # pylint: disable=too-few-public-methods ACA_PY_ADMIN_API_URL = os.getenv('ACA_PY_ADMIN_API_URL') ACA_PY_ADMIN_API_KEY = os.getenv('ACA_PY_ADMIN_API_KEY') + ACA_PY_ADMIN_API_TOKEN = os.getenv('ACA_PY_ADMIN_API_TOKEN') ACA_PY_ENTITY_DID = os.getenv('ACA_PY_ENTITY_DID') TESTING = False From ff81e1ba84b1872a2e62ed570b914aef4d29c896 Mon Sep 17 00:00:00 2001 From: Akiff Manji Date: Mon, 11 Sep 2023 23:49:02 +0000 Subject: [PATCH 04/13] feat: issue credentials through Traction tenant Signed-off-by: Akiff Manji --- legal-api/src/legal_api/config.py | 8 +- .../business/business_digital_credentials.py | 37 +++-- .../legal_api/services/digital_credentials.py | 129 +++++++++++------- 3 files changed, 106 insertions(+), 68 deletions(-) diff --git a/legal-api/src/legal_api/config.py b/legal-api/src/legal_api/config.py index caab458b6..d4a00c0f1 100644 --- a/legal-api/src/legal_api/config.py +++ b/legal-api/src/legal_api/config.py @@ -144,10 +144,10 @@ class _Config(): # pylint: disable=too-few-public-methods NAICS_API_URL = os.getenv('NAICS_API_URL', 'https://NAICS_API_URL/api/v2/naics') - ACA_PY_ADMIN_API_URL = os.getenv('ACA_PY_ADMIN_API_URL') - ACA_PY_ADMIN_API_KEY = os.getenv('ACA_PY_ADMIN_API_KEY') - ACA_PY_ADMIN_API_TOKEN = os.getenv('ACA_PY_ADMIN_API_TOKEN') - ACA_PY_ENTITY_DID = os.getenv('ACA_PY_ENTITY_DID') + # digital credential configuration values + TRACTION_API_URL = os.getenv('TRACTION_API_URL') + TRACTION_API_TOKEN = os.getenv('TRACTION_API_TOKEN') + TRACTION_PUBLIC_DID = os.getenv('TRACTION_PUBLIC_DID') TESTING = False DEBUG = False diff --git a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py index 3d9f040b6..a77ecfc76 100644 --- a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py +++ b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py @@ -148,28 +148,44 @@ def _get_data_for_credential(credential_type: DCDefinition.CredentialType, busin if credential_type == DCDefinition.CredentialType.business: return [ { - 'name': 'legalName', + 'name': 'credential_id', + 'value': '' + }, + { + 'name': 'identifier', + 'value': business.identifier + }, + { + 'name': 'business_name', 'value': business.legal_name }, { - 'name': 'foundingDate', - 'value': business.founding_date.isoformat() + 'name': 'business_type', + 'value': business.legal_type }, { - 'name': 'taxId', + 'name': 'cra_business_number', 'value': business.tax_id or '' }, { - 'name': 'homeJurisdiction', - 'value': 'BC' # for corp types that are not -xpro, the jurisdiction is BC + 'name': 'registered_on_dateint', + 'value': business.founding_date.isoformat() }, { - 'name': 'legalType', - 'value': business.legal_type + 'name': 'company_status', + 'value': business.state }, { - 'name': 'identifier', - 'value': business.identifier + 'name': 'family_name', + 'value': '' + }, + { + 'name': 'given_names', + 'value': '' + }, + { + 'name': 'role', + 'value': '' } ] @@ -178,7 +194,6 @@ def _get_data_for_credential(credential_type: DCDefinition.CredentialType, busin @bp_dc.route('/topic/', methods=['POST'], strict_slashes=False) @cross_origin(origin='*') -@jwt.requires_auth def webhook_notification(topic_name: str): """To receive notification from aca-py admin api.""" json_input = request.get_json() diff --git a/legal-api/src/legal_api/services/digital_credentials.py b/legal-api/src/legal_api/services/digital_credentials.py index 6f2339db7..db6c446e6 100644 --- a/legal-api/src/legal_api/services/digital_credentials.py +++ b/legal-api/src/legal_api/services/digital_credentials.py @@ -27,20 +27,7 @@ class DigitalCredentialsService: """Provides services to do digital credentials using aca-py agent.""" - # business_schema = { - # 'attributes': [ - # 'legalName', - # 'foundingDate', - # 'taxId', - # 'homeJurisdiction', - # 'legalType', - # 'identifier' - # ], - # 'schema_name': 'business_schema', # do not change schema name. this is the name registered in aca-py agent - # 'schema_version': '1.0.0' # if attributes changes update schema_version to re-register - # } - - digital_business_card_schema = { + business_schema = { 'attributes': [ 'business_name', 'company_status', @@ -53,8 +40,8 @@ class DigitalCredentialsService: 'business_type', 'given_names', ], - 'schema_name': 'digital_business_card', - 'schema_version': '1.0.0' + 'schema_name': 'digital_business_card', # do not change schema name. this is the name registered in aca-py agent + 'schema_version': '1.0.0' # if attributes changes update schema_version to re-register } def __init__(self): @@ -62,74 +49,110 @@ def __init__(self): self.app = None self.api_url = None - self.api_key = None self.api_token = None - self.entity_did = None + self.public_did = None def init_app(self, app): """Initialize digital credentials using aca-py agent.""" self.app = app - self.api_url = app.config.get('ACA_PY_ADMIN_API_URL') - self.api_key = app.config.get('ACA_PY_ADMIN_API_KEY') - self.api_token = app.config.get('ACA_PY_ADMIN_API_TOKEN') - self.entity_did = app.config.get('ACA_PY_ENTITY_DID') + self.api_url = app.config.get('TRACTION_API_URL') + self.api_token = app.config.get('TRACTION_API_TOKEN') + self.public_did = app.config.get('TRACTION_PUBLIC_DID') with suppress(Exception): - self._register_business() + self._register_business_definition() - def _register_business(self): - """Register business schema and credential definition.""" - # check for the current schema definition. + def _register_business_definition(self): + """Publish Business schema and credential definition and save a Business definition.""" + # check for the current Business definition. definition = DCDefinition.find_by( credential_type=DCDefinition.CredentialType.business, - schema_name=self.digital_business_card_schema['schema_name'], - schema_version=self.digital_business_card_schema['schema_version'] + schema_name=self.business_schema['schema_name'], + schema_version=self.business_schema['schema_version'] ) - if definition: - if definition.is_deleted: - raise Exception('Digital Credentials: digital_business_card_schema is marked as delete, fix it.') # noqa: E501; pylint: disable=broad-exception-raised, line-too-long - else: - # deactivate any existing schema definition before registering new one + if definition and not definition.is_deleted: + # deactivate any existing Business definition before creating new one DCDefinition.deactivate(DCDefinition.CredentialType.business) - schema_id = self._register_schema(self.digital_business_card_schema) - definition = DCDefinition( - credential_type=DCDefinition.CredentialType.business, - schema_name=self.digital_business_card_schema['schema_name'], - schema_version=self.digital_business_card_schema['schema_version'], - schema_id=schema_id - ) - definition.save() + # look for a published schema first, if it's not there then register one. + schema_id = self._get_schema(self.business_schema) # TODO: This should look up the last updated definition in Traction storage + if not schema_id: + schema_id = self._publish_schema(self.business_schema) + # create a new definition and add the new schema_id + definition = DCDefinition( + credential_type=DCDefinition.CredentialType.business, + schema_name=self.business_schema['schema_name'], + schema_version=self.business_schema['schema_version'], + schema_id=schema_id + ) + + # look for a published credential definition first, if it's not there then register one. if not definition.credential_definition_id: - definition.credential_definition_id = self._register_credential_definitions(definition.schema_id) - definition.save() + schema_id = definition.schema_id + credential_definition_id = self._get_credential_definition(schema_id) # TODO: this should look up the last updated credential definition in Traction storage + if not credential_definition_id: + credential_definition_id = self._publish_credential_definition(schema_id) + + # add the new credential_definition_id + definition.credential_definition_id = credential_definition_id + + # lastly, save the definition + definition.save() - def _register_schema(self, schema: dict) -> Optional[str]: - """Send a schema to the ledger.""" + def _get_schema(self, schema: dict) -> Optional[str]: + """Find a published schema""" + try: + response = requests.get(self.api_url + '/schemas/created', + params={'schema_name': schema['schema_name'], + 'schema_version': schema['schema_version']}, + headers=self._get_headers()) + response.raise_for_status() + return response.json()['schema_ids'][0] + except Exception as err: + self.app.logger.error( + f"Failed to find digital credential schema {schema['schema_name']}:{schema['schema_version']}") + self.app.logger.error(err) + raise err + + def _publish_schema(self, schema: dict) -> Optional[str]: + """Publish a schema onto the ledger.""" try: response = requests.post(self.api_url + '/schemas', headers=self._get_headers(), data=json.dumps(schema)) response.raise_for_status() - return response.json()['schema_id'] + return response.json()[0]['schema_id'] except Exception as err: self.app.logger.error( f"Failed to register digital credential schema {schema['schema_name']}:{schema['schema_version']}") self.app.logger.error(err) raise err + + def _get_credential_definition(self, schema_id: str) -> Optional[str]: + """Find a published credential definition""" + try: + response = requests.get(self.api_url + '/credential-definitions/created', + params={'schema_id': schema_id}, + headers=self._get_headers()) + response.raise_for_status() + return response.json()['credential_definition_ids'][0] + except Exception as err: + self.app.logger.error(f'Failed to find credential definition with schema_id:{schema_id}') + self.app.logger.error(err) + raise err - def _register_credential_definitions(self, schema_id: str) -> Optional[str]: - """Send a credential definition to the ledger.""" + def _publish_credential_definition(self, schema_id: str) -> Optional[str]: + """Publish a credential definition onto the ledger.""" try: response = requests.post(self.api_url + '/credential-definitions', headers=self._get_headers(), data=json.dumps({ - 'revocation_registry_size': 1000, + # 'revocation_registry_size': 1000, 'schema_id': schema_id, - 'support_revocation': True, - 'tag': 'Digital Business Card' + 'support_revocation': False, + 'tag': 'DigitalBusinessCard' })) response.raise_for_status() return response.json()['credential_definition_id'] @@ -168,9 +191,9 @@ def issue_credential(self, '@type': 'issue-credential/1.0/credential-preview', 'attributes': data }, - 'issuer_did': self.entity_did, + 'issuer_did': self.public_did, 'schema_id': definition.schema_id, - 'schema_issuer_did': self.entity_did, + 'schema_issuer_did': self.public_did, 'schema_name': definition.schema_name, 'schema_version': definition.schema_version, 'trace': True From ed2a4c5b624c806c27fed617b7d5f586aad41ab9 Mon Sep 17 00:00:00 2001 From: Akiff Manji Date: Tue, 12 Sep 2023 17:27:49 +0000 Subject: [PATCH 05/13] refactor: app initialization workflow Signed-off-by: Akiff Manji --- .devcontainer/devcontainer.json | 7 +- legal-api/src/legal_api/config.py | 10 +- .../src/legal_api/models/dc_definition.py | 11 +- .../legal_api/services/digital_credentials.py | 161 +++++++++--------- 4 files changed, 96 insertions(+), 93 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index dd57cbfbf..6d9f05ae9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ "features": { "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, "ghcr.io/itsmechlark/features/postgresql:1": {} - } + }, // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, @@ -24,5 +24,8 @@ // "customizations": {}, // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + // "remoteUser": "root", + + // Enable this on OSX to add ssh key to agent inside container + "initializeCommand": "find ~/.ssh/ -type f -exec grep -l 'PRIVATE' {} \\; | xargs ssh-add" } diff --git a/legal-api/src/legal_api/config.py b/legal-api/src/legal_api/config.py index d4a00c0f1..ed13c9c9b 100644 --- a/legal-api/src/legal_api/config.py +++ b/legal-api/src/legal_api/config.py @@ -144,10 +144,16 @@ class _Config(): # pylint: disable=too-few-public-methods NAICS_API_URL = os.getenv('NAICS_API_URL', 'https://NAICS_API_URL/api/v2/naics') - # digital credential configuration values + # Digital Credential configuration values TRACTION_API_URL = os.getenv('TRACTION_API_URL') TRACTION_API_TOKEN = os.getenv('TRACTION_API_TOKEN') - TRACTION_PUBLIC_DID = os.getenv('TRACTION_PUBLIC_DID') + TRACTION_PUBLIC_SCHEMA_DID = os.getenv('TRACTION_PUBLIC_SCHEMA_DID') + TRACTION_PUBLIC_ISSUER_DID = os.getenv('TRACTION_PUBLIC_ISSUER_DID') + + BUSINESS_SCHEMA_NAME = os.getenv('BUSINESS_SCHEMA_NAME') + BUSINESS_SCHEMA_VERSION = os.getenv('BUSINESS_SCHEMA_VERSION') + BUSINESS_SCHEMA_ID = os.getenv('BUSINESS_SCHEMA_ID') + BUSINESS_CRED_DEF_ID = os.getenv('BUSINESS_CRED_DEF_ID') TESTING = False DEBUG = False diff --git a/legal-api/src/legal_api/models/dc_definition.py b/legal-api/src/legal_api/models/dc_definition.py index 0094c8ff4..11158b90d 100644 --- a/legal-api/src/legal_api/models/dc_definition.py +++ b/legal-api/src/legal_api/models/dc_definition.py @@ -83,14 +83,15 @@ def find_by_credential_type(cls, credential_type: CredentialType) -> DCDefinitio @classmethod def find_by(cls, credential_type: CredentialType, - schema_name: str, - schema_version: str) -> DCDefinition: + schema_id: str, + credential_definition_id: str, + ) -> DCDefinition: """Return the digital credential definition matching the filter.""" query = db.session.query(DCDefinition). \ + filter(DCDefinition.is_deleted == False). \ filter(DCDefinition.credential_type == credential_type). \ - filter(DCDefinition.schema_name == schema_name). \ - filter(DCDefinition.schema_version == schema_version) - + filter(DCDefinition.schema_id == schema_id). \ + filter(DCDefinition.credential_definition_id == credential_definition_id) return query.one_or_none() @classmethod diff --git a/legal-api/src/legal_api/services/digital_credentials.py b/legal-api/src/legal_api/services/digital_credentials.py index db6c446e6..1addcb394 100644 --- a/legal-api/src/legal_api/services/digital_credentials.py +++ b/legal-api/src/legal_api/services/digital_credentials.py @@ -50,7 +50,13 @@ def __init__(self): self.api_url = None self.api_token = None - self.public_did = None + self.public_schema_did = None + self.public_issuer_did = None + + self.business_schema_name = None + self.business_schema_version = None + self.business_schema_id = None + self.business_cred_def_id = None def init_app(self, app): """Initialize digital credentials using aca-py agent.""" @@ -58,106 +64,93 @@ def init_app(self, app): self.api_url = app.config.get('TRACTION_API_URL') self.api_token = app.config.get('TRACTION_API_TOKEN') - self.public_did = app.config.get('TRACTION_PUBLIC_DID') + self.public_schema_did = app.config.get('TRACTION_PUBLIC_SCHEMA_DID') + self.public_issuer_did = app.config.get('TRACTION_PUBLIC_ISSUER_DID') + + self.business_schema_name = app.config.get('BUSINESS_SCHEMA_NAME') + self.business_schema_version = app.config.get('BUSINESS_SCHEMA_VERSION') + self.business_schema_id = app.config.get('BUSINESS_SCHEMA_ID') + self.business_cred_def_id = app.config.get('BUSINESS_CRED_DEF_ID') + with suppress(Exception): self._register_business_definition() def _register_business_definition(self): - """Publish Business schema and credential definition and save a Business definition.""" - # check for the current Business definition. - definition = DCDefinition.find_by( - credential_type=DCDefinition.CredentialType.business, - schema_name=self.business_schema['schema_name'], - schema_version=self.business_schema['schema_version'] - ) - - if definition and not definition.is_deleted: - # deactivate any existing Business definition before creating new one - DCDefinition.deactivate(DCDefinition.CredentialType.business) - - # look for a published schema first, if it's not there then register one. - schema_id = self._get_schema(self.business_schema) # TODO: This should look up the last updated definition in Traction storage - if not schema_id: - schema_id = self._publish_schema(self.business_schema) - - # create a new definition and add the new schema_id - definition = DCDefinition( - credential_type=DCDefinition.CredentialType.business, - schema_name=self.business_schema['schema_name'], - schema_version=self.business_schema['schema_version'], - schema_id=schema_id - ) - - # look for a published credential definition first, if it's not there then register one. - if not definition.credential_definition_id: - schema_id = definition.schema_id - credential_definition_id = self._get_credential_definition(schema_id) # TODO: this should look up the last updated credential definition in Traction storage - if not credential_definition_id: - credential_definition_id = self._publish_credential_definition(schema_id) - - # add the new credential_definition_id - definition.credential_definition_id = credential_definition_id - - # lastly, save the definition - definition.save() - - def _get_schema(self, schema: dict) -> Optional[str]: - """Find a published schema""" try: - response = requests.get(self.api_url + '/schemas/created', - params={'schema_name': schema['schema_name'], - 'schema_version': schema['schema_version']}, - headers=self._get_headers()) - response.raise_for_status() - return response.json()['schema_ids'][0] + if (self.business_schema_id is None): + self.app.logger.error(f'Environment variable: BUSINESS_SCHEMA_ID must be configured') + raise Exception(f'Environment variable: BUSINESS_SCHEMA_ID must be configured') + + if (self.business_cred_def_id is None): + self.app.logger.error(f'Environment variable: BUSINESS_CRED_DEF_ID must be configured') + raise Exception(f'Environment variable: BUSINESS_CRED_DEF_ID must be configured') + + """Fetch schema and credential definition and save a Business definition.""" + # Check for the current Business definition. + definition = DCDefinition.find_by( + credential_type=DCDefinition.CredentialType.business, + schema_id=self.business_schema_id, + credential_definition_id=self.business_cred_def_id + ) + + if definition and not definition.is_deleted: + # Deactivate any existing Business definition before creating new one + DCDefinition.deactivate(DCDefinition.CredentialType.business) + + ### + # The following just a sanity check to make sure the schema and credential definition are stored in Traction tenant + # These calls also include a ledger lookup to see if the schema and credential definition are published + ### + + # Look for a schema first, and copy it into the Traction tenant if it's not there + schema_id = self._fetch_schema(self.business_schema_id) + if not schema_id: + raise Exception(f'Schema with id:{self.business_schema_id} must be available in Traction tenant storage') + + # Look for a published credential definition first, and copy it into the Traction tenant if it's not there + credential_definition_id = self._fetch_credential_definition(self.business_cred_def_id) + if not credential_definition_id: + raise Exception(f'Credential Definition with id:{self.business_cred_def_id} must be avaible in Traction tenant storage') + + # Create a new definition and add the new schema_id + definition = DCDefinition( + credential_type=DCDefinition.CredentialType.business, + schema_name=self.business_schema_name, + schema_version=self.business_schema_version, + schema_id=schema_id, + credential_definition_id=credential_definition_id + ) + # Lastly, save the definition + definition.save() except Exception as err: - self.app.logger.error( - f"Failed to find digital credential schema {schema['schema_name']}:{schema['schema_version']}") self.app.logger.error(err) - raise err + return None - def _publish_schema(self, schema: dict) -> Optional[str]: - """Publish a schema onto the ledger.""" + def _fetch_schema(self, schema_id: str) -> Optional[str]: + """Find a schema in Traction storage""" try: - response = requests.post(self.api_url + '/schemas', - headers=self._get_headers(), - data=json.dumps(schema)) - response.raise_for_status() - return response.json()[0]['schema_id'] - except Exception as err: - self.app.logger.error( - f"Failed to register digital credential schema {schema['schema_name']}:{schema['schema_version']}") - self.app.logger.error(err) - raise err - - def _get_credential_definition(self, schema_id: str) -> Optional[str]: - """Find a published credential definition""" - try: - response = requests.get(self.api_url + '/credential-definitions/created', + response = requests.get(self.api_url + '/schema-storage', params={'schema_id': schema_id}, headers=self._get_headers()) response.raise_for_status() - return response.json()['credential_definition_ids'][0] + first_or_default = next((x for x in response.json()['results'] if x['schema_id'] == schema_id), None) + return first_or_default['schema_id'] if first_or_default else None except Exception as err: - self.app.logger.error(f'Failed to find credential definition with schema_id:{schema_id}') + self.app.logger.error(f"Failed to fetch schema with id:{schema_id} from Traction tenant storage") self.app.logger.error(err) raise err - def _publish_credential_definition(self, schema_id: str) -> Optional[str]: - """Publish a credential definition onto the ledger.""" + def _fetch_credential_definition(self, cred_def_id: str) -> Optional[str]: + """Find a published credential definition""" try: - response = requests.post(self.api_url + '/credential-definitions', - headers=self._get_headers(), - data=json.dumps({ - # 'revocation_registry_size': 1000, - 'schema_id': schema_id, - 'support_revocation': False, - 'tag': 'DigitalBusinessCard' - })) + response = requests.get(self.api_url + '/credential-definition-storage', + params={'cred_def_id': cred_def_id}, + headers=self._get_headers()) response.raise_for_status() - return response.json()['credential_definition_id'] + first_or_default = next((x for x in response.json()['results'] if x['cred_def_id'] == cred_def_id), None) + return first_or_default['cred_def_id'] if first_or_default else None except Exception as err: - self.app.logger.error(f'Failed to register credential definition schema_id:{schema_id}') + self.app.logger.error(f'Failed to find credential definition with id:{cred_def_id} from Traction tenant storage') self.app.logger.error(err) raise err @@ -191,9 +184,9 @@ def issue_credential(self, '@type': 'issue-credential/1.0/credential-preview', 'attributes': data }, - 'issuer_did': self.public_did, + 'issuer_did': self.public_issuer_did, 'schema_id': definition.schema_id, - 'schema_issuer_did': self.public_did, + 'schema_issuer_did': self.public_schema_did, 'schema_name': definition.schema_name, 'schema_version': definition.schema_version, 'trace': True From c1de48dc2451f5dee85ecf53c2012ce11ca0c66b Mon Sep 17 00:00:00 2001 From: Akiff Manji Date: Wed, 20 Sep 2023 23:38:19 +0000 Subject: [PATCH 06/13] feat: use out-of-band invitation for connecting Signed-off-by: Akiff Manji --- .../v2/business/business_digital_credentials.py | 16 ++++++++++------ .../legal_api/services/digital_credentials.py | 13 ++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py index a77ecfc76..f9e1f4b87 100644 --- a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py +++ b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py @@ -52,7 +52,7 @@ def create_invitation(identifier): return jsonify({'message': 'Unable to create an invitation.'}), HTTPStatus.INTERNAL_SERVER_ERROR connection = DCConnection( - connection_id=invitation['connection_id'], + connection_id=invitation['invitation']['@id'], invitation_url=invitation['invitation_url'], is_active=False, connection_state='invitation', @@ -199,11 +199,15 @@ def webhook_notification(topic_name: str): json_input = request.get_json() try: if topic_name == 'connections': - connection = DCConnection.find_by_connection_id(json_input['connection_id']) - # Trinsic Wallet will send `active` only when it’s used the first time. - # Looking for `response` state to handle it. - if connection and not connection.is_active and json_input['state'] in ('response', 'active'): - connection.connection_state = 'active' + if 'invitation' in json_input and json_input['invitation'] != None: + connection = DCConnection.find_by_connection_id(json_input['invitation']['@id']) + else: + connection = DCConnection.find_by_connection_id(json_input['invitation_msg_id']) + # Using https://didcomm.org/connections/1.0 protocol the final state is 'active' + # Using https://didcomm.org/didexchange/1.0 protocol the final state is 'completed' + if connection and not connection.is_active and json_input['state'] in ('active', 'completed'): + connection.connection_id = json_input['connection_id'] + connection.connection_state = json_input['state'] connection.is_active = True connection.save() elif topic_name == 'issue_credential': diff --git a/legal-api/src/legal_api/services/digital_credentials.py b/legal-api/src/legal_api/services/digital_credentials.py index 1addcb394..3b9968ddf 100644 --- a/legal-api/src/legal_api/services/digital_credentials.py +++ b/legal-api/src/legal_api/services/digital_credentials.py @@ -80,10 +80,10 @@ def _register_business_definition(self): if (self.business_schema_id is None): self.app.logger.error(f'Environment variable: BUSINESS_SCHEMA_ID must be configured') raise Exception(f'Environment variable: BUSINESS_SCHEMA_ID must be configured') - + if (self.business_cred_def_id is None): self.app.logger.error(f'Environment variable: BUSINESS_CRED_DEF_ID must be configured') - raise Exception(f'Environment variable: BUSINESS_CRED_DEF_ID must be configured') + raise Exception(f'Environment variable: BUSINESS_CRED_DEF_ID must be configured') """Fetch schema and credential definition and save a Business definition.""" # Check for the current Business definition. @@ -120,7 +120,7 @@ def _register_business_definition(self): schema_id=schema_id, credential_definition_id=credential_definition_id ) - # Lastly, save the definition + # Lastly, save the definition definition.save() except Exception as err: self.app.logger.error(err) @@ -157,9 +157,12 @@ def _fetch_credential_definition(self, cred_def_id: str) -> Optional[str]: def create_invitation(self) -> Optional[dict]: """Create a new connection invitation.""" try: - response = requests.post(self.api_url + '/connections/create-invitation', + response = requests.post(self.api_url + '/out-of-band/create-invitation', headers=self._get_headers(), - data={}) + params={'auto_accept': 'true'}, + data=json.dumps({ + 'handshake_protocols': ['https://didcomm.org/connections/1.0'] + })) response.raise_for_status() return response.json() except Exception as err: From f0b71151bb56cf4cbfcb39188566dc6c25e6fdd9 Mon Sep 17 00:00:00 2001 From: Akiff Manji Date: Thu, 21 Sep 2023 00:40:15 +0000 Subject: [PATCH 07/13] feat: use v2.0 for issuing credential Signed-off-by: Akiff Manji --- .../business/business_digital_credentials.py | 8 +++--- .../legal_api/services/digital_credentials.py | 28 +++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py index f9e1f4b87..690c68778 100644 --- a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py +++ b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py @@ -137,7 +137,7 @@ def send_credential(identifier, credential_type): issued_credential = DCIssuedCredential( dc_definition_id=definition.id, dc_connection_id=connection.id, - credential_exchange_id=response['credential_exchange_id'] + credential_exchange_id=response['cred_ex_id'] ) issued_credential.save() @@ -210,9 +210,9 @@ def webhook_notification(topic_name: str): connection.connection_state = json_input['state'] connection.is_active = True connection.save() - elif topic_name == 'issue_credential': - issued_credential = DCIssuedCredential.find_by_credential_exchange_id(json_input['credential_exchange_id']) - if issued_credential and json_input['state'] == 'credential_issued': + elif topic_name == 'issue_credential_v2_0': + issued_credential = DCIssuedCredential.find_by_credential_exchange_id(json_input['cred_ex_id']) + if issued_credential and json_input['state'] in ('credential-issued', 'done'): issued_credential.date_of_issue = datetime.utcnow() issued_credential.is_issued = True issued_credential.save() diff --git a/legal-api/src/legal_api/services/digital_credentials.py b/legal-api/src/legal_api/services/digital_credentials.py index 3b9968ddf..06b50da73 100644 --- a/legal-api/src/legal_api/services/digital_credentials.py +++ b/legal-api/src/legal_api/services/digital_credentials.py @@ -40,8 +40,8 @@ class DigitalCredentialsService: 'business_type', 'given_names', ], - 'schema_name': 'digital_business_card', # do not change schema name. this is the name registered in aca-py agent - 'schema_version': '1.0.0' # if attributes changes update schema_version to re-register + 'schema_name': 'digital_business_card', # do not change schema name. this is the name registered in aca-py agent + 'schema_version': '1.0.0' # if attributes changes update schema_version to re-register } def __init__(self): @@ -176,22 +176,26 @@ def issue_credential(self, comment: str = '') -> Optional[dict]: """Send holder a credential, automating entire flow.""" try: - response = requests.post(self.api_url + '/issue-credential/send', + response = requests.post(self.api_url + '/issue-credential-2.0/send', headers=self._get_headers(), data=json.dumps({ - 'auto_remove': True, + 'auto_remove': 'true', 'comment': comment, 'connection_id': connection_id, - 'cred_def_id': definition.credential_definition_id, - 'credential_proposal': { - '@type': 'issue-credential/1.0/credential-preview', + 'credential_preview': { + '@type': 'issue-credential/2.0/credential-preview', 'attributes': data }, - 'issuer_did': self.public_issuer_did, - 'schema_id': definition.schema_id, - 'schema_issuer_did': self.public_schema_did, - 'schema_name': definition.schema_name, - 'schema_version': definition.schema_version, + 'filter': { + 'indy': { + 'cred_def_id': definition.credential_definition_id, + 'issuer_did': self.public_issuer_did, + 'schema_id': definition.schema_id, + 'schema_issuer_did': self.public_schema_did, + 'schema_name': definition.schema_name, + 'schema_version': definition.schema_version, + } + }, 'trace': True })) response.raise_for_status() From e8438cc85a57d49b60cc54f170efc1ee06888780 Mon Sep 17 00:00:00 2001 From: Akiff Manji Date: Wed, 11 Oct 2023 19:08:04 +0000 Subject: [PATCH 08/13] feat: web socket implmentation with flask-socketio Signed-off-by: Akiff Manji --- legal-api/Makefile | 5 +- legal-api/requirements.txt | 13 ++--- legal-api/requirements.txt.1 | 52 ------------------- legal-api/src/legal_api/__init__.py | 3 ++ legal-api/src/legal_api/extensions.py | 9 ++++ .../business/business_digital_credentials.py | 22 +++++--- legal-api/wsgi.py | 3 +- 7 files changed, 40 insertions(+), 67 deletions(-) delete mode 100644 legal-api/requirements.txt.1 create mode 100644 legal-api/src/legal_api/extensions.py diff --git a/legal-api/Makefile b/legal-api/Makefile index 37e2570c8..ea26d5527 100644 --- a/legal-api/Makefile +++ b/legal-api/Makefile @@ -131,7 +131,10 @@ tag: push ## tag image # COMMANDS - Local # ################################################################################# run: ## Run the project in local - . venv/bin/activate && python -m flask run -p 5000 + . venv/bin/activate && python -m flask run -p 5050 + +run-websockets: ## Run the project in local with websockets + . venv/bin/activate && python -m gunicorn --threads 100 --bind :5050 wsgi ################################################################################# # Self Documenting Commands # diff --git a/legal-api/requirements.txt b/legal-api/requirements.txt index c7e747588..aacc5801c 100755 --- a/legal-api/requirements.txt +++ b/legal-api/requirements.txt @@ -6,15 +6,16 @@ Flask-Moment==0.11.0 Flask-Pydantic==0.8.0 Flask-SQLAlchemy==2.5.1 Flask-Script==2.0.6 +Flask-SocketIO==5.3.6 Flask==1.1.2 Jinja2==2.11.3 Mako==1.1.4 MarkupSafe==1.1.1 +PyPDF2==1.26.0 SQLAlchemy-Continuum==1.3.13 SQLAlchemy-Utils==0.37.8 SQLAlchemy==1.4.44 Werkzeug==1.0.1 -nest_asyncio alembic==1.7.5 aniso8601==9.0.1 asyncio-nats-client==0.11.4 @@ -31,11 +32,15 @@ ecdsa==0.14.1 expiringdict==1.1.4 flask-jwt-oidc==0.3.0 flask-restx==0.3.0 +git+https://github.com/bcgov/business-schemas.git@2.18.10#egg=registry_schemas gunicorn==20.1.0 +html-sanitizer==1.9.3 idna==2.10 itsdangerous==1.1.0 jsonschema==4.16.0 launchdarkly-server-sdk==7.1.0 +minio==7.0.2 +nest_asyncio protobuf==3.15.8 psycopg2-binary==2.8.6 pyRFC3339==1.1 @@ -48,6 +53,7 @@ python-dotenv==0.17.1 python-editor==1.0.4 python-jose==3.2.0 pytz==2021.1 +reportlab==3.6.12 requests==2.25.1 rsa==4.7.2 semver==2.13.0 @@ -55,8 +61,3 @@ sentry-sdk==1.20.0 six==1.15.0 strict-rfc3339==0.7 urllib3==1.26.11 -minio==7.0.2 -PyPDF2==1.26.0 -reportlab==3.6.12 -html-sanitizer==1.9.3 -git+https://github.com/bcgov/business-schemas.git@2.18.10#egg=registry_schemas diff --git a/legal-api/requirements.txt.1 b/legal-api/requirements.txt.1 deleted file mode 100644 index 167cdacaa..000000000 --- a/legal-api/requirements.txt.1 +++ /dev/null @@ -1,52 +0,0 @@ -Babel==2.9.1 -Flask-Babel==2.0.0 -Flask-Migrate==3.1.0 -Flask-Moment==1.0.2 -Flask-SQLAlchemy==2.5.1 -Flask-Script==2.0.6 -Flask==2.0.1 -Jinja2==3.0.1 -Mako==1.1.5 -MarkupSafe==2.0.1 -SQLAlchemy-Continuum==1.3.11 -SQLAlchemy-Utils==0.37.8 -SQLAlchemy==1.4.23 -Werkzeug==2.0.1 -alembic==1.7.3 -aniso8601==9.0.1 -asyncio-nats-client==0.11.4 -asyncio-nats-streaming==0.4.0 -attrs==21.2.0 -blinker==1.4 -cachelib==0.3.0 -certifi==2021.5.30 -charset-normalizer==2.0.6 -click==8.0.1 -datedelta==1.3 -dpath==2.0.5 -ecdsa==0.17.0 -expiringdict==1.1.4 -flask-jwt-oidc==0.3.0 -flask-restx==0.5.1 -gunicorn==20.1.0 -idna==3.2 -itsdangerous==2.0.1 -jsonschema==3.2.0 -launchdarkly-server-sdk==7.2.0 -minio==7.1.0 -protobuf==3.18.0 -psycopg2-binary==2.9.1 -pyRFC3339==1.1 -pyasn1==0.4.8 -pycountry==20.7.3 -pyrsistent==0.18.0 -python-dotenv==0.19.0 -python-jose==3.3.0 -pytz==2021.1 -requests==2.26.0 -rsa==4.7.2 -semver==2.13.0 -sentry-sdk==1.4.0 -six==1.16.0 -strict-rfc3339==0.7 -urllib3==1.26.6 diff --git a/legal-api/src/legal_api/__init__.py b/legal-api/src/legal_api/__init__.py index e0d2fa647..858da23dd 100644 --- a/legal-api/src/legal_api/__init__.py +++ b/legal-api/src/legal_api/__init__.py @@ -32,6 +32,7 @@ from legal_api.utils.auth import jwt from legal_api.utils.logging import setup_logging from legal_api.utils.run_version import get_run_version +from legal_api.extensions import socketio # noqa: I003; the sentry import creates a bad line count in isort setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'logging.conf')) # important to do this first @@ -67,6 +68,8 @@ def create_app(run_mode=os.getenv('FLASK_ENV', 'production')): register_shellcontext(app) + socketio.init_app(app, cors_allowed_origins='*') + return app diff --git a/legal-api/src/legal_api/extensions.py b/legal-api/src/legal_api/extensions.py new file mode 100644 index 000000000..0c904db60 --- /dev/null +++ b/legal-api/src/legal_api/extensions.py @@ -0,0 +1,9 @@ +from flask import current_app +from flask_socketio import SocketIO + +socketio = SocketIO() + + +@socketio.on('connect') +def on_connect(): + current_app.logger.debug(f"Socket connected to client") diff --git a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py index 690c68778..1463b8333 100644 --- a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py +++ b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py @@ -13,15 +13,18 @@ # limitations under the License. """API endpoints for managing an Digital Credentials resource.""" +import json from datetime import datetime from http import HTTPStatus from flask import Blueprint, current_app, jsonify, request from flask_cors import cross_origin +from flask_socketio import emit from legal_api.models import Business, DCConnection, DCDefinition, DCIssuedCredential from legal_api.services import digital_credentials from legal_api.utils.auth import jwt +from legal_api.extensions import socketio from .bp import bp @@ -60,23 +63,26 @@ def create_invitation(identifier): ) connection.save() - return jsonify({'invitationUrl': connection.invitation_url}), HTTPStatus.OK + return jsonify(connection.json), HTTPStatus.OK -@bp.route('//digitalCredentials/connection', methods=['GET', 'OPTIONS'], strict_slashes=False) +@bp.route('//digitalCredentials/connections', methods=['GET', 'OPTIONS'], strict_slashes=False) @cross_origin(origin='*') @jwt.requires_auth -def get_active_connection(identifier): +def get_connections(identifier): """Get active connection for this business.""" business = Business.find_by_identifier(identifier) if not business: return jsonify({'message': f'{identifier} not found.'}), HTTPStatus.NOT_FOUND - connection = DCConnection.find_active_by(business_id=business.id) - if not connection: - return jsonify({'message': 'No active connection found.'}), HTTPStatus.NOT_FOUND + connections = DCConnection.find_by(business_id=business.id) + if len(connections) == 0: + return jsonify({'connections': []}), HTTPStatus.OK - return jsonify(connection.json), HTTPStatus.OK + response = [] + for connection in connections: + response.append(connection.json) + return jsonify({'connections': response}), HTTPStatus.OK @bp.route('//digitalCredentials', methods=['GET', 'OPTIONS'], strict_slashes=False) @@ -210,12 +216,14 @@ def webhook_notification(topic_name: str): connection.connection_state = json_input['state'] connection.is_active = True connection.save() + socketio.emit('connections', connection.json) elif topic_name == 'issue_credential_v2_0': issued_credential = DCIssuedCredential.find_by_credential_exchange_id(json_input['cred_ex_id']) if issued_credential and json_input['state'] in ('credential-issued', 'done'): issued_credential.date_of_issue = datetime.utcnow() issued_credential.is_issued = True issued_credential.save() + socketio.emit('issue_credential_v2_0', issued_credential.json) except Exception as err: current_app.logger.error(err) raise err diff --git a/legal-api/wsgi.py b/legal-api/wsgi.py index 5d42ecd85..93c38f48d 100755 --- a/legal-api/wsgi.py +++ b/legal-api/wsgi.py @@ -16,10 +16,11 @@ import os from legal_api import create_app +from legal_api.extensions import socketio # Openshift s2i expects a lower case name of application application = create_app() # pylint: disable=invalid-name if __name__ == "__main__": server_port = os.environ.get('PORT', '8080') - application.run(debug=False, port=server_port, host='0.0.0.0') + socketio.run(application, port=server_port, host='0.0.0.0') From acb6b500702f092ae04399a9f64e0e70473f0f46 Mon Sep 17 00:00:00 2001 From: Akiff Manji Date: Mon, 16 Oct 2023 18:42:34 +0000 Subject: [PATCH 09/13] feat: db migration script to enable revocation Signed-off-by: Akiff Manji --- ...0a5164_add_revocation_to_dc_credentials.py | 26 +++++++++++++++++++ .../legal_api/models/dc_issued_credential.py | 6 ++++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 legal-api/migrations/versions/6b65b40a5164_add_revocation_to_dc_credentials.py diff --git a/legal-api/migrations/versions/6b65b40a5164_add_revocation_to_dc_credentials.py b/legal-api/migrations/versions/6b65b40a5164_add_revocation_to_dc_credentials.py new file mode 100644 index 000000000..5392f271d --- /dev/null +++ b/legal-api/migrations/versions/6b65b40a5164_add_revocation_to_dc_credentials.py @@ -0,0 +1,26 @@ +"""add revocation to dc_credentials + +Revision ID: 6b65b40a5164 +Revises: 9a9ac165365e +Create Date: 2023-10-11 22:20:14.023687 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6b65b40a5164' +down_revision = '9a9ac165365e' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('dc_issued_credentials', sa.Column('credential_revocation_id', sa.String(length=10), nullable=True)) + op.add_column('dc_issued_credentials', sa.Column('revocation_registry_id', sa.String(length=200), nullable=True)) + + +def downgrade(): + op.drop_column('dc_issued_credentials', 'credential_revocation_id') + op.drop_column('dc_issued_credentials', 'revocation_registry_id') diff --git a/legal-api/src/legal_api/models/dc_issued_credential.py b/legal-api/src/legal_api/models/dc_issued_credential.py index ac56d1296..3189d617d 100644 --- a/legal-api/src/legal_api/models/dc_issued_credential.py +++ b/legal-api/src/legal_api/models/dc_issued_credential.py @@ -35,6 +35,8 @@ class DCIssuedCredential(db.Model): # pylint: disable=too-many-instance-attribu date_of_issue = db.Column('date_of_issue', db.DateTime(timezone=True)) is_revoked = db.Column('is_revoked', db.Boolean, default=False) + credential_revocation_id = db.Column('credential_revocation_id', db.String(10)) + revocation_registry_id = db.Column('revocation_registry_id', db.String(200)) @property def json(self): @@ -46,7 +48,9 @@ def json(self): 'credentialExchangeId': self.credential_exchange_id, 'isIssued': self.is_issued, 'dateOfIssue': self.date_of_issue.isoformat(), - 'isRevoked': self.is_revoked + 'isRevoked': self.is_revoked, + 'credentialRevocationId': self.credential_revocation_id, + 'revocationRegistryId': self.revocation_registry_id } return dc_issued_credential From 30ec0bca2ebae7bcea62e6916c6667ead23808e3 Mon Sep 17 00:00:00 2001 From: Akiff Manji Date: Wed, 18 Oct 2023 01:20:54 +0000 Subject: [PATCH 10/13] feat: revocation endpoint Signed-off-by: Akiff Manji --- .../8148a25d695e_change_field_type.py | 28 +++++ .../legal_api/models/dc_issued_credential.py | 14 ++- .../business/business_digital_credentials.py | 112 ++++++++++++------ .../legal_api/services/digital_credentials.py | 23 +++- 4 files changed, 136 insertions(+), 41 deletions(-) create mode 100644 legal-api/migrations/versions/8148a25d695e_change_field_type.py diff --git a/legal-api/migrations/versions/8148a25d695e_change_field_type.py b/legal-api/migrations/versions/8148a25d695e_change_field_type.py new file mode 100644 index 000000000..cd01a0a05 --- /dev/null +++ b/legal-api/migrations/versions/8148a25d695e_change_field_type.py @@ -0,0 +1,28 @@ +"""change field type + +Revision ID: 8148a25d695e +Revises: 6b65b40a5164 +Create Date: 2023-10-17 01:05:30.977475 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8148a25d695e' +down_revision = '6b65b40a5164' +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column('dc_issued_credentials', 'credential_id', + existing_type=sa.String(length=100), + type_=sa.String(length=10)) + + +def downgrade(): + op.alter_column('dc_issued_credentials', 'credential_id', + existing_type=sa.String(length=10), + type_=sa.String(length=100)) diff --git a/legal-api/src/legal_api/models/dc_issued_credential.py b/legal-api/src/legal_api/models/dc_issued_credential.py index 3189d617d..beac6a3ea 100644 --- a/legal-api/src/legal_api/models/dc_issued_credential.py +++ b/legal-api/src/legal_api/models/dc_issued_credential.py @@ -30,7 +30,7 @@ class DCIssuedCredential(db.Model): # pylint: disable=too-many-instance-attribu dc_connection_id = db.Column('dc_connection_id', db.Integer, db.ForeignKey('dc_connections.id')) credential_exchange_id = db.Column('credential_exchange_id', db.String(100)) - credential_id = db.Column('credential_id', db.String(100)) # not in use + credential_id = db.Column('credential_id', db.String(10)) is_issued = db.Column('is_issued', db.Boolean, default=False) date_of_issue = db.Column('date_of_issue', db.DateTime(timezone=True)) @@ -46,6 +46,7 @@ def json(self): 'dcDefinitionId': self.dc_definition_id, 'dcConnectionId': self.dc_connection_id, 'credentialExchangeId': self.credential_exchange_id, + "credentialId": self.credential_id, 'isIssued': self.is_issued, 'dateOfIssue': self.date_of_issue.isoformat(), 'isRevoked': self.is_revoked, @@ -69,13 +70,22 @@ def find_by_id(cls, dc_issued_credential_id: str) -> DCIssuedCredential: @classmethod def find_by_credential_exchange_id(cls, credential_exchange_id: str) -> DCIssuedCredential: - """Return the issued credential matching the id.""" + """Return the issued credential matching the credential exchange id.""" dc_issued_credential = None if credential_exchange_id: dc_issued_credential = cls.query. \ filter(DCIssuedCredential.credential_exchange_id == credential_exchange_id).one_or_none() return dc_issued_credential + @classmethod + def find_by_credential_id(cls, credential_id: str) -> DCIssuedCredential: + """Return the issued credential matching the credential id.""" + dc_issued_credential = None + if credential_id: + dc_issued_credential = cls.query. \ + filter(DCIssuedCredential.credential_id == credential_id).one_or_none() + return dc_issued_credential + @classmethod def find_by(cls, dc_definition_id: int = None, diff --git a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py index 1463b8333..2692c25d9 100644 --- a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py +++ b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py @@ -45,7 +45,6 @@ def create_invitation(identifier): if active_connection: return jsonify({'message': f'{identifier} already have an active connection.'}), HTTPStatus.UNPROCESSABLE_ENTITY - # check whether this business has an existing connection which is not active connections = DCConnection.find_by(business_id=business.id, connection_state='invitation') if connections: connection = connections[0] @@ -108,6 +107,7 @@ def get_issued_credentials(identifier): response.append({ 'legalName': business.legal_name, 'credentialType': definition.credential_type.name, + 'credentialId': issued_credential.credential_id, 'isIssued': issued_credential.is_issued, 'dateOfIssue': issued_credential.date_of_issue.isoformat() if issued_credential.date_of_issue else '', 'isRevoked': issued_credential.is_revoked @@ -143,11 +143,82 @@ def send_credential(identifier, credential_type): issued_credential = DCIssuedCredential( dc_definition_id=definition.id, dc_connection_id=connection.id, - credential_exchange_id=response['cred_ex_id'] + credential_exchange_id=response['cred_ex_id'], + # TODO: Add a real ID + credential_id='123456' ) issued_credential.save() - return jsonify({'message': 'Issue Credential is initiated.'}), HTTPStatus.OK + return jsonify({'message': 'Credential offer has been sent.'}), HTTPStatus.OK + + +@bp.route('//digitalCredentials//revoke', methods=['POST'], strict_slashes=False) +@cross_origin(origin='*') +@jwt.requires_auth +def revoke_credential(identifier, credential_id): + """Revoke a credential.""" + business = Business.find_by_identifier(identifier) + if not business: + return jsonify({'message': f'{identifier} not found.'}), HTTPStatus.NOT_FOUND + + connection = DCConnection.find_active_by(business_id=business.id) + if not connection: + return jsonify({'message': f'{identifier} active connection not found.'}), HTTPStatus.NOT_FOUND + + # TODO: Use a real ID + issued_credential = DCIssuedCredential.find_by_credential_id(credential_id='123456') + if not issued_credential or issued_credential.is_revoked: + return jsonify({'message': f'{identifier} issued credential not found.'}), HTTPStatus.NOT_FOUND + + revoked = digital_credentials.revoke_credential(connection.connection_id, + issued_credential.credential_revocation_id, + issued_credential.revocation_registry_id) + if revoked == None: + return jsonify({'message': 'Failed to revoke credential.'}), HTTPStatus.INTERNAL_SERVER_ERROR + + issued_credential.is_revoked = True + issued_credential.save() + return jsonify({'message': 'Credential has been revoked.'}), HTTPStatus.OK + + +@bp_dc.route('/topic/', methods=['POST'], strict_slashes=False) +@cross_origin(origin='*') +def webhook_notification(topic_name: str): + """To receive notification from aca-py admin api.""" + json_input = request.get_json() + try: + if topic_name == 'connections': + if 'invitation' in json_input and json_input['invitation'] != None: + connection = DCConnection.find_by_connection_id(json_input['invitation']['@id']) + else: + connection = DCConnection.find_by_connection_id(json_input['invitation_msg_id']) + # Using https://didcomm.org/connections/1.0 protocol the final state is 'active' + # Using https://didcomm.org/didexchange/1.0 protocol the final state is 'completed' + if connection and not connection.is_active and json_input['state'] in ('active', 'completed'): + connection.connection_id = json_input['connection_id'] + connection.connection_state = json_input['state'] + connection.is_active = True + connection.save() + socketio.emit('connections', connection.json) + elif topic_name == 'issuer_cred_rev': + issued_credential = DCIssuedCredential.find_by_credential_exchange_id(json_input['cred_ex_id']) + if issued_credential and json_input['state'] == 'issued': + issued_credential.credential_revocation_id=json_input['cred_rev_id'], + issued_credential.revocation_registry_id=json_input['rev_reg_id'] + issued_credential.save() + elif topic_name == 'issue_credential_v2_0': + # TODO: We want to deactivate the connection once the credential is issued + issued_credential = DCIssuedCredential.find_by_credential_exchange_id(json_input['cred_ex_id']) + if issued_credential and json_input['state'] == 'done': + issued_credential.date_of_issue = datetime.utcnow() + issued_credential.is_issued = True + issued_credential.save() + socketio.emit('issue_credential_v2_0', issued_credential.json) + except Exception as err: + current_app.logger.error(err) + raise err + + return jsonify({'message': 'Webhook received.'}), HTTPStatus.OK def _get_data_for_credential(credential_type: DCDefinition.CredentialType, business: Business): @@ -195,37 +266,4 @@ def _get_data_for_credential(credential_type: DCDefinition.CredentialType, busin } ] - return None - - -@bp_dc.route('/topic/', methods=['POST'], strict_slashes=False) -@cross_origin(origin='*') -def webhook_notification(topic_name: str): - """To receive notification from aca-py admin api.""" - json_input = request.get_json() - try: - if topic_name == 'connections': - if 'invitation' in json_input and json_input['invitation'] != None: - connection = DCConnection.find_by_connection_id(json_input['invitation']['@id']) - else: - connection = DCConnection.find_by_connection_id(json_input['invitation_msg_id']) - # Using https://didcomm.org/connections/1.0 protocol the final state is 'active' - # Using https://didcomm.org/didexchange/1.0 protocol the final state is 'completed' - if connection and not connection.is_active and json_input['state'] in ('active', 'completed'): - connection.connection_id = json_input['connection_id'] - connection.connection_state = json_input['state'] - connection.is_active = True - connection.save() - socketio.emit('connections', connection.json) - elif topic_name == 'issue_credential_v2_0': - issued_credential = DCIssuedCredential.find_by_credential_exchange_id(json_input['cred_ex_id']) - if issued_credential and json_input['state'] in ('credential-issued', 'done'): - issued_credential.date_of_issue = datetime.utcnow() - issued_credential.is_issued = True - issued_credential.save() - socketio.emit('issue_credential_v2_0', issued_credential.json) - except Exception as err: - current_app.logger.error(err) - raise err - - return jsonify({'message': 'Webhook received.'}), HTTPStatus.OK + return None \ No newline at end of file diff --git a/legal-api/src/legal_api/services/digital_credentials.py b/legal-api/src/legal_api/services/digital_credentials.py index 06b50da73..41d2df5c4 100644 --- a/legal-api/src/legal_api/services/digital_credentials.py +++ b/legal-api/src/legal_api/services/digital_credentials.py @@ -41,7 +41,7 @@ class DigitalCredentialsService: 'given_names', ], 'schema_name': 'digital_business_card', # do not change schema name. this is the name registered in aca-py agent - 'schema_version': '1.0.0' # if attributes changes update schema_version to re-register + 'schema_version': '1.0.0' # if attributes change update schema_version to re-register } def __init__(self): @@ -193,7 +193,7 @@ def issue_credential(self, 'schema_id': definition.schema_id, 'schema_issuer_did': self.public_schema_did, 'schema_name': definition.schema_name, - 'schema_version': definition.schema_version, + 'schema_version': definition.schema_version } }, 'trace': True @@ -204,6 +204,25 @@ def issue_credential(self, self.app.logger.error(err) return None + def revoke_credential(self, connection_id, cred_rev_id: str, rev_reg_id: str) -> Optional[dict]: + """Revoke a credential.""" + try: + response = requests.post(self.api_url + '/revocation/revoke', + headers=self._get_headers(), + data=json.dumps({ + 'connection_id': connection_id, + 'cred_rev_id': cred_rev_id, + 'rev_reg_id': rev_reg_id, + 'publish': True, + "notify": True, + "notify_version": "v1_0" + })) + response.raise_for_status() + return response.json() + except Exception as err: + self.app.logger.error(err) + return None + def _get_headers(self) -> dict: return { 'Content-Type': 'application/json', From 13a9938cd1110245fa2bc5c3f291d31b3ef48f5e Mon Sep 17 00:00:00 2001 From: Akiff Manji Date: Thu, 19 Oct 2023 02:35:20 +0000 Subject: [PATCH 11/13] feat: replace endpoints Signed-off-by: Akiff Manji --- .../src/legal_api/models/dc_connection.py | 5 +++ .../legal_api/models/dc_issued_credential.py | 5 +++ .../business/business_digital_credentials.py | 36 ++++++++++++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/legal-api/src/legal_api/models/dc_connection.py b/legal-api/src/legal_api/models/dc_connection.py index 06bf725f7..f1e7320c5 100644 --- a/legal-api/src/legal_api/models/dc_connection.py +++ b/legal-api/src/legal_api/models/dc_connection.py @@ -53,6 +53,11 @@ def save(self): db.session.add(self) db.session.commit() + def delete(self): + """Delete the object from the database immediately.""" + db.session.delete(self) + db.session.commit() + @classmethod def find_by_id(cls, dc_connection_id: str) -> DCConnection: """Return the digital credential connection matching the id.""" diff --git a/legal-api/src/legal_api/models/dc_issued_credential.py b/legal-api/src/legal_api/models/dc_issued_credential.py index beac6a3ea..0dbe593e4 100644 --- a/legal-api/src/legal_api/models/dc_issued_credential.py +++ b/legal-api/src/legal_api/models/dc_issued_credential.py @@ -60,6 +60,11 @@ def save(self): db.session.add(self) db.session.commit() + def delete(self): + """Delete the object from the database immediately.""" + db.session.delete(self) + db.session.commit() + @classmethod def find_by_id(cls, dc_issued_credential_id: str) -> DCIssuedCredential: """Return the issued credential matching the id.""" diff --git a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py index 2692c25d9..2fd416a68 100644 --- a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py +++ b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py @@ -83,6 +83,22 @@ def get_connections(identifier): response.append(connection.json) return jsonify({'connections': response}), HTTPStatus.OK +@bp.route('//digitalCredentials/connection', methods=['DELETE'], strict_slashes=False) +@cross_origin(origin='*') +@jwt.requires_auth +def delete_connection(identifier): + """Delete an active connection for this business.""" + business = Business.find_by_identifier(identifier) + if not business: + return jsonify({'message': f'{identifier} not found.'}), HTTPStatus.NOT_FOUND + + connection = DCConnection.find_active_by(business_id=business.id) + if not connection: + return jsonify({'message': f'{identifier} active connection not found.'}), HTTPStatus.NOT_FOUND + + connection.delete() + return jsonify({'message': 'Connection has been deleted.'}), HTTPStatus.OK + @bp.route('//digitalCredentials', methods=['GET', 'OPTIONS'], strict_slashes=False) @cross_origin(origin='*') @@ -152,7 +168,7 @@ def send_credential(identifier, credential_type): return jsonify({'message': 'Credential offer has been sent.'}), HTTPStatus.OK -@bp.route('//digitalCredentials//revoke', methods=['POST'], strict_slashes=False) +@bp.route('//digitalCredentials//revoke', methods=['POST'], strict_slashes=False) @cross_origin(origin='*') @jwt.requires_auth def revoke_credential(identifier, credential_id): @@ -181,6 +197,24 @@ def revoke_credential(identifier, credential_id): return jsonify({'message': 'Credential has been revoked.'}), HTTPStatus.OK +@bp.route('//digitalCredentials/', methods=['DELETE'], strict_slashes=False) +@cross_origin(origin='*') +@jwt.requires_auth +def delete_credential(identifier, credential_id): + """Delete a credential.""" + business = Business.find_by_identifier(identifier) + if not business: + return jsonify({'message': f'{identifier} not found.'}), HTTPStatus.NOT_FOUND + + # TODO: Use a real ID + issued_credential = DCIssuedCredential.find_by_credential_id(credential_id='123456') + if not issued_credential: + return jsonify({'message': f'{identifier} issued credential not found.'}), HTTPStatus.NOT_FOUND + + issued_credential.delete() + return jsonify({'message': 'Credential has been deleted.'}), HTTPStatus.OK + + @bp_dc.route('/topic/', methods=['POST'], strict_slashes=False) @cross_origin(origin='*') def webhook_notification(topic_name: str): From fed055037e1bb4e5a6f719abcccc66db3d1825a3 Mon Sep 17 00:00:00 2001 From: Akiff Manji Date: Thu, 19 Oct 2023 15:06:17 +0000 Subject: [PATCH 12/13] chore: fix linting errors Signed-off-by: Akiff Manji --- legal-api/src/legal_api/__init__.py | 2 +- legal-api/src/legal_api/extensions.py | 18 +++++++- .../src/legal_api/models/dc_definition.py | 2 +- .../legal_api/models/dc_issued_credential.py | 2 +- .../business/business_digital_credentials.py | 22 ++++----- .../legal_api/services/digital_credentials.py | 46 +++++++++++-------- 6 files changed, 58 insertions(+), 34 deletions(-) diff --git a/legal-api/src/legal_api/__init__.py b/legal-api/src/legal_api/__init__.py index 858da23dd..7956f43a9 100644 --- a/legal-api/src/legal_api/__init__.py +++ b/legal-api/src/legal_api/__init__.py @@ -24,6 +24,7 @@ from registry_schemas.flask import SchemaServices # noqa: I001 from legal_api import config, models +from legal_api.extensions import socketio from legal_api.models import db from legal_api.resources import endpoints from legal_api.schemas import rsbc_schemas @@ -32,7 +33,6 @@ from legal_api.utils.auth import jwt from legal_api.utils.logging import setup_logging from legal_api.utils.run_version import get_run_version -from legal_api.extensions import socketio # noqa: I003; the sentry import creates a bad line count in isort setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'logging.conf')) # important to do this first diff --git a/legal-api/src/legal_api/extensions.py b/legal-api/src/legal_api/extensions.py index 0c904db60..d7cc8f257 100644 --- a/legal-api/src/legal_api/extensions.py +++ b/legal-api/src/legal_api/extensions.py @@ -1,9 +1,25 @@ +# Copyright © 2022 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Extensions module.""" from flask import current_app from flask_socketio import SocketIO + socketio = SocketIO() @socketio.on('connect') def on_connect(): - current_app.logger.debug(f"Socket connected to client") + """Handle socket connection.""" + current_app.logger.debug('Socket connected to client') diff --git a/legal-api/src/legal_api/models/dc_definition.py b/legal-api/src/legal_api/models/dc_definition.py index 11158b90d..4e05061a7 100644 --- a/legal-api/src/legal_api/models/dc_definition.py +++ b/legal-api/src/legal_api/models/dc_definition.py @@ -88,7 +88,7 @@ def find_by(cls, ) -> DCDefinition: """Return the digital credential definition matching the filter.""" query = db.session.query(DCDefinition). \ - filter(DCDefinition.is_deleted == False). \ + filter(DCDefinition.is_deleted is False). \ filter(DCDefinition.credential_type == credential_type). \ filter(DCDefinition.schema_id == schema_id). \ filter(DCDefinition.credential_definition_id == credential_definition_id) diff --git a/legal-api/src/legal_api/models/dc_issued_credential.py b/legal-api/src/legal_api/models/dc_issued_credential.py index 0dbe593e4..70d71dedc 100644 --- a/legal-api/src/legal_api/models/dc_issued_credential.py +++ b/legal-api/src/legal_api/models/dc_issued_credential.py @@ -46,7 +46,7 @@ def json(self): 'dcDefinitionId': self.dc_definition_id, 'dcConnectionId': self.dc_connection_id, 'credentialExchangeId': self.credential_exchange_id, - "credentialId": self.credential_id, + 'credentialId': self.credential_id, 'isIssued': self.is_issued, 'dateOfIssue': self.date_of_issue.isoformat(), 'isRevoked': self.is_revoked, diff --git a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py index 2fd416a68..91fd7913c 100644 --- a/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py +++ b/legal-api/src/legal_api/resources/v2/business/business_digital_credentials.py @@ -13,18 +13,16 @@ # limitations under the License. """API endpoints for managing an Digital Credentials resource.""" -import json from datetime import datetime from http import HTTPStatus from flask import Blueprint, current_app, jsonify, request from flask_cors import cross_origin -from flask_socketio import emit +from legal_api.extensions import socketio from legal_api.models import Business, DCConnection, DCDefinition, DCIssuedCredential from legal_api.services import digital_credentials from legal_api.utils.auth import jwt -from legal_api.extensions import socketio from .bp import bp @@ -83,6 +81,7 @@ def get_connections(identifier): response.append(connection.json) return jsonify({'connections': response}), HTTPStatus.OK + @bp.route('//digitalCredentials/connection', methods=['DELETE'], strict_slashes=False) @cross_origin(origin='*') @jwt.requires_auth @@ -168,7 +167,8 @@ def send_credential(identifier, credential_type): return jsonify({'message': 'Credential offer has been sent.'}), HTTPStatus.OK -@bp.route('//digitalCredentials//revoke', methods=['POST'], strict_slashes=False) +@bp.route('//digitalCredentials//revoke', + methods=['POST'], strict_slashes=False) @cross_origin(origin='*') @jwt.requires_auth def revoke_credential(identifier, credential_id): @@ -185,13 +185,13 @@ def revoke_credential(identifier, credential_id): issued_credential = DCIssuedCredential.find_by_credential_id(credential_id='123456') if not issued_credential or issued_credential.is_revoked: return jsonify({'message': f'{identifier} issued credential not found.'}), HTTPStatus.NOT_FOUND - + revoked = digital_credentials.revoke_credential(connection.connection_id, issued_credential.credential_revocation_id, issued_credential.revocation_registry_id) - if revoked == None: + if revoked is None: return jsonify({'message': 'Failed to revoke credential.'}), HTTPStatus.INTERNAL_SERVER_ERROR - + issued_credential.is_revoked = True issued_credential.save() return jsonify({'message': 'Credential has been revoked.'}), HTTPStatus.OK @@ -222,7 +222,7 @@ def webhook_notification(topic_name: str): json_input = request.get_json() try: if topic_name == 'connections': - if 'invitation' in json_input and json_input['invitation'] != None: + if 'invitation' in json_input and json_input['invitation'] is not None: connection = DCConnection.find_by_connection_id(json_input['invitation']['@id']) else: connection = DCConnection.find_by_connection_id(json_input['invitation_msg_id']) @@ -237,8 +237,8 @@ def webhook_notification(topic_name: str): elif topic_name == 'issuer_cred_rev': issued_credential = DCIssuedCredential.find_by_credential_exchange_id(json_input['cred_ex_id']) if issued_credential and json_input['state'] == 'issued': - issued_credential.credential_revocation_id=json_input['cred_rev_id'], - issued_credential.revocation_registry_id=json_input['rev_reg_id'] + issued_credential.credential_revocation_id = json_input['cred_rev_id'] + issued_credential.revocation_registry_id = json_input['rev_reg_id'] issued_credential.save() elif topic_name == 'issue_credential_v2_0': # TODO: We want to deactivate the connection once the credential is issued @@ -300,4 +300,4 @@ def _get_data_for_credential(credential_type: DCDefinition.CredentialType, busin } ] - return None \ No newline at end of file + return None diff --git a/legal-api/src/legal_api/services/digital_credentials.py b/legal-api/src/legal_api/services/digital_credentials.py index 41d2df5c4..f22985d91 100644 --- a/legal-api/src/legal_api/services/digital_credentials.py +++ b/legal-api/src/legal_api/services/digital_credentials.py @@ -40,8 +40,10 @@ class DigitalCredentialsService: 'business_type', 'given_names', ], - 'schema_name': 'digital_business_card', # do not change schema name. this is the name registered in aca-py agent - 'schema_version': '1.0.0' # if attributes change update schema_version to re-register + # do not change schema name. this is the name registered in aca-py agent + 'schema_name': 'digital_business_card', + # if attributes change update schema_version to re-register + 'schema_version': '1.0.0' } def __init__(self): @@ -76,16 +78,16 @@ def init_app(self, app): self._register_business_definition() def _register_business_definition(self): + """Fetch schema and credential definition and save a Business definition.""" try: - if (self.business_schema_id is None): - self.app.logger.error(f'Environment variable: BUSINESS_SCHEMA_ID must be configured') - raise Exception(f'Environment variable: BUSINESS_SCHEMA_ID must be configured') + if self.business_schema_id is None: + self.app.logger.error('Environment variable: BUSINESS_SCHEMA_ID must be configured') + raise ValueError('Environment variable: BUSINESS_SCHEMA_ID must be configured') - if (self.business_cred_def_id is None): - self.app.logger.error(f'Environment variable: BUSINESS_CRED_DEF_ID must be configured') - raise Exception(f'Environment variable: BUSINESS_CRED_DEF_ID must be configured') + if self.business_cred_def_id is None: + self.app.logger.error('Environment variable: BUSINESS_CRED_DEF_ID must be configured') + raise ValueError('Environment variable: BUSINESS_CRED_DEF_ID must be configured') - """Fetch schema and credential definition and save a Business definition.""" # Check for the current Business definition. definition = DCDefinition.find_by( credential_type=DCDefinition.CredentialType.business, @@ -98,19 +100,23 @@ def _register_business_definition(self): DCDefinition.deactivate(DCDefinition.CredentialType.business) ### - # The following just a sanity check to make sure the schema and credential definition are stored in Traction tenant - # These calls also include a ledger lookup to see if the schema and credential definition are published + # The following just a sanity check to make sure the schema and + # credential definition are stored in Traction tenant. + # These calls also include a ledger lookup to see if the schema + # and credential definition are published. ### # Look for a schema first, and copy it into the Traction tenant if it's not there schema_id = self._fetch_schema(self.business_schema_id) if not schema_id: - raise Exception(f'Schema with id:{self.business_schema_id} must be available in Traction tenant storage') + raise ValueError(f'Schema with id:{self.business_schema_id}' + + ' must be available in Traction tenant storage') # Look for a published credential definition first, and copy it into the Traction tenant if it's not there credential_definition_id = self._fetch_credential_definition(self.business_cred_def_id) if not credential_definition_id: - raise Exception(f'Credential Definition with id:{self.business_cred_def_id} must be avaible in Traction tenant storage') + raise ValueError(f'Credential Definition with id:{self.business_cred_def_id}' + + ' must be avaible in Traction tenant storage') # Create a new definition and add the new schema_id definition = DCDefinition( @@ -122,12 +128,13 @@ def _register_business_definition(self): ) # Lastly, save the definition definition.save() + return None except Exception as err: self.app.logger.error(err) return None def _fetch_schema(self, schema_id: str) -> Optional[str]: - """Find a schema in Traction storage""" + """Find a schema in Traction storage.""" try: response = requests.get(self.api_url + '/schema-storage', params={'schema_id': schema_id}, @@ -136,12 +143,12 @@ def _fetch_schema(self, schema_id: str) -> Optional[str]: first_or_default = next((x for x in response.json()['results'] if x['schema_id'] == schema_id), None) return first_or_default['schema_id'] if first_or_default else None except Exception as err: - self.app.logger.error(f"Failed to fetch schema with id:{schema_id} from Traction tenant storage") + self.app.logger.error(f'Failed to fetch schema with id:{schema_id} from Traction tenant storage') self.app.logger.error(err) raise err def _fetch_credential_definition(self, cred_def_id: str) -> Optional[str]: - """Find a published credential definition""" + """Find a published credential definition.""" try: response = requests.get(self.api_url + '/credential-definition-storage', params={'cred_def_id': cred_def_id}, @@ -150,7 +157,8 @@ def _fetch_credential_definition(self, cred_def_id: str) -> Optional[str]: first_or_default = next((x for x in response.json()['results'] if x['cred_def_id'] == cred_def_id), None) return first_or_default['cred_def_id'] if first_or_default else None except Exception as err: - self.app.logger.error(f'Failed to find credential definition with id:{cred_def_id} from Traction tenant storage') + self.app.logger.error(f'Failed to find credential definition with id:{cred_def_id}' + + ' from Traction tenant storage') self.app.logger.error(err) raise err @@ -214,8 +222,8 @@ def revoke_credential(self, connection_id, cred_rev_id: str, rev_reg_id: str) -> 'cred_rev_id': cred_rev_id, 'rev_reg_id': rev_reg_id, 'publish': True, - "notify": True, - "notify_version": "v1_0" + 'notify': True, + 'notify_version': 'v1_0' })) response.raise_for_status() return response.json() From fb7968b47630add9cffdf768c0c75581f580bcec Mon Sep 17 00:00:00 2001 From: Akiff Manji Date: Thu, 19 Oct 2023 15:46:07 +0000 Subject: [PATCH 13/13] chore: update requirements Signed-off-by: Akiff Manji --- legal-api/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legal-api/requirements.txt b/legal-api/requirements.txt index f582b911e..23ce7d78d 100755 --- a/legal-api/requirements.txt +++ b/legal-api/requirements.txt @@ -32,7 +32,7 @@ ecdsa==0.14.1 expiringdict==1.1.4 flask-jwt-oidc==0.3.0 flask-restx==0.3.0 -git+https://github.com/bcgov/business-schemas.git@2.18.10#egg=registry_schemas +git+https://github.com/bcgov/business-schemas.git@2.18.13#egg=registry_schemas gunicorn==20.1.0 html-sanitizer==1.9.3 idna==2.10