Skip to content

Commit

Permalink
18284: digital credentials (#2260)
Browse files Browse the repository at this point in the history
* feat: devcontainer configuraton for vscode

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* feat: hard code digital business card schema

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* feat: hard code digital business card schema

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* feat: issue credentials through Traction tenant

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* refactor: app initialization workflow

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* feat: use out-of-band invitation for connecting

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* feat: use v2.0 for issuing credential

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* feat: web socket implmentation with flask-socketio

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* feat: db migration script to enable revocation

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* feat: revocation endpoint

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* feat: replace endpoints

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* chore: fix linting errors

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* chore: update requirements

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* chore: update tests

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* feat: traction token exchanger

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* chore: update workflow variables

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* chore: update workflow variables

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* refactor: ws cors setting is a config option

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* chore: fix linting errors

Signed-off-by: Akiff Manji <amanji@petridish.dev>

* refactor: clean up init in digital credential service

Signed-off-by: Akiff Manji <amanji@petridish.dev>

---------

Signed-off-by: Akiff Manji <amanji@petridish.dev>
  • Loading branch information
amanji authored Oct 27, 2023
1 parent 01467b0 commit 6beb1b1
Show file tree
Hide file tree
Showing 22 changed files with 605 additions and 234 deletions.
15 changes: 15 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 <your-package-list-here>



31 changes: 31 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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",

// Enable this on OSX to add ssh key to agent inside container
"initializeCommand": "find ~/.ssh/ -type f -exec grep -l 'PRIVATE' {} \\; | xargs ssh-add"
}
35 changes: 35 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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:
4 changes: 4 additions & 0 deletions .github/workflows/legal-api-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ jobs:
NATS_QUEUE: entity-filer-worker
JWT_OIDC_JWKS_CACHE_TIMEOUT: 300
GO_LIVE_DATE: 2019-08-12
BUSINESS_SCHEMA_ID: test_business_schema_id
BUSINESS_CRED_DEF_ID: test_credential_definition_id
BUSINESS_SCHEMA_NAME: digital_business_card
BUSINESS_SCHEMA_VERSION: "1.0.0"

runs-on: ubuntu-20.04

Expand Down
5 changes: 4 additions & 1 deletion legal-api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
Expand Down
Original file line number Diff line number Diff line change
@@ -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')
28 changes: 28 additions & 0 deletions legal-api/migrations/versions/8148a25d695e_change_field_type.py
Original file line number Diff line number Diff line change
@@ -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))
15 changes: 8 additions & 7 deletions legal-api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,33 +32,33 @@ 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.13#egg=registry_schemas
gunicorn==20.1.0
html-sanitizer==1.9.3
idna==2.10
itsdangerous==1.1.0
jsonschema==4.19.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
pyasn1==0.4.8
pycountry==20.7.3
pydantic==1.10.2
pyjwt==2.8.0
pyrsistent==0.17.3
python-dateutil==2.8.1
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
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.13#egg=registry_schemas

52 changes: 0 additions & 52 deletions legal-api/requirements.txt.1

This file was deleted.

6 changes: 6 additions & 0 deletions legal-api/src/legal_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -67,6 +68,11 @@ def create_app(run_mode=os.getenv('FLASK_ENV', 'production')):

register_shellcontext(app)

ws_allowed_origins = app.config.get('WS_ALLOWED_ORIGINS', [])
if isinstance(ws_allowed_origins, str) and ws_allowed_origins != '*':
ws_allowed_origins = ws_allowed_origins.split(',')
socketio.init_app(app, cors_allowed_origins=ws_allowed_origins)

return app


Expand Down
18 changes: 15 additions & 3 deletions legal-api/src/legal_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,21 @@ 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_ENTITY_DID = os.getenv('ACA_PY_ENTITY_DID')
# Traction ACA-Py tenant settings to issue credentials from
TRACTION_API_URL = os.getenv('TRACTION_API_URL')
TRACTION_TENANT_ID = os.getenv('TRACTION_TENANT_ID')
TRACTION_API_KEY = os.getenv('TRACTION_API_KEY')
TRACTION_PUBLIC_SCHEMA_DID = os.getenv('TRACTION_PUBLIC_SCHEMA_DID')
TRACTION_PUBLIC_ISSUER_DID = os.getenv('TRACTION_PUBLIC_ISSUER_DID')

# Web socket settings
WS_ALLOWED_ORIGINS = os.getenv('WS_ALLOWED_ORIGINS')

# Digital Business Card configuration values (required to issue credentials)
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
Expand Down
56 changes: 56 additions & 0 deletions legal-api/src/legal_api/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright © 2023 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.
"""This module holds function decorators."""

import json
from functools import wraps

import jwt
import requests
from flask import current_app
from jwt import ExpiredSignatureError


def requires_traction_auth(f):
"""Check for a valid Traction token and refresh if needed."""
@wraps(f)
def decorated_function(*args, **kwargs):
traction_api_url = current_app.config['TRACTION_API_URL']
traction_tenant_id = current_app.config['TRACTION_TENANT_ID']
traction_api_key = current_app.config['TRACTION_API_KEY']

if traction_api_url is None:
raise EnvironmentError('TRACTION_API_URL environment vairable is not set')

if traction_tenant_id is None:
raise EnvironmentError('TRACTION_TENANT_ID environment vairable is not set')

if traction_api_key is None:
raise EnvironmentError('TRACTION_API_KEY environment vairable is not set')

try:
if not hasattr(current_app, 'api_token'):
raise jwt.ExpiredSignatureError

jwt.decode(current_app.api_token, options={'verify_signature': False})
except ExpiredSignatureError:
current_app.logger.info('JWT token expired or is missing, requesting new token')
response = requests.post(f'{traction_api_url}/multitenancy/tenant/{traction_tenant_id}/token',
headers={'Content-Type': 'application/json'},
data=json.dumps({'api_key': traction_api_key}))
response.raise_for_status()
current_app.api_token = response.json()['token']

return f(*args, **kwargs)
return decorated_function
Loading

0 comments on commit 6beb1b1

Please sign in to comment.