mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 07:17:17 +00:00
add more validation rules
This commit is contained in:
parent
cbcfff27b8
commit
20974dae6f
@ -1 +1 @@
|
||||
skips: ['B101', 'B105', 'B106', 'B404']
|
||||
skips: ['B101', 'B104', 'B105', 'B106', 'B404']
|
||||
|
@ -69,7 +69,7 @@ Build related configuration. Group name can refer to architecture, e.g. ``build:
|
||||
* ``makepkg_flags`` - additional flags passed to ``makepkg`` command, space separated list of strings, optional.
|
||||
* ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional.
|
||||
* ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of mention.
|
||||
* ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, int, optional, default ``0``.
|
||||
* ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, int, optional, default ``604800``.
|
||||
|
||||
``repository`` group
|
||||
--------------------
|
||||
|
@ -52,7 +52,7 @@ class Validate(Handler):
|
||||
unsafe(bool): if set no user check will be performed before path creation
|
||||
"""
|
||||
schema = Validate.schema(architecture, configuration)
|
||||
validator = Validator(instance=configuration, schema=schema)
|
||||
validator = Validator(configuration=configuration, schema=schema)
|
||||
|
||||
if validator.validate(configuration.dump()):
|
||||
return # no errors found
|
||||
|
@ -64,6 +64,7 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
"mirror": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"is_url": [],
|
||||
},
|
||||
"repositories": {
|
||||
"type": "list",
|
||||
@ -111,10 +112,13 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
},
|
||||
"cookie_secret_key": {
|
||||
"type": "string",
|
||||
"minlength": 32,
|
||||
"maxlength": 64, # we cannot verify maxlength, because base64 representation might be longer than bytes
|
||||
},
|
||||
"max_age": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"oauth_provider": {
|
||||
"type": "string",
|
||||
@ -162,6 +166,7 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
"vcs_allowed_age": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -204,6 +209,7 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
"schema": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"is_url": ["http", "https"],
|
||||
},
|
||||
"debug": {
|
||||
"type": "boolean",
|
||||
@ -220,9 +226,11 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"is_ip_address": ["localhost"],
|
||||
},
|
||||
"index_url": {
|
||||
"type": "string",
|
||||
"is_url": ["http", "https"],
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
@ -258,44 +266,4 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"remote-pull": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"remote-push": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"report": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"upload": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -17,9 +17,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import ipaddress
|
||||
|
||||
from cerberus import TypeDefinition, Validator as RootValidator # type: ignore
|
||||
from pathlib import Path
|
||||
from typing import Any, List
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
@ -29,7 +32,7 @@ class Validator(RootValidator): # type: ignore
|
||||
class which defines custom validation methods for the service configuration
|
||||
|
||||
Attributes:
|
||||
instance(Configuration): configuration instance
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
|
||||
types_mapping = RootValidator.types_mapping.copy()
|
||||
@ -40,12 +43,12 @@ class Validator(RootValidator): # type: ignore
|
||||
default constructor
|
||||
|
||||
Args:
|
||||
instance(Configuration): configuration instance used for extraction
|
||||
configuration(Configuration): configuration instance used for extraction
|
||||
*args(Any): positional arguments to be passed to base validator
|
||||
**kwargs(): keyword arguments to be passed to base validator
|
||||
"""
|
||||
RootValidator.__init__(self, *args, **kwargs)
|
||||
self.instance: Configuration = kwargs["instance"]
|
||||
self.configuration: Configuration = kwargs["configuration"]
|
||||
|
||||
def _normalize_coerce_absolute_path(self, value: str) -> Path:
|
||||
"""
|
||||
@ -57,7 +60,7 @@ class Validator(RootValidator): # type: ignore
|
||||
Returns:
|
||||
Path: value converted to path instance according to configuration rules
|
||||
"""
|
||||
converted: Path = self.instance.converters["path"](value)
|
||||
converted: Path = self.configuration.converters["path"](value)
|
||||
return converted
|
||||
|
||||
def _normalize_coerce_boolean(self, value: str) -> bool:
|
||||
@ -71,7 +74,7 @@ class Validator(RootValidator): # type: ignore
|
||||
bool: value converted to boolean according to configuration rules
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
converted: bool = self.instance._convert_to_boolean(value) # type: ignore
|
||||
converted: bool = self.configuration._convert_to_boolean(value) # type: ignore
|
||||
return converted
|
||||
|
||||
def _normalize_coerce_integer(self, value: str) -> int:
|
||||
@ -97,9 +100,50 @@ class Validator(RootValidator): # type: ignore
|
||||
Returns:
|
||||
List[str]: value converted to string list instance according to configuration rules
|
||||
"""
|
||||
converted: List[str] = self.instance.converters["list"](value)
|
||||
converted: List[str] = self.configuration.converters["list"](value)
|
||||
return converted
|
||||
|
||||
def _validate_is_ip_address(self, constraint: List[str], field: str, value: str) -> None:
|
||||
"""
|
||||
check if the specified value is valid ip address
|
||||
|
||||
Args:
|
||||
constraint(List[str]): optional list of allowed special words (e.g. ``localhost``)
|
||||
field(str): field name to be checked
|
||||
value(Path): value to be checked
|
||||
|
||||
Examples:
|
||||
The rule's arguments are validated against this schema:
|
||||
{"type": "list", "schema": {"type": "string"}}
|
||||
"""
|
||||
if value in constraint:
|
||||
return
|
||||
try:
|
||||
ipaddress.ip_address(value)
|
||||
except ValueError:
|
||||
self._error(field, f"Value {value} must be valid IP address")
|
||||
|
||||
def _validate_is_url(self, constraint: List[str], field: str, value: str) -> None:
|
||||
"""
|
||||
check if the specified value is a valid url
|
||||
|
||||
Args:
|
||||
constraint(List[str]): optional list of supported schemas. If empty, no schema validation will be performed
|
||||
field(str): field name to be checked
|
||||
value(str): value to be checked
|
||||
|
||||
Examples:
|
||||
The rule's arguments are validated against this schema:
|
||||
{"type": "list", "schema": {"type": "string"}}
|
||||
"""
|
||||
url = urlparse(value) # it probably will never rise exceptions on parse
|
||||
if not url.scheme:
|
||||
self._error(field, f"Url scheme is not set for {value}")
|
||||
if not url.netloc and url.scheme not in ("file",):
|
||||
self._error(field, f"Location must be set for url {value} of scheme {url.scheme}")
|
||||
if constraint and url.scheme not in constraint:
|
||||
self._error(field, f"Url {value} scheme must be one of {constraint}")
|
||||
|
||||
def _validate_path_exists(self, constraint: bool, field: str, value: Path) -> None:
|
||||
"""
|
||||
check if paths exists
|
||||
|
@ -33,6 +33,16 @@ class RemotePullTrigger(Trigger):
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"remote-pull": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"gitremote": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
|
@ -38,6 +38,16 @@ class RemotePushTrigger(Trigger):
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"remote-push": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"gitremote": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
|
@ -35,6 +35,16 @@ class ReportTrigger(Trigger):
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"report": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"console": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
@ -62,6 +72,7 @@ class ReportTrigger(Trigger):
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"is_url": ["http", "https"],
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
@ -70,6 +81,7 @@ class ReportTrigger(Trigger):
|
||||
"link_path": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"is_url": [],
|
||||
},
|
||||
"no_empty_report": {
|
||||
"type": "boolean",
|
||||
@ -82,6 +94,8 @@ class ReportTrigger(Trigger):
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"required": True,
|
||||
"min": 0,
|
||||
"max": 65535,
|
||||
},
|
||||
"receivers": {
|
||||
"type": "list",
|
||||
@ -118,10 +132,12 @@ class ReportTrigger(Trigger):
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"is_url": ["http", "https"],
|
||||
},
|
||||
"link_path": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"is_url": [],
|
||||
},
|
||||
"path": {
|
||||
"type": "path",
|
||||
@ -153,10 +169,12 @@ class ReportTrigger(Trigger):
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"is_url": ["http", "https"],
|
||||
},
|
||||
"link_path": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"is_url": [],
|
||||
},
|
||||
"template_path": {
|
||||
"type": "path",
|
||||
@ -171,6 +189,7 @@ class ReportTrigger(Trigger):
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -35,6 +35,16 @@ class UploadTrigger(Trigger):
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"upload": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"target": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"github": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
@ -57,6 +67,7 @@ class UploadTrigger(Trigger):
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
@ -101,6 +112,7 @@ class UploadTrigger(Trigger):
|
||||
"chunk_size": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
|
@ -62,9 +62,11 @@ def test_schema(configuration: Configuration) -> None:
|
||||
assert schema.pop("email")
|
||||
assert schema.pop("github")
|
||||
assert schema.pop("html")
|
||||
assert schema.pop("report")
|
||||
assert schema.pop("rsync")
|
||||
assert schema.pop("s3")
|
||||
assert schema.pop("telegram")
|
||||
assert schema.pop("upload")
|
||||
|
||||
assert schema == CONFIGURATION_SCHEMA
|
||||
|
||||
|
@ -16,4 +16,4 @@ def validator(configuration: Configuration) -> Validator:
|
||||
Returns:
|
||||
Validator: validator test instance
|
||||
"""
|
||||
return Validator(instance=configuration, schema=CONFIGURATION_SCHEMA)
|
||||
return Validator(configuration=configuration, schema=CONFIGURATION_SCHEMA)
|
||||
|
@ -1,6 +1,6 @@
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, call as MockCall
|
||||
|
||||
from ahriman.core.configuration.validator import Validator
|
||||
|
||||
@ -18,7 +18,7 @@ def test_normalize_coerce_absolute_path(validator: Validator) -> None:
|
||||
must convert string value to path by using configuration converters
|
||||
"""
|
||||
convert_mock = MagicMock()
|
||||
validator.instance.converters["path"] = convert_mock
|
||||
validator.configuration.converters["path"] = convert_mock
|
||||
|
||||
validator._normalize_coerce_absolute_path("value")
|
||||
convert_mock.assert_called_once_with("value")
|
||||
@ -46,12 +46,56 @@ def test_normalize_coerce_list(validator: Validator) -> None:
|
||||
must convert string value to list by using configuration converters
|
||||
"""
|
||||
convert_mock = MagicMock()
|
||||
validator.instance.converters["list"] = convert_mock
|
||||
validator.configuration.converters["list"] = convert_mock
|
||||
|
||||
validator._normalize_coerce_list("value")
|
||||
convert_mock.assert_called_once_with("value")
|
||||
|
||||
|
||||
def test_validate_is_ip_address(validator: Validator, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must validate addresses correctly
|
||||
"""
|
||||
error_mock = mocker.patch("ahriman.core.configuration.validator.Validator._error")
|
||||
|
||||
validator._validate_is_ip_address(["localhost"], "field", "localhost")
|
||||
validator._validate_is_ip_address([], "field", "localhost")
|
||||
|
||||
validator._validate_is_ip_address([], "field", "127.0.0.1")
|
||||
validator._validate_is_ip_address([], "field", "::")
|
||||
validator._validate_is_ip_address([], "field", "0.0.0.0")
|
||||
|
||||
validator._validate_is_ip_address([], "field", "random string")
|
||||
|
||||
error_mock.assert_has_calls([
|
||||
MockCall("field", "Value localhost must be valid IP address"),
|
||||
MockCall("field", "Value random string must be valid IP address"),
|
||||
])
|
||||
|
||||
|
||||
def test_validate_is_url(validator: Validator, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must validate url correctly
|
||||
"""
|
||||
error_mock = mocker.patch("ahriman.core.configuration.validator.Validator._error")
|
||||
|
||||
validator._validate_is_url([], "field", "http://example.com")
|
||||
validator._validate_is_url([], "field", "https://example.com")
|
||||
validator._validate_is_url([], "field", "file:///tmp")
|
||||
|
||||
validator._validate_is_url(["http", "https"], "field", "file:///tmp")
|
||||
|
||||
validator._validate_is_url([], "field", "http:///path")
|
||||
|
||||
validator._validate_is_url([], "field", "random string")
|
||||
|
||||
error_mock.assert_has_calls([
|
||||
MockCall("field", "Url file:///tmp scheme must be one of ['http', 'https']"),
|
||||
MockCall("field", "Location must be set for url http:///path of scheme http"),
|
||||
MockCall("field", "Url scheme is not set for random string"),
|
||||
])
|
||||
|
||||
|
||||
def test_validate_path_exists(validator: Validator, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must validate that paths exists
|
||||
@ -67,4 +111,6 @@ def test_validate_path_exists(validator: Validator, mocker: MockerFixture) -> No
|
||||
mocker.patch("pathlib.Path.exists", return_value=True)
|
||||
validator._validate_path_exists(True, "field", Path("3"))
|
||||
|
||||
error_mock.assert_called_once_with("field", "Path 2 must exist")
|
||||
error_mock.assert_has_calls([
|
||||
MockCall("field", "Path 2 must exist"),
|
||||
])
|
||||
|
Loading…
Reference in New Issue
Block a user