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
2 changes: 1 addition & 1 deletion src/mock_vws/_flask_server/vws.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
TargetStatusProcessingError,
ValidatorError,
)
from mock_vws._services_validators.request_rate_validators import (
from mock_vws._services_validators.request_rate_limiter import (
RequestRateLimiter,
)
from mock_vws.database import CloudDatabase, VuMarkDatabase
Expand Down
165 changes: 22 additions & 143 deletions src/mock_vws/_services_validators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,62 +6,15 @@

from mock_vws._database_matchers import AnyDatabase

from .active_flag_validators import validate_active_flag
from .auth_validators import (
validate_access_key_exists,
validate_auth_header_exists,
validate_auth_header_has_signature,
validate_authorization,
)
from .content_length_validators import (
validate_content_length_header_is_int,
validate_content_length_header_not_too_large,
validate_content_length_header_not_too_small,
)
from .content_type_validators import validate_content_type_header_given
from .database_id_validators import validate_database_id_matches_keys
from .date_validators import (
validate_date_format,
validate_date_header_given,
validate_date_in_range,
)
from .image_validators import (
validate_image_color_space,
validate_image_data_type,
validate_image_encoding,
validate_image_format,
validate_image_integrity,
validate_image_is_image,
validate_image_pixel_count,
validate_image_size,
)
from .instance_id_validators import (
validate_instance_id_not_empty,
validate_instance_id_type,
)
from .json_validators import validate_body_given, validate_json
from .key_validators import validate_keys
from .metadata_validators import (
validate_metadata_encoding,
validate_metadata_size,
validate_metadata_type,
)
from .name_validators import (
validate_name_characters_in_range,
validate_name_does_not_exist_existing_target,
validate_name_does_not_exist_new_target,
validate_name_length,
validate_name_type,
)
from .project_state_validators import validate_project_state
from .request_quota_validators import validate_request_quota
from .request_rate_validators import (
RequestRateLimiter,
validate_request_rate,
)
from .target_quota_validators import validate_target_quota
from .target_validators import validate_target_id_exists
from .width_validators import validate_width
from .context import ValidatorContext
from .request_rate_limiter import RequestRateLimiter
from .routes import match_route


@beartype
Expand All @@ -74,7 +27,12 @@ def run_services_validators[DatabaseT: AnyDatabase](
databases: Iterable[DatabaseT],
request_rate_limiter: RequestRateLimiter,
) -> DatabaseT:
"""Run all validators.
"""Run the validators which apply to the request.

Every request is authorized first, because the validators which follow
are given the database which the request's server keys belong to. Which
validators follow, and in which order, is decided by the route the
request was made to. See :py:mod:`mock_vws._services_validators.routes`.

Args:
request_path: The path of the request.
Expand All @@ -100,104 +58,25 @@ def run_services_validators[DatabaseT: AnyDatabase](
request_path=request_path,
databases=databases,
)
validate_database_id_matches_keys(
request_path=request_path,
database=database,
)
validate_request_quota(database=database)
validate_request_rate(
request_method=request_method,
request_path=request_path,
database=database,
request_rate_limiter=request_rate_limiter,
)
validate_project_state(
request_method=request_method,
request_path=request_path,
database=database,
)
validate_target_quota(
request_method=request_method,
request_path=request_path,
database=database,
)
validate_target_id_exists(
request_path=request_path,
database=database,
)

validate_body_given(
request_body=request_body,
request_method=request_method,
)

validate_date_header_given(request_headers=request_headers)
validate_date_format(request_headers=request_headers)
validate_date_in_range(request_headers=request_headers)

validate_json(
request_body=request_body,
request_path=request_path,
request_method=request_method,
)

validate_keys(
request_body=request_body,
route = match_route(
request_path=request_path,
request_method=request_method,
)
validate_metadata_type(request_body=request_body)
validate_metadata_encoding(request_body=request_body)
validate_metadata_size(request_body=request_body)
validate_active_flag(request_body=request_body)
validate_instance_id_type(request_body=request_body)
validate_instance_id_not_empty(request_body=request_body)

validate_image_data_type(request_body=request_body)
validate_image_encoding(request_body=request_body)
validate_image_is_image(request_body=request_body)
validate_image_format(request_body=request_body)
validate_image_color_space(request_body=request_body)
validate_image_size(request_body=request_body)
validate_image_pixel_count(request_body=request_body)
validate_image_integrity(request_body=request_body)

validate_name_type(request_body=request_body)
validate_name_length(request_body=request_body)
validate_name_characters_in_range(
request_body=request_body,
request_method=request_method,
context = ValidatorContext(
request_path=request_path,
)
validate_name_does_not_exist_new_target(
request_body=request_body,
request_path=request_path,
database=database,
)
validate_name_does_not_exist_existing_target(
request_body=request_body,
request_path=request_path,
database=database,
)

validate_width(request_body=request_body)
validate_content_type_header_given(
request_headers=request_headers,
request_method=request_method,
)

validate_content_length_header_is_int(
request_headers=request_headers,
request_body=request_body,
)
validate_content_length_header_not_too_large(
request_headers=request_headers,
request_body=request_body,
)

validate_content_length_header_not_too_small(
request_headers=request_headers,
request_body=request_body,
)
database=database,
request_rate_limiter=request_rate_limiter,
mandatory_keys=route.mandatory_keys,
optional_keys=route.optional_keys,
rate_limited_endpoint=route.rate_limited_endpoint,
allowed_for_inactive_cloud_project=(
route.allowed_for_inactive_cloud_project
),
)
for validator in route.validators:
validator(context=context)

return database
14 changes: 6 additions & 8 deletions src/mock_vws/_services_validators/active_flag_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,28 @@

from beartype import beartype

from mock_vws._services_validators.context import ValidatorContext
from mock_vws._services_validators.exceptions import FailError

_LOGGER = logging.getLogger(name=__name__)


@beartype
def validate_active_flag(*, request_body: bytes) -> None:
def validate_active_flag(*, context: ValidatorContext) -> None:
"""Validate the active flag data given to the endpoint.

Args:
request_body: The body of the request.
context: The context of the request.

Raises:
FailError: There is active flag data given to the endpoint which is not
either a Boolean or NULL.
"""
if not request_body:
request_json = json.loads(s=context.request_body.decode())
if "active_flag" not in request_json:
return

request_text = request_body.decode()
if "active_flag" not in json.loads(s=request_text):
return

active_flag = json.loads(s=request_text).get("active_flag")
active_flag = request_json["active_flag"]

if active_flag in {True, False, None}:
return
Expand Down
65 changes: 29 additions & 36 deletions src/mock_vws/_services_validators/content_length_validators.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Content-Length header validators to use in the mock."""

import logging
from collections.abc import Mapping

from beartype import beartype

from mock_vws._services_validators.context import ValidatorContext
from mock_vws._services_validators.exceptions import (
AuthenticationFailureError,
ContentLengthHeaderNotIntError,
Expand All @@ -14,31 +14,39 @@
_LOGGER = logging.getLogger(name=__name__)


@beartype
def _given_content_length(*, context: ValidatorContext) -> str | int:
"""Return the given ``Content-Length``, or the real body length.

Args:
context: The context of the request.

Returns:
The value of the ``Content-Length`` header, or the length of the
request body if no such header was given.
"""
return dict(context.request_headers).get(
"Content-Length",
len(context.request_body),
)


@beartype
def validate_content_length_header_is_int(
*,
request_headers: Mapping[str, str],
request_body: bytes,
context: ValidatorContext,
) -> None:
"""Validate the ``Content-Length`` header is an integer.

Args:
request_headers: The headers sent with the request.
request_body: The body of the request.
context: The context of the request.

Raises:
ContentLengthHeaderNotIntError: The content length header is not an
integer
"""
body_length = len(request_body)
request_headers_dict = dict(request_headers)
given_content_length = request_headers_dict.get(
"Content-Length",
body_length,
)

try:
int(given_content_length)
int(_given_content_length(context=context))
except ValueError as exc:
_LOGGER.warning(msg="The Content-Length header is not an integer.")
raise ContentLengthHeaderNotIntError from exc
Expand All @@ -47,26 +55,19 @@ def validate_content_length_header_is_int(
@beartype
def validate_content_length_header_not_too_large(
*,
request_headers: Mapping[str, str],
request_body: bytes,
context: ValidatorContext,
) -> None:
"""Validate the ``Content-Length`` header is not too large.

Args:
request_headers: The headers sent with the request.
request_body: The body of the request.
context: The context of the request.

Raises:
ContentLengthHeaderTooLargeError: The given content length header says
that the content length is greater than the body length.
"""
body_length = len(request_body)
request_headers_dict = dict(request_headers)
given_content_length = request_headers_dict.get(
"Content-Length",
body_length,
)
given_content_length_value = int(given_content_length)
given_content_length_value = int(_given_content_length(context=context))
body_length = len(context.request_body)
# We skip coverage here as running a test to cover this is very slow.
if given_content_length_value > body_length: # pragma: no cover
_LOGGER.warning(msg="The Content-Length header is too large.")
Expand All @@ -76,27 +77,19 @@ def validate_content_length_header_not_too_large(
@beartype
def validate_content_length_header_not_too_small(
*,
request_headers: Mapping[str, str],
request_body: bytes,
context: ValidatorContext,
) -> None:
"""Validate the ``Content-Length`` header is not too small.

Args:
request_headers: The headers sent with the request.
request_body: The body of the request.
context: The context of the request.

Raises:
AuthenticationFailureError: The given content length header says that
the content length is smaller than the body length.
"""
body_length = len(request_body)
request_headers_dict = dict(request_headers)
given_content_length = request_headers_dict.get(
"Content-Length",
body_length,
)
given_content_length_value = int(given_content_length)
given_content_length_value = int(_given_content_length(context=context))

if given_content_length_value < body_length:
if given_content_length_value < len(context.request_body):
_LOGGER.warning(msg="The Content-Length header is too small.")
raise AuthenticationFailureError
Loading
Loading