Files
gatehouse-api/gatehouse_app/utils/validators.py
T

95 lines
3.0 KiB
Python

"""Validation helpers for request data."""
from urllib.parse import urlparse
# Special sentinel values allowed in allowed_cors_origins
_CORS_SENTINELS = {"+", "*"}
def validate_cors_origins(origins):
"""Validate a list of CORS origin values.
Accepts:
None - means "use global CORS config" (pass-through)
["+"] - derive origins from the client's redirect_uris
["*"] - allow any origin
["https://host"] - explicit allow-list of well-formed origins
Each non-sentinel entry must be a well-formed origin:
scheme (http or https) + host + optional port, with NO path,
query string, or fragment.
Returns:
(validated_value, None) on success, or
(None, error_message) on failure.
"""
if origins is None:
return None, None
if not isinstance(origins, list):
return None, "allowed_cors_origins must be a list or null"
validated = []
for i, entry in enumerate(origins):
if not isinstance(entry, str):
return None, f"allowed_cors_origins[{i}]: expected a string, got {type(entry).__name__}"
entry = entry.strip()
if not entry:
return None, f"allowed_cors_origins[{i}]: empty string is not allowed"
# Sentinel values are accepted as-is
if entry in _CORS_SENTINELS:
validated.append(entry)
continue
# Parse and validate as origin
error = _validate_single_origin(entry, i)
if error:
return None, error
validated.append(entry)
return validated, None
def _validate_single_origin(origin, index):
"""Validate that a string is a well-formed browser origin.
A valid origin is: scheme://host[:port] with no path, query, or fragment.
Only http and https schemes are accepted.
Returns an error message string on failure, or None on success.
"""
try:
parsed = urlparse(origin)
except Exception:
return f"allowed_cors_origins[{index}]: '{origin}' is not a valid URL"
if parsed.scheme not in ("http", "https"):
return (
f"allowed_cors_origins[{index}]: '{origin}' has an invalid scheme "
f"'{parsed.scheme}'; only 'http' and 'https' are allowed"
)
if not parsed.hostname:
return f"allowed_cors_origins[{index}]: '{origin}' is missing a hostname"
# Origins must not have a path (other than empty or "/"), query, or fragment
if parsed.path and parsed.path != "/":
return (
f"allowed_cors_origins[{index}]: '{origin}' must not contain a path "
f"(got '{parsed.path}'). Specify only scheme://host[:port]"
)
if parsed.query:
return (
f"allowed_cors_origins[{index}]: '{origin}' must not contain a query string"
)
if parsed.fragment:
return (
f"allowed_cors_origins[{index}]: '{origin}' must not contain a fragment"
)
return None