Add source files: generator, validator, fuzzer modules
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
This commit is contained in:
393
src/core/validator.py
Normal file
393
src/core/validator.py
Normal file
@@ -0,0 +1,393 @@
|
||||
"""Request Validator for validating requests against OpenAPI schemas."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from jsonschema import Draft7Validator, ValidationError, validate
|
||||
|
||||
|
||||
class RequestValidatorError(Exception):
|
||||
"""Base exception for request validator errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ValidationFailure(RequestValidatorError):
|
||||
"""Raised when request validation fails."""
|
||||
|
||||
def __init__(self, message: str, errors: list[ValidationError]) -> None:
|
||||
"""Initialize validation failure.
|
||||
|
||||
Args:
|
||||
message: Error message.
|
||||
errors: List of validation errors.
|
||||
"""
|
||||
super().__init__(message)
|
||||
self.errors = errors
|
||||
|
||||
|
||||
class RequestValidator:
|
||||
"""Validates HTTP requests against OpenAPI schema definitions."""
|
||||
|
||||
def __init__(self, spec: dict[str, Any]) -> None:
|
||||
"""Initialize the validator with an OpenAPI specification.
|
||||
|
||||
Args:
|
||||
spec: Parsed OpenAPI specification.
|
||||
"""
|
||||
self.spec = spec
|
||||
self.schemas = self._extract_schemas()
|
||||
self.definitions = self._extract_definitions()
|
||||
|
||||
def _extract_schemas(self) -> dict[str, Any]:
|
||||
"""Extract schemas from components (OpenAPI 3.x).
|
||||
|
||||
Returns:
|
||||
Dictionary of schema definitions.
|
||||
"""
|
||||
components = self.spec.get("components", {})
|
||||
return components.get("schemas", {})
|
||||
|
||||
def _extract_definitions(self) -> dict[str, Any]:
|
||||
"""Extract definitions (Swagger 2.0).
|
||||
|
||||
Returns:
|
||||
Dictionary of schema definitions.
|
||||
"""
|
||||
return self.spec.get("definitions", {})
|
||||
|
||||
def _resolve_ref(self, ref: str) -> dict[str, Any] | None:
|
||||
"""Resolve a $ref reference.
|
||||
|
||||
Args:
|
||||
ref: The reference string.
|
||||
|
||||
Returns:
|
||||
The resolved schema or None.
|
||||
"""
|
||||
if ref.startswith("#/components/schemas/"):
|
||||
schema_name = ref.split("/")[-1]
|
||||
return self.schemas.get(schema_name)
|
||||
elif ref.startswith("#/definitions/"):
|
||||
schema_name = ref.split("/")[-1]
|
||||
return self.definitions.get(schema_name)
|
||||
return None
|
||||
|
||||
def _convert_schema(self, schema: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Convert OpenAPI schema to JSON Schema format.
|
||||
|
||||
Args:
|
||||
schema: OpenAPI schema definition.
|
||||
|
||||
Returns:
|
||||
JSON Schema compatible dictionary.
|
||||
"""
|
||||
if "$ref" in schema:
|
||||
resolved = self._resolve_ref(schema["$ref"])
|
||||
if resolved:
|
||||
return self._convert_schema(resolved)
|
||||
return schema
|
||||
|
||||
converted = {}
|
||||
|
||||
for key, value in schema.items():
|
||||
if key in {"type", "format", "description", "default", "example"}:
|
||||
converted[key] = value
|
||||
elif key == "properties":
|
||||
converted["properties"] = {
|
||||
prop_name: self._convert_schema(prop_schema)
|
||||
for prop_name, prop_schema in value.items()
|
||||
}
|
||||
elif key == "items":
|
||||
converted["items"] = self._convert_schema(value)
|
||||
elif key == "required":
|
||||
converted["required"] = value
|
||||
elif key == "enum":
|
||||
converted["enum"] = value
|
||||
elif key in {"minimum", "maximum", "minLength", "maxLength", "minItems", "maxItems"}:
|
||||
converted[key] = value
|
||||
elif key == "pattern":
|
||||
converted["pattern"] = value
|
||||
elif key == "additionalProperties":
|
||||
converted["additionalProperties"] = value
|
||||
|
||||
return converted
|
||||
|
||||
def validate_headers(
|
||||
self,
|
||||
headers: dict[str, Any],
|
||||
required_headers: list[str] | None = None,
|
||||
header_schemas: dict[str, dict[str, Any]] | None = None,
|
||||
) -> ValidationFailure | None:
|
||||
"""Validate request headers.
|
||||
|
||||
Args:
|
||||
headers: Request headers.
|
||||
required_headers: List of required header names.
|
||||
header_schemas: Schema definitions for headers.
|
||||
|
||||
Returns:
|
||||
ValidationFailure if validation fails, None otherwise.
|
||||
"""
|
||||
if required_headers:
|
||||
missing = [h for h in required_headers if h.lower() not in headers]
|
||||
if missing:
|
||||
errors = [
|
||||
ValidationError(
|
||||
f"Missing required header: {missing_h}",
|
||||
validator="required",
|
||||
validator_value=required_headers,
|
||||
instance=headers,
|
||||
)
|
||||
for missing_h in missing
|
||||
]
|
||||
return ValidationFailure("Header validation failed", errors)
|
||||
|
||||
if header_schemas:
|
||||
for header_name, header_schema in header_schemas.items():
|
||||
header_value = headers.get(header_name.lower())
|
||||
if header_value is not None:
|
||||
converted_schema = self._convert_schema(header_schema)
|
||||
try:
|
||||
validate(
|
||||
header_value,
|
||||
converted_schema,
|
||||
resolver=Draft7Validator,
|
||||
)
|
||||
except ValidationError as e:
|
||||
return ValidationFailure(
|
||||
f"Header '{header_name}' validation failed", [e]
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def validate_query_params(
|
||||
self,
|
||||
params: dict[str, Any],
|
||||
param_schemas: dict[str, dict[str, Any]],
|
||||
) -> ValidationFailure | None:
|
||||
"""Validate query parameters.
|
||||
|
||||
Args:
|
||||
params: Query parameters from request.
|
||||
param_schemas: Schema definitions for each parameter.
|
||||
|
||||
Returns:
|
||||
ValidationFailure if validation fails, None otherwise.
|
||||
"""
|
||||
for param_name, param_schema in param_schemas.items():
|
||||
param_value = params.get(param_name)
|
||||
required = param_schema.get("required", False)
|
||||
|
||||
if required and param_value is None:
|
||||
return ValidationFailure(
|
||||
f"Missing required query parameter: {param_name}",
|
||||
[
|
||||
ValidationError(
|
||||
f"Missing required parameter: {param_name}",
|
||||
validator="required",
|
||||
validator_value=[param_name],
|
||||
instance=params,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
if param_value is not None:
|
||||
converted_schema = self._convert_schema(param_schema)
|
||||
try:
|
||||
validate(param_value, converted_schema, resolver=Draft7Validator)
|
||||
except ValidationError as e:
|
||||
return ValidationFailure(
|
||||
f"Query parameter '{param_name}' validation failed", [e]
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def validate_path_params(
|
||||
self,
|
||||
params: dict[str, Any],
|
||||
param_schemas: dict[str, dict[str, Any]],
|
||||
) -> ValidationFailure | None:
|
||||
"""Validate path parameters.
|
||||
|
||||
Args:
|
||||
params: Path parameters extracted from URL.
|
||||
param_schemas: Schema definitions for each parameter.
|
||||
|
||||
Returns:
|
||||
ValidationFailure if validation fails, None otherwise.
|
||||
"""
|
||||
for param_name, param_schema in param_schemas.items():
|
||||
if param_name not in params:
|
||||
return ValidationFailure(
|
||||
f"Missing path parameter: {param_name}",
|
||||
[
|
||||
ValidationError(
|
||||
f"Missing path parameter: {param_name}",
|
||||
validator="required",
|
||||
validator_value=[param_name],
|
||||
instance=params,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def validate_body(
|
||||
self,
|
||||
body: Any,
|
||||
schema: dict[str, Any] | None,
|
||||
) -> ValidationFailure | None:
|
||||
"""Validate request body against schema.
|
||||
|
||||
Args:
|
||||
body: Request body.
|
||||
schema: JSON Schema for validation.
|
||||
|
||||
Returns:
|
||||
ValidationFailure if validation fails, None otherwise.
|
||||
"""
|
||||
if schema is None:
|
||||
return None
|
||||
|
||||
if body is None or body == "":
|
||||
if schema.get("required", False):
|
||||
return ValidationFailure(
|
||||
"Request body is required",
|
||||
[
|
||||
ValidationError(
|
||||
"Request body is required",
|
||||
validator="type",
|
||||
validator_value=["object", "array", "string"],
|
||||
instance=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
return None
|
||||
|
||||
converted_schema = self._convert_schema(schema)
|
||||
|
||||
try:
|
||||
validate(body, converted_schema, resolver=Draft7Validator)
|
||||
except ValidationError as e:
|
||||
return ValidationFailure("Request body validation failed", [e])
|
||||
|
||||
return None
|
||||
|
||||
def validate_request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
headers: dict[str, Any],
|
||||
query_params: dict[str, Any],
|
||||
path_params: dict[str, Any],
|
||||
body: Any,
|
||||
operation_spec: dict[str, Any],
|
||||
) -> ValidationFailure | None:
|
||||
"""Validate a complete request.
|
||||
|
||||
Args:
|
||||
method: HTTP method.
|
||||
path: Request path.
|
||||
headers: Request headers.
|
||||
query_params: Query parameters.
|
||||
path_params: Path parameters.
|
||||
body: Request body.
|
||||
operation_spec: Operation specification from OpenAPI.
|
||||
|
||||
Returns:
|
||||
ValidationFailure if validation fails, None otherwise.
|
||||
"""
|
||||
parameters = operation_spec.get("parameters", [])
|
||||
|
||||
header_schemas: dict[str, dict[str, Any]] = {}
|
||||
required_headers: list[str] = []
|
||||
|
||||
query_param_schemas: dict[str, dict[str, Any]] = {}
|
||||
path_param_schemas: dict[str, dict[str, Any]] = {}
|
||||
|
||||
for param in parameters:
|
||||
param_name = param.get("name", "")
|
||||
param_in = param.get("in", "")
|
||||
param_required = param.get("required", False)
|
||||
param_schema = param.get("schema", {})
|
||||
|
||||
if param_in == "header":
|
||||
header_schemas[param_name] = param_schema
|
||||
if param_required:
|
||||
required_headers.append(param_name)
|
||||
elif param_in == "query":
|
||||
query_param_schemas[param_name] = param_schema
|
||||
elif param_in == "path":
|
||||
path_param_schemas[param_name] = param_schema
|
||||
|
||||
validation_error = self.validate_headers(headers, required_headers, header_schemas)
|
||||
if validation_error:
|
||||
return validation_error
|
||||
|
||||
validation_error = self.validate_query_params(query_params, query_param_schemas)
|
||||
if validation_error:
|
||||
return validation_error
|
||||
|
||||
validation_error = self.validate_path_params(path_params, path_param_schemas)
|
||||
if validation_error:
|
||||
return validation_error
|
||||
|
||||
request_body = operation_spec.get("requestBody", {})
|
||||
if request_body:
|
||||
content = request_body.get("content", {})
|
||||
json_content = content.get("application/json", {})
|
||||
body_schema = json_content.get("schema")
|
||||
validation_error = self.validate_body(body, body_schema)
|
||||
if validation_error:
|
||||
return validation_error
|
||||
|
||||
return None
|
||||
|
||||
def format_validation_errors(
|
||||
self, errors: list[ValidationError]
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Format validation errors for response.
|
||||
|
||||
Args:
|
||||
errors: List of validation errors.
|
||||
|
||||
Returns:
|
||||
List of formatted error details.
|
||||
"""
|
||||
formatted = []
|
||||
|
||||
for error in errors:
|
||||
error_info: dict[str, Any] = {
|
||||
"message": error.message,
|
||||
"path": list(error.absolute_path) if error.absolute_path else [],
|
||||
"validator": error.validator,
|
||||
}
|
||||
|
||||
if error.instance is not None:
|
||||
error_info["instance"] = str(error.instance)
|
||||
|
||||
formatted.append(error_info)
|
||||
|
||||
return formatted
|
||||
|
||||
def create_error_response(
|
||||
self, failure: ValidationFailure
|
||||
) -> dict[str, Any]:
|
||||
"""Create an error response from a validation failure.
|
||||
|
||||
Args:
|
||||
failure: The validation failure.
|
||||
|
||||
Returns:
|
||||
Error response dictionary.
|
||||
"""
|
||||
return {
|
||||
"status_code": 400,
|
||||
"body": {
|
||||
"error": {
|
||||
"type": "Validation Error",
|
||||
"message": str(failure),
|
||||
"details": self.format_validation_errors(failure.errors),
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user