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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 30 additions & 85 deletions python/common/business-registry-digital-credentials/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
[tool.poetry]
name = "business-registry-digital-credentials"
version = "0.1.1"
version = "0.2.0"
description = ""
authors = ["Lucas O'Neil <lucasoneil@gmail.com>"]
readme = "README.md"
packages = [{ include = "business_registry_digital_credentials", from = "src" }]

[tool.poetry.dependencies]
python = ">=3.9,<4.0"
python = ">=3.13,<3.14"
datedelta = "^1.4"
flask-jwt-oidc = "^0.8.0"
pytz = "^2025.1"
pyjwt = "^2.8.0"
requests = "^2.32.3"
business-model = { git = "https://github.com/bcgov/lear.git", subdirectory = "python/common/business-registry-model", branch = "main" }
business-registry-common = { git = "https://github.com/bcgov/lear.git", subdirectory = "python/common/business-registry-common", branch = "main" }

[tool.poetry.group.dev.dependencies]
pytest = "^8.3.5"
Expand Down Expand Up @@ -60,7 +61,7 @@ docstring-min-length = 10
count = true

[tool.black]
target-version = ["py39", "py310", "py311", "py312"]
target-version = ["py313"]
line-length = 120

[tool.isort]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,34 @@
# 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.
"""Digital Business Card (DBC) shared package.
Centralizes the Traction REST client, access-rules engine, credential helpers,
and credential-lifecycle DB wrappers used by both legal-api and the
business-digital-credentials queue service.
"""

# Import the service class and create the module-level singleton FIRST.
# Modules imported below (e.g. ``digital_credentials_lifecycle``) reference this
# attribute at top-level, so it must exist before they are imported.
from .digital_credentials import DigitalCredentialsService

"""This module holds general utility functions and helpers for the main package."""
digital_credentials = DigitalCredentialsService()

from .digital_credentials_auth import ( # noqa: E402
are_digital_credentials_allowed,
get_digital_credentials_preconditions,
)
from .digital_credentials_helpers import ( # noqa: E402
extract_invitation_message_id,
get_digital_credential_data,
get_or_create_business_user,
get_roles,
)
from .digital_credentials_lifecycle import ( # noqa: E402
get_all_digital_credentials_for_business,
issue_digital_credential,
replace_digital_credential,
revoke_digital_credential,
)
from .digital_credentials_rules import DigitalCredentialsRulesService # noqa: E402
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

from dotenv import find_dotenv, load_dotenv


# this will load all the envars from a .env file located in the project root (api)
load_dotenv(find_dotenv())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,18 @@

import json
import time
from datetime import datetime
from datetime import UTC, datetime
Comment thread
loneil marked this conversation as resolved.
from functools import wraps
from http import HTTPStatus

import jwt as pyjwt
import requests
from flask import current_app, jsonify
from flask_jwt_oidc import JwtManager
from flask import current_app
from jwt import ExpiredSignatureError

from business_model.models import Business
from .digital_credentials_auth import are_digital_credentials_allowed

# from legal_api.utils.auth import jwt


MAX_RETRIES = 5 # Number of times to retry getting the token
TOKEN_RETRY_WAIT = 2 # Delay in seconds between retries

jwt = JwtManager()


def _get_traction_token():
"""Get a traction token and check if it is valid."""
Expand Down Expand Up @@ -65,7 +56,7 @@ def _get_traction_token():
# Use the token to check if it is valid by calling the tenant endpoint
check_response = requests.get(f"{traction_api_url}/tenant", headers={"Authorization": f"Bearer {token}"})

if check_response.status_code == 401:
if check_response.status_code == HTTPStatus.UNAUTHORIZED:
current_app.logger.warning(f"Attempt {attempt + 1}: Received 401 checking token. Retry.")
time.sleep(TOKEN_RETRY_WAIT)
continue
Expand Down Expand Up @@ -97,7 +88,7 @@ def decorated_function(*args, **kwargs):
if not (decoded := pyjwt.decode(current_app.api_token, options={"verify_signature": False})):
raise pyjwt.ExpiredSignatureError

if datetime.utcfromtimestamp(decoded["exp"]) <= datetime.utcnow():
if datetime.fromtimestamp(decoded["exp"], UTC) <= datetime.now(UTC):
raise pyjwt.ExpiredSignatureError
except ExpiredSignatureError:
current_app.logger.info("Traction token expired or is missing, requesting new token")
Expand All @@ -106,21 +97,3 @@ def decorated_function(*args, **kwargs):
return f(*args, **kwargs)

return decorated_function


def can_access_digital_credentials(f):
"""Ensure the business can has access to digital credentials."""

@wraps(f)
def decorated_function(*args, **kwargs):
identifier = kwargs.get("identifier", None)

if not (business := Business.find_by_identifier(identifier)):
return jsonify({"message": f"{identifier} not found."}), HTTPStatus.NOT_FOUND

if not are_digital_credentials_allowed(business, jwt):
return jsonify({"message": f"digital credential not available for: {identifier}."}), HTTPStatus.UNAUTHORIZED

return f(*args, **kwargs)

return decorated_function
Loading