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 10 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
54 changes: 49 additions & 5 deletions optimade/server/query_params.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,52 @@
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]):
"""This method checks whether all the query_parameters that are specified in the URL string occur in the relevant QueryParams class.
If a query parameter is found that is not defined in the QueryParams class and it does not have a known prefix, an appropriate error or warning will be given.

args:
query_params: An iterable object that returns the query parameters, as strings, for which it should be checked that they are in the relevant QueryParams class.

"""
JPBergsma marked this conversation as resolved.
Show resolved Hide resolved
if not CONFIG.validate_query_parameters:
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 +211,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 +287,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
7 changes: 7 additions & 0 deletions optimade/server/warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,10 @@ class UnknownProviderProperty(OptimadeWarning):
recognised by this implementation.

"""


class UnknownProviderQueryParameter(OptimadeWarning):
"""A provider-specific query parameter has been requested in the query who's prefix is not
JPBergsma marked this conversation as resolved.
Show resolved Hide resolved
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
)