Skip to content

Commit

Permalink
feat: add default and custom validations for name field (#316)
Browse files Browse the repository at this point in the history
**Issue
number:[ADDON-74237](https://splunk.atlassian.net/browse/ADDON-74237)**

## Summary

Added support for default and custom name field validation

### Changes

* RestModel now has an additional parameter "special_fields"
* RestEndpoint now has two additional methods for executing custom
validations
* _pre_request decorator has new method "basic_name_validation" for
basic validations

### User experience

Name field will be validated on the server side

## Checklist

If your change doesn't seem to apply, please leave them unchecked.

* [x] I have performed a self-review of this change
* [x] Changes have been tested
* [ ] Changes are documented
* [x] PR title follows [conventional commit
semantics](https://www.conventionalcommits.org/en/v1.0.0/)

---------

Co-authored-by: mkolasinski-splunk <105011638+mkolasinski-splunk@users.noreply.github.com>
  • Loading branch information
sgoral-splunk and mkolasinski-splunk authored Oct 9, 2024
1 parent 21b6088 commit 1681309
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 55 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build-test-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ jobs:
- meta
- test-unit
strategy:
fail-fast: false
matrix:
splunk: ${{ fromJson(needs.meta.outputs.matrix_supportedSplunk) }}
env:
Expand Down
15 changes: 14 additions & 1 deletion splunktaucclib/rest_handler/endpoint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
# limitations under the License.
#

from typing import List, Optional

from .field import RestField
from ..error import RestError
from ..util import get_base_app_name

Expand All @@ -28,14 +30,18 @@


class RestModel:
def __init__(self, fields, name=None):
def __init__(
self, fields, name=None, special_fields: Optional[List[RestField]] = None
):
"""
REST Model.
:param name:
:param fields:
:param special_fields:
"""
self.name = name
self.fields = fields
self.special_fields = special_fields if special_fields else []


class RestEndpoint:
Expand Down Expand Up @@ -84,6 +90,13 @@ def _loop_fields(self, meth, name, data, *args, **kwargs):
def validate(self, name, data, existing=None):
self._loop_fields("validate", name, data, existing=existing)

def _loop_field_special(self, meth, name, data, *args, **kwargs):
model = self.model(name)
return [getattr(f, meth)(data, *args, **kwargs) for f in model.special_fields]

def validate_special(self, name, data):
self._loop_field_special("validate", name, data, validate_name=name)

def encode(self, name, data):
self._loop_fields("encode", name, data)

Expand Down
4 changes: 2 additions & 2 deletions splunktaucclib/rest_handler/endpoint/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ def __init__(
self.validator = validator
self.converter = converter

def validate(self, data, existing=None):
def validate(self, data, existing=None, validate_name=None):
# update case: check required field in data
if existing and self.name in data and not data.get(self.name) and self.required:
raise RestError(400, "Required field is missing: %s" % self.name)
value = data.get(self.name)
value = data.get(self.name) if not validate_name else validate_name
if not value and existing is None:
if self.required:
raise RestError(400, "Required field is missing: %s" % self.name)
Expand Down
35 changes: 33 additions & 2 deletions splunktaucclib/rest_handler/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@

__all__ = ["RestHandler"]

BASIC_NAME_VALIDATORS = {
"PROHIBITED_NAME_CHARACTERS": ["*", "\\", "[", "]", "(", ")", "?", ":"],
"PROHIBITED_NAMES": ["default", ".", ".."],
"MAX_LENGTH": 1024,
}


def _check_name_for_create(name):
if name == "default":
Expand Down Expand Up @@ -102,13 +108,38 @@ def check_existing(self, name):
else:
return None

def basic_name_validation(name: str):
tmp_name = str(name)
prohibited_chars = BASIC_NAME_VALIDATORS["PROHIBITED_NAME_CHARACTERS"]
prohibited_names = BASIC_NAME_VALIDATORS["PROHIBITED_NAMES"]
max_chars = BASIC_NAME_VALIDATORS["MAX_LENGTH"]
val_err_msg = (
f'{prohibited_names}, string started with "_" and string including any one '
f'of {prohibited_chars} are reserved value which cannot be used for field Name"'
)

if tmp_name.startswith("_") or any(
tmp_name == el for el in prohibited_names
):
raise RestError(400, val_err_msg)

if any(pc in prohibited_chars for pc in tmp_name):
raise RestError(400, val_err_msg)

if len(tmp_name) >= max_chars:
raise RestError(
400, f"Field Name must be less than {max_chars} characters"
)

@wraps(meth)
def wrapper(self, name, data):
self._endpoint.validate(
name,
data,
check_existing(self, name),
)
basic_name_validation(name)
self._endpoint.validate_special(name, data)
self._endpoint.encode(name, data)

return meth(self, name, data)
Expand Down Expand Up @@ -194,7 +225,7 @@ def all(self, decrypt=False, **query):
response = self._client.get(
self.path_segment(self._endpoint.internal_endpoint),
output_mode="json",
**query
**query,
)
return self._format_all_response(response, decrypt)

Expand Down Expand Up @@ -382,7 +413,7 @@ def _load_credentials(self, name, data):
self._endpoint.internal_endpoint,
name=name,
),
**masked
**masked,
)

def _encrypt_raw_credentials(self, data):
Expand Down
64 changes: 15 additions & 49 deletions tests/integration/demo/globalConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,14 @@
"configuration": {
"tabs": [
{
"name": "logging",
"entity": [
{
"type": "singleSelect",
"label": "Log level",
"options": {
"disableSearch": true,
"autoCompleteFields": [
{
"value": "DEBUG",
"label": "DEBUG"
},
{
"value": "INFO",
"label": "INFO"
},
{
"value": "WARN",
"label": "WARN"
},
{
"value": "ERROR",
"label": "ERROR"
},
{
"value": "CRITICAL",
"label": "CRITICAL"
}
]
},
"defaultValue": "INFO",
"field": "loglevel"
}
],
"title": "Logging"
"type": "loggingTab",
"levels": [
"DEBUG",
"INFO",
"WARN",
"ERROR",
"CRITICAL"
]
}
],
"title": "Configuration",
Expand All @@ -55,7 +28,7 @@
{
"type": "regex",
"errorMsg": "Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.",
"pattern": "^[a-zA-Z]\\w*$"
"pattern": "^[a-dA-D]\\w*$"
},
{
"type": "string",
Expand All @@ -69,19 +42,12 @@
"required": true
},
{
"type": "text",
"label": "Interval",
"validators": [
{
"type": "regex",
"errorMsg": "Interval must be an integer.",
"pattern": "^\\-[1-9]\\d*$|^\\d*$"
}
],
"defaultValue": "300",
"type": "interval",
"field": "interval",
"label": "Interval",
"help": "Time interval of the data input, in seconds.",
"required": true
"required": true,
"defaultValue": "300"
}
],
"title": "Demo"
Expand Down Expand Up @@ -144,6 +110,6 @@
"restRoot": "demo",
"version": "0.0.1",
"displayName": "Demo",
"schemaVersion": "0.0.3"
"schemaVersion": "0.0.8"
}
}
}
60 changes: 60 additions & 0 deletions tests/integration/demo/package/bin/demo_rh_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import import_declare_test

from splunktaucclib.rest_handler.endpoint import (
field,
validator,
RestModel,
DataInputModel,
)
from splunktaucclib.rest_handler import admin_external, util
from splunktaucclib.rest_handler.admin_external import AdminExternalHandler
import logging

util.remove_http_proxy_env_vars()


special_fields = [
field.RestField(
"name",
required=True,
encrypted=False,
default=None,
validator=validator.AllOf(
validator.Pattern(
regex=r"""^[a-dA-D]\w*$""",
),
validator.String(
max_len=100,
min_len=1,
),
),
)
]

fields = [
field.RestField(
"interval",
required=True,
encrypted=False,
default="300",
validator=validator.Pattern(
regex=r"""^(?:-1|\d+(?:\.\d+)?)$""",
),
),
field.RestField("disabled", required=False, validator=None),
]
model = RestModel(fields, name=None, special_fields=special_fields)


endpoint = DataInputModel(
"demo",
model,
)


if __name__ == "__main__":
logging.getLogger().addHandler(logging.NullHandler())
admin_external.handle(
endpoint,
handler=AdminExternalHandler,
)
37 changes: 37 additions & 0 deletions tests/integration/demo/package/bin/demo_rh_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import import_declare_test

from splunktaucclib.rest_handler.endpoint import (
field,
validator,
RestModel,
MultipleModel,
)
from splunktaucclib.rest_handler import admin_external, util
from splunktaucclib.rest_handler.admin_external import AdminExternalHandler
import logging

util.remove_http_proxy_env_vars()


special_fields = []

fields_logging = [
field.RestField(
"loglevel", required=True, encrypted=False, default="INFO", validator=None
)
]
model_logging = RestModel(fields_logging, name="logging", special_fields=special_fields)


endpoint = MultipleModel(
"demo_settings",
models=[model_logging],
)


if __name__ == "__main__":
logging.getLogger().addHandler(logging.NullHandler())
admin_external.handle(
endpoint,
handler=AdminExternalHandler,
)
Loading

0 comments on commit 1681309

Please sign in to comment.