Source code for univention.config_registry.validation

# SPDX-FileCopyrightText: 2022-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only

"""
|UCR| type validation classes.

Checks validity of type definitions and type compatibility of values to be set.
"""

from __future__ import annotations

import ipaddress
import json
import re
from re import Pattern
from typing import TYPE_CHECKING, Any, cast
from urllib.parse import urlsplit

import univention.config_registry_info as cri
from univention.config_registry.backend import BooleanConfigRegistry


if TYPE_CHECKING:
    from collections.abc import Container, Iterator


[docs] class BaseValidator: """Base class for |UCR| type validators.""" NAME = "" def __init__(self, attrs: dict[str, str]) -> None: pass
[docs] def is_valid(self, value: str) -> bool: """ Check if value is valid. :returns: ``True`` if valid, ``False`` otherwise. """ try: return bool(self.validate(value)) except Exception: return False
[docs] def validate(self, value: str) -> Any: """ Check is value is valid. :returns: something that can be evaulated with ``bool()``. :raises Exception: on errors. """ raise NotImplementedError()
def __str__(self) -> str: return self.NAME @classmethod def _recurse_subclasses(cls) -> Iterator[type[BaseValidator]]: for clazz in cls.__subclasses__(): if clazz.NAME: yield clazz yield from clazz._recurse_subclasses()
[docs] class String(BaseValidator): """ Validator for |UCR| type "str". Supports a Python compatible regular expression defined in the optional `regex` constraints """ NAME = "str" REGEX: Pattern | None = None def __init__(self, attrs: dict[str, str]) -> None: self.regex = attrs.get('regex', self.REGEX) # type: ignore @property def regex(self) -> str | None: return self._rxc.pattern if self._rxc else None @regex.setter def regex(self, regex: str | Pattern | None) -> None: rxc = None if regex is not None: try: rxc = re.compile(regex) except (ValueError, TypeError, re.error): raise ValueError('error compiling regex: %s' % regex) self._rxc = rxc
[docs] def validate(self, value: str) -> Any: if self._rxc: return self._rxc.match(value) else: return isinstance(value, str)
def __str__(self) -> str: return "%s(regex=%r)" % (self.NAME, self.regex) if self.regex else self.NAME
[docs] class URLHttp(BaseValidator): """Validator for |UCR| type "url_http".""" NAME = "url_http"
[docs] def validate(self, value: str) -> bool: o = urlsplit(value) o.port # may raise ValueError # noqa: B018 return o.scheme in {"http", "https"}
[docs] class URLProxy(BaseValidator): """Validator for |UCR| type "url_proxy".""" NAME = "url_proxy"
[docs] def validate(self, value: str) -> bool: o = urlsplit(value) o.port # may raise ValueError # noqa: B018 return o.scheme in {"http", "https"} and not o.path and not o.query and not o.fragment
[docs] class IPv4Address(BaseValidator): """Validator for |UCR| type "ipv4address".""" NAME = "ipv4address"
[docs] def validate(self, value: str) -> Any: return ipaddress.IPv4Address(value)
[docs] class IPv6Address(BaseValidator): """Validator for |UCR| type "ipv6address".""" NAME = "ipv6address"
[docs] def validate(self, value: str) -> Any: return ipaddress.IPv6Address(value)
[docs] class IPAddress(BaseValidator): """Validator for |UCR| type "ipaddress".""" NAME = "ipaddress"
[docs] def validate(self, value: str) -> Any: return ipaddress.ip_address(value)
[docs] class Integer(BaseValidator): """ Validator for |UCR| type "int". Supports optional 'min' and 'max' constraints """ NAME = "int" MIN: int | None = None MAX: int | None = None def __init__(self, attrs: dict[str, str]) -> None: self._min: int | None = None self._max: int | None = None self.min = cast(int | None, attrs.get('min', self.MIN)) self.max = cast(int | None, attrs.get('max', self.MAX)) @property def min(self) -> int | None: return self._min @min.setter def min(self, value: str | None) -> None: if value is None: self._min = None return val = int(value) if self._max is not None and val > self._max: raise ValueError('min %d > max %d' % (val, self._max)) self._min = val @property def max(self) -> int | None: return self._max @max.setter def max(self, value: str | None) -> None: if value is None: self._max = None return val = int(value) if self._min is not None and self._min > val: raise ValueError('min %d > max %d' % (self._min, val)) self._max = val
[docs] def validate(self, value: str) -> bool: val = int(value) if self._min is not None and val < self._min: return False return not (self._max is not None and val > self._max)
def __str__(self) -> str: return '%s(min=%r, max=%r)' % (self.NAME, self._min, self._max) if self._min or self._max else self.NAME
[docs] class UnsignedNumber(Integer): """Validator for |UCR| type "uint".""" NAME = "uint" MIN = 0 def __str__(self) -> str: return '%s(max=%r)' % (self.NAME, self._max) if self._max else self.NAME
[docs] class PositiveNumber(Integer): """Validator for |UCR| type "pint".""" NAME = "pint" MIN = 1 def __str__(self) -> str: return '%s(max=%r)' % (self.NAME, self._max) if self._max else self.NAME
[docs] class PortNumber(Integer): """Validator for |UCR| type "portnumber".""" NAME = "portnumber" MIN = 0 MAX = 65535 def __str__(self) -> str: return self.NAME
[docs] class Bool(BaseValidator): """Validator for |UCR| type "bool".""" NAME = "bool" _BCR = BooleanConfigRegistry()
[docs] def validate(self, value: str) -> bool: return self._BCR.is_true(value=value) or self._BCR.is_false(value=value)
[docs] class Json(BaseValidator): """Validator for |UCR| type "json".""" NAME = "json"
[docs] def validate(self, value: str) -> Any: return json.loads(value) or True
[docs] class List(BaseValidator): """Validator for |UCR| type "list".""" NAME = "list" DEFAULT_SEPARATOR = ',' def __init__(self, attrs: dict[str, str]) -> None: self.element_type = attrs.get('elementtype', "str") regex = attrs.get('separator', self.DEFAULT_SEPARATOR) try: self.separator = re.compile(regex) except re.error: raise ValueError('error compiling regex: %s' % regex) typ = Type.TYPE_CLASSES.get(self.element_type, String) self.checker = typ(attrs)
[docs] def validate(self, value: str) -> bool: if self.element_type is None: return False vinfo = cri.Variable() vinfo['type'] = self.element_type val = Type(vinfo) return all(val.check(element.strip()) for element in self.separator.split(value))
def __str__(self) -> str: return '%s[%s%s]' % (self.NAME, self.element_type, self.separator.pattern)
[docs] class Cron(BaseValidator): """Validator for |UCR| type "cron".""" NAME = "cron" PREDEFINED = frozenset(["@annually", "@yearly", "@monthly", "@weekly", "@daily", "@hourly", "@reboot"]) MONTHS = frozenset(["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"]) DAYS = frozenset(["sun", "mon", "tue", "wed", "thu", "fri", "sat"])
[docs] def validate(self, value: str) -> bool: if value.startswith("#"): pass elif value in self.PREDEFINED: pass else: minutes, hours, days_month, months, days_week = value.split() self._check(minutes, 0, 59) self._check(hours, 0, 23) self._check(days_month, 1, 31) self._check(months, 1, 12, self.MONTHS) self._check(days_week, 0, 7, self.DAYS) return True
@staticmethod def _check(text: str, low: int, high: int, extra: Container[str] = {}) -> None: if text.lower() in extra: return for value in text.split(","): base, _, step = value.partition("/") start, _, end = base.partition("-") if start == "*": pass elif low <= int(start) <= high: pass else: raise ValueError("start={start!r} not in range [{low}-{high}]".format(**locals())) if not end: pass elif start == "*": raise ValueError("end={end!r} not compatible with '*'".format(**locals())) elif low <= int(start) <= int(end) <= high: pass else: raise ValueError("end={end!r} not in range [{low}-{start}-{high}]".format(**locals())) if not step: pass elif start != "*" and not end: raise ValueError("step={step!r} requires range".format(**locals())) elif (low or 1) <= int(step) <= high: pass else: raise ValueError("step={step!r} not in range [{low_}-{high}]".format(low_=low or 1, **locals()))
[docs] class Type: """ Basic |UCR| type validation class. Usage:: try: validator = Type(vinfo) except (TypeError, ValueError): # invalid type else: if validator.check(value_to_be_set): # check ok: set value else: # value is not compatible with type definition """ TYPE_CLASSES: dict[str | None, type[BaseValidator]] = { clazz.NAME: clazz for clazz in BaseValidator._recurse_subclasses() } def __init__(self, vinfo: cri.Variable) -> None: self.vinfo = vinfo self.vtype: str | None = self.vinfo.get('type') typ = self.TYPE_CLASSES.get(self.vtype, String) self.checker = typ(self.vinfo)
[docs] def check(self, value: str) -> bool: return self.checker.is_valid(value)
def __str__(self) -> str: return str(self.checker)