Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

#299-feature/presentation submission dynamic handler #314

Draft
wants to merge 11 commits into
base: refac
Choose a base branch
from
Draft
87 changes: 87 additions & 0 deletions pyeudiw/openid4vp/presentation_submission/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import os
import yaml
import importlib
from typing import Dict, Any
import logging

logger = logging.getLogger(__name__)

class PresentationSubmission:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't put the classes within the init file

we are going to refactor all the code within this project to remove this code-style in favour or a proper division of files by scopes

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

def __init__(self, submission: Dict[str, Any]):
"""
Initialize the PresentationSubmission handler with the submission data.

Args:
submission (Dict[str, Any]): The presentation submission data.

Raises:
KeyError: If the 'format' key is missing in the submission.
ValueError: If the format is not supported or not defined in the configuration.
ImportError: If the module or class cannot be loaded.
"""
self.config = self._load_config()
self.submission = submission
self.handlers = self._initialize_handlers()

def _load_config(self) -> Dict[str, Any]:
"""
Load the configuration from format_config.yml located in the same directory.

Returns:
Dict[str, Any]: The configuration dictionary.

Raises:
FileNotFoundError: If the configuration file is not found.
"""
config_path = os.path.join(os.path.dirname(__file__), "format_config.yml")
if not os.path.exists(config_path):
raise FileNotFoundError(f"Configuration file not found: {config_path}")

with open(config_path, "r") as config_file:
return yaml.safe_load(config_file)

def _initialize_handlers(self) -> Dict[int, object]:
"""
Initialize handlers for each item in the 'descriptor_map' of the submission.

Returns:
Dict[int, object]: A dictionary mapping indices to handler instances.

Raises:
KeyError: If the 'format' key is missing in any descriptor.
ValueError: If a format is not supported or not defined in the configuration.
ImportError: If a module or class cannot be loaded.
"""
handlers = {}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please create and add a pydantic schema validation here for the prensentation_submission object

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please put some limits on the presentation submission size, in a way that an attacker might not push 10mb of presentation submission causing resource exaustion to the RP

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added control for byte size and credential number too

try:
descriptor_map = self.submission.get("descriptor_map", [])
except KeyError:
raise KeyError("The 'descriptor_map' key is missing in the submission.")

for index, descriptor in enumerate(descriptor_map):
format_name = descriptor.get("format")
if not format_name:
raise KeyError(f"The 'format' key is missing in descriptor at index {index}.")

# Search for the format in the configuration
format_conf = next((fmt for fmt in self.config.get("formats", []) if fmt["name"] == format_name), None)
if not format_conf:
raise ValueError(f"Format '{format_name}' is not supported or not defined in the configuration.")

module_name = format_conf["module"]
class_name = format_conf["class"]

try:
# Dynamically load the module and class
module = importlib.import_module(module_name)
cls = getattr(module, class_name)
handlers[index] = cls() # Instantiate the class
except ModuleNotFoundError:
logger.warning(f"Module '{module_name}' not found for format '{format_name}'. Skipping index {index}.")
except AttributeError:
logger.warning(f"Class '{class_name}' not found in module '{module_name}' for format '{format_name}'. Skipping index {index}.")
except Exception as e:
logger.warning(f"Error loading format '{format_name}' for index {index}: {e}")

return handlers
13 changes: 13 additions & 0 deletions pyeudiw/openid4vp/presentation_submission/format_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
formats:
- name: "vc+sd-jwt"
LadyCodesItBetter marked this conversation as resolved.
Show resolved Hide resolved
module: "pyeudiw.openid4vp.presentation_submission"
class: "VcSdJwt"
- name: "ldp_vp"
module: "pyeudiw.openid4vp.presentation_submission"
class: "LdpVp"
- name: "jwt_vp_json"
module: "pyeudiw.openid4vp.presentation_submission"
class: "JwtVpJson"
- name: "ac_vp"
module: "pyeudiw.openid4vp.presentation_submission"
class: "AcVp"
77 changes: 77 additions & 0 deletions pyeudiw/tests/openid4vp/presentation_submission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import pytest
from unittest.mock import patch, MagicMock
from pyeudiw.openid4vp.presentation_submission import PresentationSubmission


# Mock data for testing
mock_format_config = {
"formats": [
{"name": "ldp_vp", "module": "mock.module", "class": "MockLdpVpHandler"},
{"name": "jwt_vp_json", "module": "mock.module", "class": "MockJwtVpJsonHandler"}
]
}

valid_submission = {
"descriptor_map": [
{"format": "ldp_vp", "id": "descriptor_1", "path": "$"},
{"format": "jwt_vp_json", "id": "descriptor_2", "path": "$"}
]
}

def test_presentation_submission_initialization():
"""
Test that the PresentationSubmission class initializes correctly,
loads handlers for all valid formats, and handles missing configurations.
"""
# Mock handler classes
mock_ldp_vp_handler = MagicMock(name="MockLdpVpHandler")
mock_jwt_vp_json_handler = MagicMock(name="MockJwtVpJsonHandler")

# Mock import_module to return a fake module with our mock classes
mock_module = MagicMock()
setattr(mock_module, "MockLdpVpHandler", mock_ldp_vp_handler)
setattr(mock_module, "MockJwtVpJsonHandler", mock_jwt_vp_json_handler)

with patch("pyeudiw.openid4vp.presentation_submission.PresentationSubmission._load_config", return_value=mock_format_config), \
patch("importlib.import_module", return_value=mock_module):

# Initialize the class
ps = PresentationSubmission(valid_submission)

# Assert that handlers were created for all formats in descriptor_map
assert len(ps.handlers) == len(valid_submission["descriptor_map"]), "Not all handlers were created."

# Check that the handlers are instances of the mocked classes
assert ps.handlers[0] is mock_ldp_vp_handler(), "Handler for 'ldp_vp' format is incorrect."
assert ps.handlers[1] is mock_jwt_vp_json_handler(), "Handler for 'jwt_vp_json' format is incorrect."

def test_presentation_submission_invalid_format():
"""
Test that the PresentationSubmission class handles unsupported formats gracefully.
"""
invalid_submission = {
"descriptor_map": [
{"format": "unsupported_format", "id": "descriptor_3", "path": "$"}
]
}

with patch("pyeudiw.openid4vp.presentation_submission.PresentationSubmission._load_config", return_value=mock_format_config):
# Expect a ValueError for unsupported format
with pytest.raises(ValueError, match="Format 'unsupported_format' is not supported or not defined in the configuration."):
PresentationSubmission(invalid_submission)

def test_presentation_submission_missing_format_key():
"""
Test that the PresentationSubmission class raises KeyError
when the 'format' key is missing in a descriptor.
"""
missing_format_submission = {
"descriptor_map": [
{"id": "descriptor_4", "path": "$"}
]
}

with patch("pyeudiw.openid4vp.presentation_submission.PresentationSubmission._load_config", return_value=mock_format_config):
# Expect a KeyError for missing 'format'
with pytest.raises(KeyError, match="The 'format' key is missing in descriptor at index 0."):
PresentationSubmission(missing_format_submission)