Skip to content

Commit e223adf

Browse files
committed
feat: implement pagination bounds validation to criteria converters and URL converter
Signed-off-by: Adria Montoto <75563346+adriamontoto@users.noreply.github.com>
1 parent 8eda650 commit e223adf

11 files changed

+825
-12
lines changed

criteria_pattern/converters/criteria_to_mysql_converter.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
from typing import Any, assert_never
77

88
from criteria_pattern import Criteria, Direction, Operator
9-
from criteria_pattern.errors import InvalidColumnError, InvalidDirectionError, InvalidOperatorError, InvalidTableError
9+
from criteria_pattern.errors import (
10+
InvalidColumnError,
11+
InvalidDirectionError,
12+
InvalidOperatorError,
13+
InvalidTableError,
14+
PaginationBoundsError,
15+
)
1016
from criteria_pattern.models.criteria import AndCriteria, NotCriteria, OrCriteria
1117

1218

@@ -32,7 +38,7 @@ class CriteriaToMysqlConverter:
3238
""" # noqa: E501 # fmt: skip
3339

3440
@classmethod
35-
def convert(
41+
def convert( # noqa: C901
3642
cls,
3743
criteria: Criteria,
3844
table: str,
@@ -43,10 +49,13 @@ def convert(
4349
check_criteria_injection: bool = False,
4450
check_operator_injection: bool = False,
4551
check_direction_injection: bool = False,
52+
check_pagination_bounds: bool = False,
4653
valid_tables: Sequence[str] | None = None,
4754
valid_columns: Sequence[str] | None = None,
4855
valid_operators: Sequence[Operator] | None = None,
4956
valid_directions: Sequence[Direction] | None = None,
57+
max_page_size: int = 10000,
58+
max_page_number: int = 1000000,
5059
) -> tuple[str, list[Any]]:
5160
"""
5261
Convert the Criteria object to a MySQL query.
@@ -66,16 +75,21 @@ def convert(
6675
operators. Default to False.
6776
check_direction_injection (bool, optional): Raise an error if the direction is not in the list of valid
6877
directions. Default to False.
78+
check_pagination_bounds (bool, optional): Raise an error if pagination parameters exceed maximum bounds.
79+
Default to False.
6980
valid_tables (Sequence[str], optional): List of valid tables to query. Default to empty list.
7081
valid_columns (Sequence[str], optional): List of valid columns to select. Default to empty list.
7182
valid_operators (Sequence[Operator], optional): List of valid operators to use. Default to empty list.
7283
valid_directions (Sequence[Direction], optional): List of valid directions to use. Default to empty list.
84+
max_page_size (int, optional): Maximum allowed page_size to prevent integer overflow. Default to 10000.
85+
max_page_number (int, optional): Maximum allowed page_number to prevent integer overflow. Default to 1000000.
7386
7487
Raises:
7588
InvalidTableError: If the table is not in the list of valid tables (only if check_table_injection=True).
7689
InvalidColumnError: If the column is not in the list of valid columns (only if check_column_injection=True).
7790
InvalidOperatorError: If the operator is not in the list of valid operators (only if check_operator_injection=True).
7891
InvalidDirectionError: If the direction is not in the list of valid directions (only if check_direction_injection=True).
92+
PaginationBoundsError: If pagination parameters exceed maximum bounds (only if check_pagination_bounds=True).
7993
8094
Returns:
8195
tuple[str, list[Any]]: The MySQL query string and the query parameters as a list.
@@ -118,6 +132,13 @@ def convert(
118132
if check_direction_injection:
119133
cls._validate_directions(criteria=criteria, valid_directions=valid_directions)
120134

135+
if check_pagination_bounds:
136+
cls._validate_pagination_bounds(
137+
criteria=criteria,
138+
max_page_size=max_page_size,
139+
max_page_number=max_page_number,
140+
)
141+
121142
query = f'SELECT {", ".join(columns)} FROM {table}' # noqa: S608 # nosec
122143
parameters: list[Any] = []
123144
parameters_counter = 0
@@ -241,6 +262,25 @@ def _validate_directions(cls, *, criteria: Criteria, valid_directions: Sequence[
241262
valid_directions=valid_directions,
242263
)
243264

265+
@classmethod
266+
def _validate_pagination_bounds(cls, *, criteria: Criteria, max_page_size: int, max_page_number: int) -> None:
267+
"""
268+
Validate the Criteria object pagination parameters to prevent integer overflow.
269+
270+
Args:
271+
criteria (Criteria): Criteria to validate.
272+
max_page_size (int): Maximum allowed page_size.
273+
max_page_number (int): Maximum allowed page_number.
274+
275+
Raises:
276+
PaginationBoundsError: If pagination parameters exceed maximum bounds.
277+
"""
278+
if criteria.page_size is not None and criteria.page_size > max_page_size:
279+
raise PaginationBoundsError(parameter='page_size', value=criteria.page_size, max_value=max_page_size)
280+
281+
if criteria.page_number is not None and criteria.page_number > max_page_number:
282+
raise PaginationBoundsError(parameter='page_number', value=criteria.page_number, max_value=max_page_number)
283+
244284
@classmethod
245285
def _process_filters(cls, *, criteria: Criteria, columns_mapping: Mapping[str, str]) -> tuple[str, list[Any]]:
246286
"""

criteria_pattern/converters/criteria_to_postgresql_converter.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
from typing import Any, assert_never
77

88
from criteria_pattern import Criteria, Direction, Operator
9-
from criteria_pattern.errors import InvalidColumnError, InvalidDirectionError, InvalidOperatorError, InvalidTableError
9+
from criteria_pattern.errors import (
10+
InvalidColumnError,
11+
InvalidDirectionError,
12+
InvalidOperatorError,
13+
InvalidTableError,
14+
PaginationBoundsError,
15+
)
1016
from criteria_pattern.models.criteria import AndCriteria, NotCriteria, OrCriteria
1117

1218

@@ -32,7 +38,7 @@ class CriteriaToPostgresqlConverter:
3238
""" # noqa: E501 # fmt: skip
3339

3440
@classmethod
35-
def convert(
41+
def convert( # noqa: C901
3642
cls,
3743
criteria: Criteria,
3844
table: str,
@@ -43,10 +49,13 @@ def convert(
4349
check_criteria_injection: bool = False,
4450
check_operator_injection: bool = False,
4551
check_direction_injection: bool = False,
52+
check_pagination_bounds: bool = False,
4653
valid_tables: Sequence[str] | None = None,
4754
valid_columns: Sequence[str] | None = None,
4855
valid_operators: Sequence[Operator] | None = None,
4956
valid_directions: Sequence[Direction] | None = None,
57+
max_page_size: int = 10000,
58+
max_page_number: int = 1000000,
5059
) -> tuple[str, dict[str, Any]]:
5160
"""
5261
Convert the Criteria object to a Postgresql query.
@@ -66,16 +75,21 @@ def convert(
6675
Default to False.
6776
check_direction_injection (bool, optional): Raise an error if the direction is not in the list of valid
6877
directions. Default to False.
78+
check_pagination_bounds (bool, optional): Raise an error if pagination parameters exceed maximum bounds.
79+
Default to False.
6980
valid_tables (Sequence[str], optional): List of valid tables to query. Default to empty list.
7081
valid_columns (Sequence[str], optional): List of valid columns to select. Default to empty list.
7182
valid_operators (Sequence[Operator], optional): List of valid operators to use. Default to empty list.
7283
valid_directions (Sequence[Direction], optional): List of valid directions to use. Default to empty list.
84+
max_page_size (int, optional): Maximum allowed page_size to prevent integer overflow. Default to 10000.
85+
max_page_number (int, optional): Maximum allowed page_number to prevent integer overflow. Default to 1000000.
7386
7487
Raises:
7588
InvalidTableError: If the table is not in the list of valid tables (only if check_table_injection=True).
7689
InvalidColumnError: If the column is not in the list of valid columns (only if check_column_injection=True).
7790
InvalidOperatorError: If the operator is not in the list of valid operators (only if check_operator_injection=True).
7891
InvalidDirectionError: If the direction is not in the list of valid directions (only if check_direction_injection=True).
92+
PaginationBoundsError: If pagination parameters exceed maximum bounds (only if check_pagination_bounds=True).
7993
8094
Returns:
8195
tuple[str, dict[str, Any]]: The Postgresql query string and the query parameters.
@@ -118,6 +132,13 @@ def convert(
118132
if check_direction_injection:
119133
cls._validate_directions(criteria=criteria, valid_directions=valid_directions)
120134

135+
if check_pagination_bounds:
136+
cls._validate_pagination_bounds(
137+
criteria=criteria,
138+
max_page_size=max_page_size,
139+
max_page_number=max_page_number,
140+
)
141+
121142
quoted_columns = ['*' if column == '*' else f'"{column}"' for column in columns]
122143
quoted_table = '.'.join(f'"{part}"' for part in table.split('.'))
123144
query = f'SELECT {", ".join(quoted_columns)} FROM {quoted_table}' # noqa: S608 # nosec
@@ -245,6 +266,25 @@ def _validate_directions(cls, *, criteria: Criteria, valid_directions: Sequence[
245266
valid_directions=valid_directions,
246267
)
247268

269+
@classmethod
270+
def _validate_pagination_bounds(cls, *, criteria: Criteria, max_page_size: int, max_page_number: int) -> None:
271+
"""
272+
Validate the Criteria object pagination parameters to prevent integer overflow.
273+
274+
Args:
275+
criteria (Criteria): Criteria to validate.
276+
max_page_size (int): Maximum allowed page_size.
277+
max_page_number (int): Maximum allowed page_number.
278+
279+
Raises:
280+
PaginationBoundsError: If pagination parameters exceed maximum bounds.
281+
"""
282+
if criteria.page_size is not None and criteria.page_size > max_page_size:
283+
raise PaginationBoundsError(parameter='page_size', value=criteria.page_size, max_value=max_page_size)
284+
285+
if criteria.page_number is not None and criteria.page_number > max_page_number:
286+
raise PaginationBoundsError(parameter='page_number', value=criteria.page_number, max_value=max_page_number)
287+
248288
@classmethod
249289
def _process_filters(cls, *, criteria: Criteria, columns_mapping: Mapping[str, str]) -> tuple[str, dict[str, Any]]:
250290
"""

criteria_pattern/converters/criteria_to_sqlite_converter.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
from typing import Any, assert_never
77

88
from criteria_pattern import Criteria, Direction, Operator
9-
from criteria_pattern.errors import InvalidColumnError, InvalidDirectionError, InvalidOperatorError, InvalidTableError
9+
from criteria_pattern.errors import (
10+
InvalidColumnError,
11+
InvalidDirectionError,
12+
InvalidOperatorError,
13+
InvalidTableError,
14+
PaginationBoundsError,
15+
)
1016
from criteria_pattern.models.criteria import AndCriteria, NotCriteria, OrCriteria
1117

1218

@@ -32,7 +38,7 @@ class CriteriaToSqliteConverter:
3238
""" # noqa: E501 # fmt: skip
3339

3440
@classmethod
35-
def convert(
41+
def convert( # noqa: C901
3642
cls,
3743
criteria: Criteria,
3844
table: str,
@@ -43,10 +49,13 @@ def convert(
4349
check_criteria_injection: bool = False,
4450
check_operator_injection: bool = False,
4551
check_direction_injection: bool = False,
52+
check_pagination_bounds: bool = False,
4653
valid_tables: Sequence[str] | None = None,
4754
valid_columns: Sequence[str] | None = None,
4855
valid_operators: Sequence[Operator] | None = None,
4956
valid_directions: Sequence[Direction] | None = None,
57+
max_page_size: int = 10000,
58+
max_page_number: int = 1000000,
5059
) -> tuple[str, dict[str, Any]]:
5160
"""
5261
Convert the Criteria object to a SQLite query.
@@ -66,16 +75,21 @@ def convert(
6675
operators. Default to False.
6776
check_direction_injection (bool, optional): Raise an error if the direction is not in the list of valid
6877
directions. Default to False.
78+
check_pagination_bounds (bool, optional): Raise an error if pagination parameters exceed maximum bounds.
79+
Default to False.
6980
valid_tables (Sequence[str], optional): List of valid tables to query. Default to empty list.
7081
valid_columns (Sequence[str], optional): List of valid columns to select. Default to empty list.
7182
valid_operators (Sequence[Operator], optional): List of valid operators to use. Default to empty list.
7283
valid_directions (Sequence[Direction], optional): List of valid directions to use. Default to empty list.
84+
max_page_size (int, optional): Maximum allowed page_size to prevent integer overflow. Default to 10000.
85+
max_page_number (int, optional): Maximum allowed page_number to prevent integer overflow. Default to 1000000.
7386
7487
Raises:
7588
InvalidTableError: If the table is not in the list of valid tables (only if check_table_injection=True).
7689
InvalidColumnError: If the column is not in the list of valid columns (only if check_column_injection=True).
7790
InvalidOperatorError: If the operator is not in the list of valid operators (only if check_operator_injection=True).
7891
InvalidDirectionError: If the direction is not in the list of valid directions (only if check_direction_injection=True).
92+
PaginationBoundsError: If pagination parameters exceed maximum bounds (only if check_pagination_bounds=True).
7993
8094
Returns:
8195
tuple[str, dict[str, Any]]: The SQLite query string and the query parameters.
@@ -118,6 +132,13 @@ def convert(
118132
if check_direction_injection:
119133
cls._validate_directions(criteria=criteria, valid_directions=valid_directions)
120134

135+
if check_pagination_bounds:
136+
cls._validate_pagination_bounds(
137+
criteria=criteria,
138+
max_page_size=max_page_size,
139+
max_page_number=max_page_number,
140+
)
141+
121142
quoted_columns = ['*' if column == '*' else f'"{column}"' for column in columns]
122143
quoted_table = '.'.join(f'"{part}"' for part in table.split('.'))
123144
query = f'SELECT {", ".join(quoted_columns)} FROM {quoted_table}' # noqa: S608 # nosec
@@ -245,6 +266,25 @@ def _validate_directions(cls, *, criteria: Criteria, valid_directions: Sequence[
245266
valid_directions=valid_directions,
246267
)
247268

269+
@classmethod
270+
def _validate_pagination_bounds(cls, *, criteria: Criteria, max_page_size: int, max_page_number: int) -> None:
271+
"""
272+
Validate the Criteria object pagination parameters to prevent integer overflow.
273+
274+
Args:
275+
criteria (Criteria): Criteria to validate.
276+
max_page_size (int): Maximum allowed page_size.
277+
max_page_number (int): Maximum allowed page_number.
278+
279+
Raises:
280+
PaginationBoundsError: If pagination parameters exceed maximum bounds.
281+
"""
282+
if criteria.page_size is not None and criteria.page_size > max_page_size:
283+
raise PaginationBoundsError(parameter='page_size', value=criteria.page_size, max_value=max_page_size)
284+
285+
if criteria.page_number is not None and criteria.page_number > max_page_number:
286+
raise PaginationBoundsError(parameter='page_number', value=criteria.page_number, max_value=max_page_number)
287+
248288
@classmethod
249289
def _process_filters(cls, *, criteria: Criteria, columns_mapping: Mapping[str, str]) -> tuple[str, dict[str, Any]]:
250290
"""

criteria_pattern/converters/url_to_criteria_converter.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
from urllib.parse import parse_qs, unquote_plus, urlparse
99

1010
from criteria_pattern import Criteria, Direction, Filter, Operator, Order
11-
from criteria_pattern.errors import InvalidColumnError, InvalidDirectionError, InvalidOperatorError
11+
from criteria_pattern.errors import (
12+
InvalidColumnError,
13+
InvalidDirectionError,
14+
InvalidOperatorError,
15+
PaginationBoundsError,
16+
)
1217

1318

1419
class UrlToCriteriaConverter:
@@ -67,9 +72,12 @@ def convert(
6772
check_field_injection: bool = False,
6873
check_operator_injection: bool = False,
6974
check_direction_injection: bool = False,
75+
check_pagination_bounds: bool = False,
7076
valid_fields: Sequence[str] | None = None,
7177
valid_operators: Sequence[Operator] | None = None,
7278
valid_directions: Sequence[Direction] | None = None,
79+
max_page_size: int = 10000,
80+
max_page_number: int = 1000000,
7381
) -> Criteria:
7482
"""
7583
Converts an URL query string into a Criteria object.
@@ -80,9 +88,12 @@ def convert(
8088
check_field_injection (bool, optional): Whether to check for field injection.
8189
check_operator_injection (bool, optional): Whether to check for operator injection.
8290
check_direction_injection (bool, optional): Whether to check for direction injection.
91+
check_pagination_bounds (bool, optional): Whether to check pagination parameters bounds.
8392
valid_fields (Sequence[str], optional): A list of valid field names. Default to empty list.
8493
valid_operators (Sequence[Operator], optional): A list of valid operators. Default to empty list.
8594
valid_directions (Sequence[Direction], optional): A list of valid directions. Default to empty list.
95+
max_page_size (int, optional): Maximum allowed page_size to prevent integer overflow. Default to 10000.
96+
max_page_number (int, optional): Maximum allowed page_number to prevent integer overflow. Default to 1000000.
8697
8798
Raises:
8899
TypeError: If the filter index is not an integer.
@@ -98,6 +109,7 @@ def convert(
98109
InvalidColumnError: If an invalid field name is found in orders.
99110
InvalidOperatorError: If an invalid operator is found in filters.
100111
InvalidDirectionError: If an invalid direction is found in orders.
112+
PaginationBoundsError: If pagination parameters exceed maximum bounds.
101113
102114
Example:
103115
```python
@@ -137,6 +149,13 @@ def convert(
137149
if check_direction_injection:
138150
cls._validate_directions(criteria=criteria, valid_directions=valid_directions)
139151

152+
if check_pagination_bounds:
153+
cls._validate_pagination_bounds(
154+
criteria=criteria,
155+
max_page_size=max_page_size,
156+
max_page_number=max_page_number,
157+
)
158+
140159
return criteria
141160

142161
@classmethod
@@ -445,3 +464,22 @@ def _validate_directions(cls, *, criteria: Criteria, valid_directions: Sequence[
445464
direction=Direction(value=order.direction),
446465
valid_directions=valid_directions,
447466
)
467+
468+
@classmethod
469+
def _validate_pagination_bounds(cls, *, criteria: Criteria, max_page_size: int, max_page_number: int) -> None:
470+
"""
471+
Validate the Criteria object pagination parameters to prevent integer overflow.
472+
473+
Args:
474+
criteria (Criteria): Criteria to validate.
475+
max_page_size (int): Maximum allowed page_size.
476+
max_page_number (int): Maximum allowed page_number.
477+
478+
Raises:
479+
PaginationBoundsError: If pagination parameters exceed maximum bounds.
480+
"""
481+
if criteria.page_size is not None and criteria.page_size > max_page_size:
482+
raise PaginationBoundsError(parameter='page_size', value=criteria.page_size, max_value=max_page_size)
483+
484+
if criteria.page_number is not None and criteria.page_number > max_page_number:
485+
raise PaginationBoundsError(parameter='page_number', value=criteria.page_number, max_value=max_page_number)

criteria_pattern/errors/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
from .invalid_direction_error import InvalidDirectionError
33
from .invalid_operator_error import InvalidOperatorError
44
from .invalid_table_error import InvalidTableError
5+
from .pagination_bounds_error import PaginationBoundsError
56
from .sql_converter_error import SqlConverterError
67

78
__all__ = (
89
'InvalidColumnError',
910
'InvalidDirectionError',
1011
'InvalidOperatorError',
1112
'InvalidTableError',
13+
'PaginationBoundsError',
1214
'SqlConverterError',
1315
)

0 commit comments

Comments
 (0)