|
14 | 14 | limitations under the License. |
15 | 15 | """ |
16 | 16 |
|
17 | | -from pynetbox.core.query import Request, RequestError |
| 17 | +from pynetbox.core.query import Request, RequestError, ParameterValidationError |
18 | 18 | from pynetbox.core.response import Record, RecordSet |
19 | 19 |
|
20 | 20 | RESERVED_KWARGS = () |
@@ -46,6 +46,7 @@ def __init__(self, api, app, name, model=None): |
46 | 46 | self.return_obj = self._lookup_ret_obj(name, model) |
47 | 47 | self.name = name.replace("_", "-") |
48 | 48 | self.api = api |
| 49 | + self.app = app |
49 | 50 | self.base_url = api.base_url |
50 | 51 | self.token = api.token |
51 | 52 | self.url = "{base_url}/{app}/{endpoint}".format( |
@@ -76,6 +77,58 @@ def _lookup_ret_obj(self, name, model): |
76 | 77 | ret = Record |
77 | 78 | return ret |
78 | 79 |
|
| 80 | + def _validate_openapi_parameters(self, method: str, parameters: dict) -> None: |
| 81 | + """Validate GET request parameters against OpenAPI specification |
| 82 | +
|
| 83 | + This method raises a **ParameterValidationError** if parameters passed to NetBox API |
| 84 | + do not match the OpenAPI specification or validation fails. |
| 85 | +
|
| 86 | + ## Parameters |
| 87 | +
|
| 88 | + * **method** : Only "get" is supported as for other methods NetBox already does proper validation |
| 89 | + * **parameters** : kwargs passed to filter() method |
| 90 | +
|
| 91 | + ## Returns |
| 92 | + None |
| 93 | + """ |
| 94 | + if method.lower() != "get": |
| 95 | + raise RuntimeError(f"Unsupported method '{method}'.") |
| 96 | + |
| 97 | + openapi_definition_path = "/api/{app}/{endpoint}/".format( |
| 98 | + app=self.app.name, |
| 99 | + endpoint=self.name, |
| 100 | + ) |
| 101 | + |
| 102 | + # Parse NetBox OpenAPI definition |
| 103 | + try: |
| 104 | + openapi_definition = self.api.openapi()["paths"].get( |
| 105 | + openapi_definition_path |
| 106 | + ) |
| 107 | + |
| 108 | + if not openapi_definition: |
| 109 | + raise ParameterValidationError( |
| 110 | + f"Path '{openapi_definition_path}' does not exist in NetBox OpenAPI specification." |
| 111 | + ) |
| 112 | + |
| 113 | + openapi_parameters = openapi_definition[method]["parameters"] |
| 114 | + allowed_parameters = [p["name"] for p in openapi_parameters] |
| 115 | + |
| 116 | + except KeyError as exc: |
| 117 | + raise ParameterValidationError( |
| 118 | + f"Error while parsing Netbox OpenAPI specification: {exc}" |
| 119 | + ) |
| 120 | + |
| 121 | + # Validate all parameters |
| 122 | + validation_errors = [] |
| 123 | + for p in parameters: |
| 124 | + if p not in allowed_parameters: |
| 125 | + validation_errors.append( |
| 126 | + f"'{p}' is not allowed as parameter on path '{openapi_definition_path}'." |
| 127 | + ) |
| 128 | + |
| 129 | + if len(validation_errors) > 0: |
| 130 | + raise ParameterValidationError(validation_errors) |
| 131 | + |
79 | 132 | def all(self, limit=0, offset=None): |
80 | 133 | """Queries the 'ListView' of a given endpoint. |
81 | 134 |
|
@@ -134,6 +187,8 @@ def get(self, *args, **kwargs): |
134 | 187 | * **key** (int, optional): id for the item to be retrieved. |
135 | 188 | * **kwargs**: Accepts the same keyword args as filter(). Any search argument the endpoint accepts can |
136 | 189 | be added as a keyword arg. |
| 190 | + * **strict_filters** (bool, optional): Overrides the global filter |
| 191 | + validation per-request basis. Handled by the filter() method. |
137 | 192 |
|
138 | 193 | ## Returns |
139 | 194 | A single Record object or None |
@@ -164,7 +219,6 @@ def get(self, *args, **kwargs): |
164 | 219 | # Row 1 |
165 | 220 | ``` |
166 | 221 | """ |
167 | | - |
168 | 222 | try: |
169 | 223 | key = args[0] |
170 | 224 | except IndexError: |
@@ -217,6 +271,8 @@ def filter(self, *args, **kwargs): |
217 | 271 | be returned with each query to the Netbox server. The queries |
218 | 272 | will be made as you iterate through the result set. |
219 | 273 | * **offset** (int, optional): Overrides the offset on paginated returns. |
| 274 | + * **strict_filters** (bool, optional): Overrides the global filter |
| 275 | + validation per-request basis. |
220 | 276 |
|
221 | 277 | ## Returns |
222 | 278 | A RecordSet object. |
@@ -272,9 +328,20 @@ def filter(self, *args, **kwargs): |
272 | 328 | ) |
273 | 329 | limit = kwargs.pop("limit") if "limit" in kwargs else 0 |
274 | 330 | offset = kwargs.pop("offset") if "offset" in kwargs else None |
| 331 | + strict_filters = ( |
| 332 | + # kwargs value takes precedence on globally set value |
| 333 | + kwargs.pop("strict_filters") |
| 334 | + if "strict_filters" in kwargs |
| 335 | + else self.api.strict_filters |
| 336 | + ) |
| 337 | + |
275 | 338 | if limit == 0 and offset is not None: |
276 | 339 | raise ValueError("offset requires a positive limit value") |
277 | 340 | filters = {x: y if y is not None else "null" for x, y in kwargs.items()} |
| 341 | + |
| 342 | + if strict_filters: |
| 343 | + self._validate_openapi_parameters("get", filters) |
| 344 | + |
278 | 345 | req = Request( |
279 | 346 | filters=filters, |
280 | 347 | base=self.url, |
|
0 commit comments