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

Added option to validate incoming URL query parameters #1122

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions optimade/server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@ class ServerConfig(BaseSettings):
Path("/var/log/optimade/"),
description="Folder in which log files will be saved.",
)
validate_query_parameters: Optional[bool] = Field(
True,
description="If True, the server will check whether the query parameters given in the request are correct.",
)

@validator("implementation", pre=True)
def set_implementation_version(cls, v):
Expand Down
62 changes: 57 additions & 5 deletions optimade/server/query_params.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,60 @@
from fastapi import Query
from pydantic import EmailStr # pylint: disable=no-name-in-module

from typing import Iterable
from optimade.server.config import CONFIG


class EntryListingQueryParams:
from warnings import warn
from optimade.server.mappers import BaseResourceMapper
from optimade.server.exceptions import BadRequest
from optimade.server.warnings import UnknownProviderQueryParameter
from abc import ABC


class BaseQueryParams(ABC):
def check_params(self, query_params: Iterable[str]) -> None:
"""This method checks whether all the query parameters that are specified
in the URL string are implemented in the relevant `*QueryParams` class.

If a query parameter is found that is not defined in the relevant `*QueryParams` class,
and it does not have a known provider prefix, an appropriate error (`BadRequest`)
or warning (`UnknownProviderQueryParameter`) will be emitted.

Arguments:
query_params: An iterable of the request's string query parameters.

Raises:
`BadRequest`: if the query parameter was not found in the relevant class, or if it
does not have a valid prefix.

"""
if not getattr(CONFIG, "validate_query_parameters", False):
return
errors = []
warnings = []
for param in query_params:
if not hasattr(self, param):
split_param = param.split("_")
if param.startswith("_") and len(split_param) > 2:
prefix = split_param[1]
if prefix in BaseResourceMapper.SUPPORTED_PREFIXES:
errors.append(param)
elif prefix not in BaseResourceMapper.KNOWN_PROVIDER_PREFIXES:
warnings.append(param)
else:
errors.append(param)

if warnings:
warn(
f"The query parameter(s) '{warnings}' are unrecognised and have been ignored.",
UnknownProviderQueryParameter,
)

if errors:
raise BadRequest(
f"The query parameter(s) '{errors}' are not recognised by this endpoint."
)


class EntryListingQueryParams(BaseQueryParams):
"""
Common query params for all Entry listing endpoints.

Expand Down Expand Up @@ -169,9 +219,10 @@ def __init__(
self.page_above = page_above
self.page_below = page_below
self.include = include
self.api_hint = api_hint


class SingleEntryQueryParams:
class SingleEntryQueryParams(BaseQueryParams):
"""
Common query params for single entry endpoints.

Expand Down Expand Up @@ -244,3 +295,4 @@ def __init__(
self.email_address = email_address
self.response_fields = response_fields
self.include = include
self.api_hint = api_hint
2 changes: 2 additions & 0 deletions optimade/server/routers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ def get_entries(
"""Generalized /{entry} endpoint getter"""
from optimade.server.routers import ENTRY_COLLECTIONS

params.check_params(request.query_params)
(
results,
data_returned,
Expand Down Expand Up @@ -284,6 +285,7 @@ def get_single_entry(
) -> EntryResponseOne:
from optimade.server.routers import ENTRY_COLLECTIONS

params.check_params(request.query_params)
params.filter = f'id="{entry_id}"'
(
results,
Expand Down
12 changes: 8 additions & 4 deletions optimade/server/warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ def __init__(self, detail: str = None, title: str = None, *args) -> None:
self.title = title if title else self.__class__.__name__

def __repr__(self) -> str:
attrs = {
"detail": self.detail,
"title": self.title,
}
attrs = {"detail": self.detail, "title": self.title}
return "<{:s}({:s})>".format(
self.__class__.__name__,
" ".join(
Expand Down Expand Up @@ -56,3 +53,10 @@ class UnknownProviderProperty(OptimadeWarning):
recognised by this implementation.

"""


class UnknownProviderQueryParameter(OptimadeWarning):
"""A provider-specific query parameter has been requested in the query with a prefix not
recognised by this implementation.

"""
46 changes: 46 additions & 0 deletions tests/server/query_params/test_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,3 +597,49 @@ def test_filter_on_unknown_fields(check_response, check_error_response):
request = "/structures?filter=_optimade_random_field = 1"
expected_ids = []
check_response(request, expected_ids=expected_ids)


def test_wrong_query_param(check_error_response):
request = "/structures?_exmpl_filter=nelements=2"
check_error_response(
request,
expected_status=400,
expected_title="Bad Request",
expected_detail="The query parameter(s) '['_exmpl_filter']' are not recognised by this endpoint.",
)

request = "/structures?filer=nelements=2"
check_error_response(
request,
expected_status=400,
expected_title="Bad Request",
expected_detail="The query parameter(s) '['filer']' are not recognised by this endpoint.",
)

request = "/structures/mpf_3819?filter=nelements=2"
check_error_response(
request,
expected_status=400,
expected_title="Bad Request",
expected_detail="The query parameter(s) '['filter']' are not recognised by this endpoint.",
)


def test_handling_prefixed_query_param(check_response):
request = "/structures?_odbx_filter=nelements=2&filter=elements LENGTH >= 9"
expected_ids = ["mpf_3819"]
check_response(request, expected_ids)

request = (
"/structures?_unknown_filter=elements HAS 'Si'&filter=elements LENGTH >= 9"
)
expected_ids = ["mpf_3819"]
expected_warnings = [
{
"title": "UnknownProviderQueryParameter",
"detail": "The query parameter(s) '['_unknown_filter']' are unrecognised and have been ignored.",
}
]
check_response(
request, expected_ids=expected_ids, expected_warnings=expected_warnings
)