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
Empty file.
15 changes: 15 additions & 0 deletions pyeudiw/openid4vp/presentation_submission/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
formats:
- name: "dc+sd-jwt"
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"

MAX_SUBMISSION_SIZE: 4096
121 changes: 121 additions & 0 deletions pyeudiw/openid4vp/presentation_submission/presentation_submission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import os
from pydantic import ValidationError
import yaml
import importlib
from typing import Dict, Any
import logging

from pyeudiw.openid4vp.presentation_submission.schemas import PresentationSubmissionSchema

logger = logging.getLogger(__name__)

class PresentationSubmission:
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.
ValidationError: If the submission data is invalid or exceeds size limits.
"""
self.config = self._load_config()
self.submission = self._validate_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__), "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 _validate_submission(self, submission: Dict[str, Any]) -> PresentationSubmissionSchema:
"""
Validate the submission data using Pydantic and check its total size.

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

Returns:
PresentationSubmissionSchema: Validated submission schema.

Raises:
ValidationError: If the submission data is invalid or exceeds size limits.
"""
max_size = self.config.get("MAX_SUBMISSION_SIZE", 10 * 1024 * 1024)

# Check submission size
submission_size = len(str(submission).encode("utf-8"))
if submission_size > max_size:
logger.warning(
f"Rejected submission: size {submission_size} bytes exceeds limit {max_size} bytes."
)
raise ValueError(
f"Submission size exceeds maximum allowed limit of {max_size} bytes."
)

try:
return PresentationSubmissionSchema(**submission)
except ValidationError as e:
logger.error(f"Submission validation failed: {e}")
raise
peppelinux marked this conversation as resolved.
Show resolved Hide resolved
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 = {}

try:
descriptor_map = self.submission.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.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
23 changes: 23 additions & 0 deletions pyeudiw/openid4vp/presentation_submission/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Any, Dict, List
from pydantic import BaseModel, field_validator


class DescriptorSchema(BaseModel):
id: str
format: str
path: str
path_nested: Dict[str, Any] = None


class PresentationSubmissionSchema(BaseModel):
id: str
definition_id: str
descriptor_map: List[DescriptorSchema]

@field_validator("descriptor_map")
@classmethod
def check_descriptor_map_size(cls, value):
max_descriptors = 100 # TODO: Define a reasonable limit
if len(value) > max_descriptors:
raise ValueError(f"descriptor_map exceeds maximum allowed size of {max_descriptors} items.")
return value
14 changes: 1 addition & 13 deletions pyeudiw/openid4vp/schemas/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,7 @@
from pydantic import BaseModel, field_validator

from pyeudiw.jwt.utils import is_jwt_format


class DescriptorSchema(BaseModel):
id: str
path: str
format: str


class PresentationSubmissionSchema(BaseModel):
definition_id: str
id: str
descriptor_map: list[DescriptorSchema]

from pyeudiw.openid4vp.presentation_submission.schemas import PresentationSubmissionSchema

class ResponseSchema(BaseModel):
state: Optional[str]
Expand Down
121 changes: 121 additions & 0 deletions pyeudiw/tests/openid4vp/presentation_submission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import pytest
from unittest.mock import patch, MagicMock
from pydantic import ValidationError
from pyeudiw.openid4vp.presentation_submission.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"}
],
"MAX_SUBMISSION_SIZE": 10 * 1024 # 10 KB
}

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

large_submission = {
"id": "submission_id_large",
"definition_id": "definition_id_large",
"descriptor_map": [{"id": f"descriptor_{i}", "format": "ldp_vp", "path": "$"} for i in range(101)] # Exceeds limit
}


def test_presentation_submission_initialization_with_schema_validation():
"""
Test that the PresentationSubmission class initializes correctly
and validates against the Pydantic schema.
"""
# 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.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_large_submission_with_schema():
"""
Test that the PresentationSubmission class raises a ValidationError
when the submission exceeds the descriptor_map size limit.
"""
with patch("pyeudiw.openid4vp.presentation_submission.presentation_submission.PresentationSubmission._load_config", return_value=mock_format_config):
# Expect a ValidationError for exceeding descriptor_map size limit
with pytest.raises(ValidationError, match="descriptor_map exceeds maximum allowed size of 100 items"):
PresentationSubmission(large_submission)


def test_presentation_submission_missing_descriptor_key():
"""
Test that the PresentationSubmission class raises a ValidationError
when required keys are missing in the descriptor_map.
"""
invalid_submission = {
"id": "invalid_submission_id",
"definition_id": "invalid_definition_id",
"descriptor_map": [
{"format": "ldp_vp"}
]
}

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

with pytest.raises(ValidationError, match=r"Field required"):
PresentationSubmission(invalid_submission)

def test_presentation_submission_invalid_format():
"""
Test that the PresentationSubmission class raises a ValueError
when an unsupported format is encountered.
"""
invalid_submission = {
"id": "invalid_submission_id",
"definition_id": "invalid_definition_id",
"descriptor_map": [
{"format": "unsupported_format", "id": "descriptor_1", "path": "$"}
]
}

with patch("pyeudiw.openid4vp.presentation_submission.presentation_submission.PresentationSubmission._load_config", return_value=mock_format_config):
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 a KeyError
when the 'format' key is missing in a descriptor.
"""
missing_format_key_submission = {
"id": "missing_format_submission_id",
"definition_id": "missing_format_definition_id",
"descriptor_map": [
{"id": "descriptor_1", "path": "$"} # Missing 'format' key
]
}

with patch("pyeudiw.openid4vp.presentation_submission.presentation_submission.PresentationSubmission._load_config", return_value=mock_format_config):
with pytest.raises(ValidationError, match=r"descriptor_map\.0\.format\s+Field required"):
PresentationSubmission(missing_format_key_submission)