mirror of
https://github.com/fuckpiracyshield/component.git
synced 2024-11-21 04:59:45 +01:00
Initial commit.
This commit is contained in:
commit
ca324d66c7
37 changed files with 1510 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
__pycache__/
|
||||
build/
|
||||
eggs/
|
||||
.eggs/
|
||||
*.egg
|
||||
*.egg-info/
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
### Component
|
||||
|
||||
Necessary components needed by every other package.
|
5
pyproject.toml
Normal file
5
pyproject.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=54",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
18
setup.cfg
Normal file
18
setup.cfg
Normal file
|
@ -0,0 +1,18 @@
|
|||
[metadata]
|
||||
name = piracyshield_component
|
||||
version = 1.0.0
|
||||
description = Base Components
|
||||
|
||||
[options]
|
||||
package_dir=
|
||||
=src
|
||||
packages = find:
|
||||
python_requires = >= 3.10
|
||||
install_requires =
|
||||
toml
|
||||
pyjwt
|
||||
argon2-cffi
|
||||
pytz
|
||||
|
||||
[options.packages.find]
|
||||
where = src
|
1
src/piracyshield_component/__init__.py
Normal file
1
src/piracyshield_component/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
|
80
src/piracyshield_component/config.py
Normal file
80
src/piracyshield_component/config.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
from piracyshield_component.environment import Environment
|
||||
|
||||
import os
|
||||
import toml
|
||||
|
||||
class Config:
|
||||
|
||||
"""
|
||||
Configuration utility that supports TOML config creation.
|
||||
"""
|
||||
|
||||
config_path = None
|
||||
|
||||
config_content = None
|
||||
|
||||
def __init__(self, config_path: str):
|
||||
"""
|
||||
Handles the setting of the path of a config file.
|
||||
This is representable by: Environment.CONFIG_PATH/<config_path>/my_config.toml
|
||||
|
||||
:param config_path: a valid path present in CONFIG_PATH directory.
|
||||
"""
|
||||
|
||||
self.config_path = config_path
|
||||
|
||||
self.config_content = self.load()
|
||||
|
||||
def load(self) -> dict | Exception:
|
||||
"""
|
||||
Loads the whole file.
|
||||
|
||||
:return: returns the content of the file.
|
||||
"""
|
||||
|
||||
file_path = f'{Environment.CONFIG_PATH}/{self.config_path}.toml'
|
||||
|
||||
try:
|
||||
return toml.load(file_path)
|
||||
|
||||
except FileNotFoundError:
|
||||
raise ConfigNotFound(f'Impossibile trovare file {file_path}')
|
||||
|
||||
def get(self, key: str) -> str | Exception:
|
||||
"""
|
||||
Gets a single key from the loaded content.
|
||||
|
||||
:param key: a valid key.
|
||||
:return: value of the dictionary key.
|
||||
"""
|
||||
|
||||
try:
|
||||
return self.config_content[key]
|
||||
|
||||
except KeyError:
|
||||
raise ConfigKeyNotFound(f'Impossibile trovare chiave {key}')
|
||||
|
||||
def get_all(self, key: str = None) -> any:
|
||||
"""
|
||||
Returns the whole content of a configuration or its path.
|
||||
|
||||
:return: different types of data.
|
||||
"""
|
||||
|
||||
return self.config_content[key] if key else self.config_path
|
||||
|
||||
class ConfigNotFound(Exception):
|
||||
|
||||
"""
|
||||
No config found.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class ConfigKeyNotFound(Exception):
|
||||
|
||||
"""
|
||||
Key passed not found.
|
||||
"""
|
||||
|
||||
pass
|
23
src/piracyshield_component/environment.py
Normal file
23
src/piracyshield_component/environment.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
import os
|
||||
|
||||
# TODO: provide development and production environment types
|
||||
# to have customization of the code in the other packages.
|
||||
|
||||
class Environment:
|
||||
|
||||
"""
|
||||
Environment management.
|
||||
|
||||
DATA_PATH consists in the root of the next folders.
|
||||
The structure should be:
|
||||
|
||||
data/
|
||||
config/
|
||||
cache/
|
||||
"""
|
||||
|
||||
DATA_PATH = os.environ['PIRACYSHIELD_DATA_PATH']
|
||||
|
||||
CONFIG_PATH = os.environ['PIRACYSHIELD_CONFIG_PATH']
|
||||
|
||||
CACHE_PATH = os.environ['PIRACYSHIELD_CACHE_PATH']
|
53
src/piracyshield_component/exception.py
Normal file
53
src/piracyshield_component/exception.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
import traceback
|
||||
|
||||
from piracyshield_component.log.logger import Logger
|
||||
|
||||
class ApplicationException(Exception):
|
||||
|
||||
"""
|
||||
Global application exception.
|
||||
This class is invoked as a last gateway from the error.
|
||||
"""
|
||||
|
||||
def __init__(self, code: str, message: str, unrecovered_exception = None):
|
||||
"""
|
||||
Provides context for the exception.
|
||||
|
||||
:param code: predefined code that identifies the error.
|
||||
:param message: short description of the issue.
|
||||
"""
|
||||
|
||||
self._code = code
|
||||
|
||||
self._message = message
|
||||
|
||||
self._unrecovered_exception = unrecovered_exception
|
||||
|
||||
logger = Logger('application')
|
||||
|
||||
logger.debug(f'{code}: {message}')
|
||||
|
||||
if unrecovered_exception:
|
||||
self._traceback = traceback.format_exc()
|
||||
|
||||
logger.error(f'Unrecovered exception: {unrecovered_exception} {self._traceback}')
|
||||
|
||||
@property
|
||||
def code(self) -> str:
|
||||
"""
|
||||
Sets the options.
|
||||
|
||||
:return: string containing the error code.
|
||||
"""
|
||||
|
||||
return self._code
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
"""
|
||||
Sets the options.
|
||||
|
||||
:return: string of the error message.
|
||||
"""
|
||||
|
||||
return self._message
|
1
src/piracyshield_component/io/__init__.py
Normal file
1
src/piracyshield_component/io/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
|
28
src/piracyshield_component/io/filesystem.py
Normal file
28
src/piracyshield_component/io/filesystem.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
import os
|
||||
|
||||
class Filesystem:
|
||||
|
||||
"""
|
||||
Class to manage elements on the filesystem.
|
||||
"""
|
||||
|
||||
def get_size(self, absolute_file_path) -> int | Exception:
|
||||
"""
|
||||
Returns the size in Bytes format.
|
||||
|
||||
:param file_path: the absolute path of the file.
|
||||
"""
|
||||
|
||||
if os.path.exists(absolute_file_path):
|
||||
return os.path.getsize(absolute_file_path)
|
||||
|
||||
else:
|
||||
raise FilesystemNotFoundException()
|
||||
|
||||
class FilesystemNotFoundException(Exception):
|
||||
|
||||
"""
|
||||
Element not found in the filesystem.
|
||||
"""
|
||||
|
||||
pass
|
1
src/piracyshield_component/log/__init__.py
Normal file
1
src/piracyshield_component/log/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
|
68
src/piracyshield_component/log/formatter.py
Normal file
68
src/piracyshield_component/log/formatter.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
import logging
|
||||
|
||||
class ColorFormatter(logging.Formatter):
|
||||
|
||||
"""
|
||||
Manages color output for the console.
|
||||
"""
|
||||
|
||||
def __init__(self, fmt = None, datefmt = None, style = '%'):
|
||||
"""
|
||||
Defines the final format of the message.
|
||||
|
||||
# TODO: this should be better explained.
|
||||
|
||||
Further info can be found on the official documentation:
|
||||
- https://docs.python.org/3/library/logging.html#formatter-objects
|
||||
|
||||
:param fmt: optional format of the string.
|
||||
:param datefmt: optional date format of the string.
|
||||
:param style: formatting string style.
|
||||
"""
|
||||
|
||||
super().__init__(fmt = fmt, datefmt = datefmt, style = style)
|
||||
|
||||
self.color_map = {
|
||||
logging.DEBUG: Color.DEBUG,
|
||||
logging.INFO: Color.INFO,
|
||||
logging.WARNING: Color.WARNING,
|
||||
logging.ERROR: Color.ERROR,
|
||||
logging.CRITICAL: Color.CRITICAL,
|
||||
}
|
||||
|
||||
self.reset_color = '\033[0m'
|
||||
|
||||
def format(self, record) -> str:
|
||||
"""
|
||||
Applies formatting to the passed string.
|
||||
|
||||
:param record: string to be formatted.
|
||||
:return: the formatted string.
|
||||
"""
|
||||
|
||||
levelname = record.levelname
|
||||
|
||||
message = super().format(record)
|
||||
|
||||
color = self.color_map.get(record.levelno)
|
||||
|
||||
if color:
|
||||
message = color + message + self.reset_color
|
||||
|
||||
return message
|
||||
|
||||
class Color:
|
||||
|
||||
"""
|
||||
Default formatting colors for each logging level.
|
||||
"""
|
||||
|
||||
DEBUG = '\033[1;37m'
|
||||
|
||||
INFO = '\033[1;32m'
|
||||
|
||||
WARNING = '\033[1;33m'
|
||||
|
||||
ERROR = '\033[1;31m'
|
||||
|
||||
CRITICAL = '\033[1;31m'
|
172
src/piracyshield_component/log/logger.py
Normal file
172
src/piracyshield_component/log/logger.py
Normal file
|
@ -0,0 +1,172 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from piracyshield_component.environment import Environment
|
||||
from piracyshield_component.config import Config
|
||||
|
||||
from piracyshield_component.log.output.console import ConsoleOutput
|
||||
from piracyshield_component.log.output.filesystem import FilesystemOutput
|
||||
|
||||
import logging
|
||||
|
||||
class Logger:
|
||||
|
||||
"""
|
||||
Application logging management class.
|
||||
"""
|
||||
|
||||
name = None
|
||||
|
||||
logger = None
|
||||
|
||||
general_config = None
|
||||
|
||||
console_config = None
|
||||
|
||||
filesystem_config = None
|
||||
|
||||
def __init__(self, name: str):
|
||||
"""
|
||||
Initializes a new logging instance using a nem as identifier.
|
||||
|
||||
:param name: a valid string representing the type of service that wants to log the operations.
|
||||
"""
|
||||
|
||||
self._prepare_configs()
|
||||
|
||||
self.name = name
|
||||
|
||||
self.logger = logging.getLogger(name)
|
||||
|
||||
self.logger.setLevel(self._get_level(self.general_config['level']))
|
||||
|
||||
self._register_handlers()
|
||||
|
||||
def _register_handlers(self) -> None:
|
||||
"""
|
||||
Registers different kind of handlers.
|
||||
|
||||
Supporterd outputs:
|
||||
- console
|
||||
- filesystem
|
||||
"""
|
||||
|
||||
if self.console_config['enabled'] == True and not self._has_handler(ConsoleOutput):
|
||||
self.logger.addHandler(
|
||||
ConsoleOutput(
|
||||
format_syntax = self.general_config['format'],
|
||||
colorize = self.console_config['colorize']
|
||||
)
|
||||
)
|
||||
|
||||
if self.filesystem_config['enabled'] == True and not self._has_handler(FilesystemOutput):
|
||||
self.logger.addHandler(
|
||||
FilesystemOutput(
|
||||
name = self.name,
|
||||
path = self.filesystem_config['path'],
|
||||
format_syntax = self.general_config['format']
|
||||
)
|
||||
)
|
||||
|
||||
def _has_handler(self, instance) -> bool:
|
||||
"""
|
||||
Avoids duplications of the already added handlers.
|
||||
|
||||
:return: true or false if the handler is already present or not.
|
||||
"""
|
||||
|
||||
return any(isinstance(h, instance) for h in self.logger.handlers)
|
||||
|
||||
def _get_level(self, level: str) -> int | Exception:
|
||||
"""
|
||||
Handles the level based on a fixed list of options.
|
||||
|
||||
:return: the real integer value of the logging levels.
|
||||
"""
|
||||
|
||||
match level:
|
||||
case 'debug':
|
||||
return logging.DEBUG
|
||||
|
||||
case 'info':
|
||||
return logging.INFO
|
||||
|
||||
case 'warning':
|
||||
return logging.WARNING
|
||||
|
||||
case 'error':
|
||||
return logging.ERROR
|
||||
|
||||
case 'critical':
|
||||
return logging.CRITICAL
|
||||
|
||||
case _:
|
||||
raise LoggerLevelNotFound()
|
||||
|
||||
def debug(self, message: str) -> None:
|
||||
"""
|
||||
Handles debugging messages.
|
||||
Logs minor informations useful for debugging.
|
||||
|
||||
:param message: the string we want to log.
|
||||
"""
|
||||
|
||||
self.logger.debug(message)
|
||||
|
||||
def info(self, message: str) -> None:
|
||||
"""
|
||||
Handles info messages.
|
||||
Logs ordnary operations.
|
||||
|
||||
:param message: the string we want to log.
|
||||
"""
|
||||
|
||||
self.logger.info(message)
|
||||
|
||||
def warning(self, message: str) -> None:
|
||||
"""
|
||||
Handles warning messages.
|
||||
Logs issues of any kind.
|
||||
|
||||
:param message: the string we want to log.
|
||||
"""
|
||||
|
||||
self.logger.warning(message)
|
||||
|
||||
def error(self, message: str) -> None:
|
||||
"""
|
||||
Handles error messages.
|
||||
Logs recoverable errors.
|
||||
|
||||
:param message: the string we want to log.
|
||||
"""
|
||||
|
||||
self.logger.error(message)
|
||||
|
||||
def critical(self, message: str) -> None:
|
||||
"""
|
||||
Handles critical messages.
|
||||
This should be considered a step before quitting the application.
|
||||
|
||||
:param message: the string we want to log.
|
||||
"""
|
||||
|
||||
self.logger.critical(message)
|
||||
|
||||
def _prepare_configs(self):
|
||||
"""
|
||||
Register configurations.
|
||||
"""
|
||||
|
||||
self.general_config = Config('logger').get('general')
|
||||
|
||||
self.console_config = Config('logger').get('console')
|
||||
|
||||
self.filesystem_config = Config('logger').get('filesystem')
|
||||
|
||||
class LoggerLevelNotFound(Exception):
|
||||
|
||||
"""
|
||||
Log level specified is non valid.
|
||||
"""
|
||||
|
||||
pass
|
1
src/piracyshield_component/log/output/__init__.py
Normal file
1
src/piracyshield_component/log/output/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
|
30
src/piracyshield_component/log/output/console.py
Normal file
30
src/piracyshield_component/log/output/console.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
import logging
|
||||
import sys
|
||||
|
||||
from piracyshield_component.log.formatter import ColorFormatter
|
||||
|
||||
class ConsoleOutput(logging.StreamHandler):
|
||||
|
||||
"""
|
||||
Manages the console output.
|
||||
"""
|
||||
|
||||
def __init__(self, format_syntax: str, colorize: bool = True):
|
||||
"""
|
||||
Sets the output options.
|
||||
|
||||
:param format_syntax: the string format to apply.
|
||||
:param colorize: whether to apply or not colorization of the output.
|
||||
"""
|
||||
|
||||
super().__init__(stream = sys.stdout)
|
||||
|
||||
formatter = None
|
||||
|
||||
if colorize == True:
|
||||
formatter = ColorFormatter(format_syntax)
|
||||
|
||||
else:
|
||||
formatter = logging.Formatter(format_syntax)
|
||||
|
||||
self.setFormatter(formatter)
|
37
src/piracyshield_component/log/output/filesystem.py
Normal file
37
src/piracyshield_component/log/output/filesystem.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
from piracyshield_component.utils.time import Time
|
||||
|
||||
import logging
|
||||
|
||||
class FilesystemOutput(logging.FileHandler):
|
||||
|
||||
"""
|
||||
Manages filesystem output.
|
||||
"""
|
||||
|
||||
FORMAT = "%d-%m-%Y"
|
||||
|
||||
def __init__(self, name: str, path: str, format_syntax: str):
|
||||
"""
|
||||
Sets the output options.
|
||||
|
||||
:param name: the filename of the log.
|
||||
:param path: absolute path of the logging directory.
|
||||
:param format_syntax: formatting string style.
|
||||
"""
|
||||
|
||||
super().__init__(self._get_filename(name, path))
|
||||
|
||||
self.setFormatter(logging.Formatter(format_syntax))
|
||||
|
||||
def _get_filename(self, name: str, path: str) -> str:
|
||||
"""
|
||||
Resolves the file position.
|
||||
|
||||
:param name: the filename of the log.
|
||||
:param path: absolute path of the log.
|
||||
:return: absolute filename path.
|
||||
"""
|
||||
|
||||
now = Time.now_format(self.FORMAT)
|
||||
|
||||
return f'/{path}/{now}-{name}.log'
|
1
src/piracyshield_component/security/__init__.py
Normal file
1
src/piracyshield_component/security/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
|
90
src/piracyshield_component/security/checksum.py
Normal file
90
src/piracyshield_component/security/checksum.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
|
||||
class Checksum:
|
||||
|
||||
"""
|
||||
Helper class for checksum calculations.
|
||||
"""
|
||||
|
||||
def from_string(self, algorithm: str, string: str) -> str:
|
||||
"""
|
||||
Calculates checksum of a given string.
|
||||
|
||||
:param str: the string to process.
|
||||
:return: checksum string.
|
||||
"""
|
||||
|
||||
try:
|
||||
instance = hashlib.new(algorithm)
|
||||
|
||||
# convert the string into bytes before handling
|
||||
instance.update(bytes(string, 'utf-8'))
|
||||
|
||||
except ValueError:
|
||||
raise ChecksumParameterException()
|
||||
|
||||
except UnicodeEncodeError:
|
||||
raise ChecksumUnicodeException()
|
||||
|
||||
except AttributeError:
|
||||
raise ChecksumParameterException()
|
||||
|
||||
return instance.hexdigest()
|
||||
|
||||
def from_file(self, algorithm: str, file_path: str) -> str:
|
||||
"""
|
||||
Calculates checksum of a given file.
|
||||
|
||||
:param str: the absolute file path.
|
||||
:return: checksum string.
|
||||
"""
|
||||
|
||||
try:
|
||||
instance = hashlib.new(algorithm)
|
||||
|
||||
with open(file_path, 'rb') as handle:
|
||||
while True:
|
||||
data = handle.read(65536)
|
||||
|
||||
# EOF
|
||||
if not data:
|
||||
break
|
||||
|
||||
instance.update(data)
|
||||
|
||||
return instance.hexdigest()
|
||||
|
||||
except ValueError:
|
||||
raise ChecksumParameterException()
|
||||
|
||||
except AttributeError:
|
||||
raise ChecksumParameterException()
|
||||
|
||||
except:
|
||||
raise ChecksumCalculationException()
|
||||
|
||||
class ChecksumCalculationException(Exception):
|
||||
|
||||
"""
|
||||
Cannot process the file.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class ChecksumUnicodeException(Exception):
|
||||
|
||||
"""
|
||||
Cannot encode the data to UTF-8.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class ChecksumParameterException(Exception):
|
||||
|
||||
"""
|
||||
Most likely not a string.
|
||||
"""
|
||||
|
||||
pass
|
23
src/piracyshield_component/security/filter.py
Normal file
23
src/piracyshield_component/security/filter.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
import re
|
||||
|
||||
class Filter:
|
||||
|
||||
"""
|
||||
Generic input filter utility.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def strip(value: str, character: str = ' '):
|
||||
"""
|
||||
Strips the character from the start and the end of a string.
|
||||
"""
|
||||
|
||||
return value.strip(character)
|
||||
|
||||
@staticmethod
|
||||
def remove_whitespace(value: str):
|
||||
"""
|
||||
Removes whitespaces from a string.
|
||||
"""
|
||||
|
||||
return re.sub(r'\s+', '', value)
|
78
src/piracyshield_component/security/hasher.py
Normal file
78
src/piracyshield_component/security/hasher.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argon2
|
||||
|
||||
class Hasher:
|
||||
|
||||
"""
|
||||
Helper class to encode and verify strings. Used as password encoder.
|
||||
"""
|
||||
|
||||
hasher_instance = None
|
||||
|
||||
def __init__(self, time_cost: int, memory_cost: int, parallelism: int, hash_length: int, salt_length: int):
|
||||
"""
|
||||
Initialize the instance and sets the options.
|
||||
|
||||
!! THESE PARAMETERS SHOULD NOT BE CHANGED AFTER THE FIRST USAGE !!
|
||||
|
||||
:param time_cost: execution time cost of the hashing operation.
|
||||
:param memory_cost: memory usage of the hasing operation.
|
||||
:param parallelism: quantity of parallel threads used.
|
||||
:param hash_length: length of the hash.
|
||||
:param salt_length: length of the generated salt.
|
||||
"""
|
||||
|
||||
self.hasher_instance = argon2.PasswordHasher(
|
||||
time_cost = time_cost,
|
||||
memory_cost = memory_cost,
|
||||
parallelism = parallelism,
|
||||
hash_len = hash_length,
|
||||
salt_len = salt_length,
|
||||
type = argon2.low_level.Type.ID
|
||||
)
|
||||
|
||||
def encode_string(self, string: str) -> str:
|
||||
"""
|
||||
Encode the string as an argon2 hash.
|
||||
|
||||
:param str: the string to hash.
|
||||
:return: the encoded argon2 hash.
|
||||
"""
|
||||
|
||||
try:
|
||||
# generate the hash
|
||||
hashed_string = self.hasher_instance.hash(string)
|
||||
|
||||
except argon2.exceptions.HashingError:
|
||||
raise HasherGenericException()
|
||||
|
||||
return hashed_string
|
||||
|
||||
def verify_hash(self, string: str, hashed_string: str) -> bool | Exception:
|
||||
"""
|
||||
Verify the string against the hash.
|
||||
|
||||
:param string: the string to verify.
|
||||
:param hashed_string: the argon2 hash to verify against.
|
||||
:return: True if the plaintext password matches the hash, otherwise False.
|
||||
"""
|
||||
|
||||
try:
|
||||
return self.hasher_instance.verify(hashed_string, string)
|
||||
|
||||
except argon2.exceptions.VerificationError:
|
||||
raise HasherNonValidException()
|
||||
|
||||
|
||||
class HasherGenericException(Exception):
|
||||
|
||||
"""
|
||||
Raised during the encoding of the plain text string.
|
||||
"""
|
||||
|
||||
class HasherNonValidException(Exception):
|
||||
|
||||
"""
|
||||
Raised during the verification procedure, if the string is not matching the hash.
|
||||
"""
|
33
src/piracyshield_component/security/identifier.py
Normal file
33
src/piracyshield_component/security/identifier.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
import secrets
|
||||
|
||||
class Identifier:
|
||||
|
||||
"""
|
||||
Class for identifiers generation.
|
||||
"""
|
||||
|
||||
def generate(self) -> str:
|
||||
"""
|
||||
Generates RFC 4122 compliant ids.
|
||||
|
||||
:return: a string based on the UUIDv4, minus the dashes.
|
||||
"""
|
||||
|
||||
string = uuid.uuid4()
|
||||
|
||||
# converts to string and removes dashes producing a 32 characters value
|
||||
return string.hex
|
||||
|
||||
def generate_short_unsafe(self, length: int = 8) -> str:
|
||||
"""
|
||||
Generates a short handy string to be used in situations where we need to add a prefix or something like that.
|
||||
Does not guarantees the uniqueness, but that's not an issue for this kind of identifier usage.
|
||||
|
||||
:param length: custom length of the string.
|
||||
:return: an alphanumeric string.
|
||||
"""
|
||||
|
||||
return secrets.token_hex(length)
|
167
src/piracyshield_component/security/token.py
Normal file
167
src/piracyshield_component/security/token.py
Normal file
|
@ -0,0 +1,167 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from piracyshield_component.utils.time import Time, TimeValueException
|
||||
|
||||
import jwt
|
||||
import datetime
|
||||
|
||||
class JWTToken:
|
||||
|
||||
"""
|
||||
Class for the management of the JWT token.
|
||||
"""
|
||||
|
||||
secret_key = None
|
||||
|
||||
algorithm = None
|
||||
|
||||
expiration_time = None
|
||||
|
||||
def __init__(self, access_secret_key: str, refresh_secret_key: str, access_expiration_time: int, refresh_expiration_time: int, algorithm: str):
|
||||
"""
|
||||
Sets the options.
|
||||
|
||||
!! THESE PARAMETERS SHOULD NOT BE CHANGED AFTER THE FIRST USAGE !!
|
||||
|
||||
:param access_secret_key: the secret key for the access token generation.
|
||||
:param access_secret_key: the secret key for the refresh token generation.
|
||||
:param access_expiration_time: time duration for the access token, set in seconds.
|
||||
:param access_expiration_time: time duration for the refresh token, set in seconds.
|
||||
:param algorithm: algorithm for encoding and decoding (default: HS256).
|
||||
"""
|
||||
|
||||
self.access_secret_key = access_secret_key
|
||||
|
||||
self.refresh_secret_key = refresh_secret_key
|
||||
|
||||
self.access_expiration_time = access_expiration_time
|
||||
|
||||
self.refresh_expiration_time = refresh_expiration_time
|
||||
|
||||
self.algorithm = algorithm
|
||||
|
||||
def generate_access_token(self, payload: dict) -> str | Exception:
|
||||
"""
|
||||
Handles the generation of an access token using the configuration data.
|
||||
It's a short lived token that needs to be refreshed periodically.
|
||||
|
||||
:param payload: dictionary containing the data to encode.
|
||||
:return: a JWT token signed with the secret key and encoded in base64.
|
||||
"""
|
||||
|
||||
return self.generate_token(payload, self.access_secret_key, self.access_expiration_time)
|
||||
|
||||
def generate_refresh_token(self, payload: dict) -> str | Exception:
|
||||
"""
|
||||
Handles the generation of a refresh token using the configuration data.
|
||||
This is intended as a long lived token, used to periodically refresh the access token.
|
||||
|
||||
:param payload: dictionary containing the data to encode.
|
||||
:return: a JWT token signed with the secret key and encoded in base64.
|
||||
"""
|
||||
|
||||
return self.generate_token(payload, self.refresh_secret_key, self.refresh_expiration_time)
|
||||
|
||||
def generate_token(self, payload: dict, secret_key: str, period: int) -> str | Exception:
|
||||
"""
|
||||
Generate a JWT token with the given payload.
|
||||
|
||||
:param payload: a dictionary containing the data to be encoded in the JWT token.
|
||||
:return: a JWT token signed with the secret key and encoded in base64.
|
||||
"""
|
||||
|
||||
# calculate the expiration time
|
||||
now = Time.now()
|
||||
|
||||
# add the issued time
|
||||
payload['iat'] = now
|
||||
|
||||
# add the expiration time
|
||||
payload['exp'] = now + datetime.timedelta(seconds = period)
|
||||
|
||||
try:
|
||||
# generate the final token
|
||||
token = jwt.encode(payload, secret_key, algorithm = self.algorithm)
|
||||
|
||||
return token
|
||||
|
||||
# TODO: get a more granular error reporting here
|
||||
except Exception as e:
|
||||
raise JWTTokenGenericException()
|
||||
|
||||
def verify_access_token(self, token: any) -> dict | Exception:
|
||||
"""
|
||||
Verifies the access token with the configured access secret key.
|
||||
|
||||
:param token: a valid JWT access token.
|
||||
:return: the original payload.
|
||||
"""
|
||||
|
||||
return self.verify_token(token, self.access_secret_key)
|
||||
|
||||
def verify_refresh_token(self, token: any) -> dict | Exception:
|
||||
"""
|
||||
Verifies the refresh token with the configured refresh secret key.
|
||||
|
||||
:param token: a valid JWT access token.
|
||||
:return: the original payload.
|
||||
"""
|
||||
|
||||
return self.verify_token(token, self.refresh_secret_key)
|
||||
|
||||
def verify_token(self, token: any, secret_key: str) -> dict | Exception:
|
||||
"""
|
||||
Verify the given JWT token and return the decoded payload if the token is valid.
|
||||
|
||||
:param token: a JWT token encoded in base64.
|
||||
:return: the decoded payload if the token is valid, otherwise an exception will be raised.
|
||||
"""
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, secret_key, algorithms = [self.algorithm])
|
||||
|
||||
# check if it's expired
|
||||
try:
|
||||
if Time.timestamp_to_datetime(payload['exp']) < Time.now():
|
||||
raise JWTTokenExpiredException()
|
||||
|
||||
except TimeValueException:
|
||||
raise JWTTokenNonValidException()
|
||||
|
||||
return payload
|
||||
|
||||
except jwt.exceptions.ExpiredSignatureError:
|
||||
raise JWTTokenExpiredException()
|
||||
|
||||
# TODO: should deal with more explicit exceptions on our side
|
||||
except (
|
||||
jwt.exceptions.InvalidTokenError,
|
||||
jwt.exceptions.InvalidSignatureError,
|
||||
jwt.exceptions.DecodeError,
|
||||
jwt.exceptions.InvalidAlgorithmError
|
||||
):
|
||||
raise JWTTokenNonValidException()
|
||||
|
||||
class JWTTokenGenericException(Exception):
|
||||
|
||||
"""
|
||||
Generic exception as a last option.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class JWTTokenExpiredException(Exception):
|
||||
|
||||
"""
|
||||
Exception raised on token expired time.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class JWTTokenNonValidException(Exception):
|
||||
|
||||
"""
|
||||
Raised during the token verification, if there's no valid match.
|
||||
"""
|
||||
|
||||
pass
|
1
src/piracyshield_component/utils/__init__.py
Normal file
1
src/piracyshield_component/utils/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
|
122
src/piracyshield_component/utils/time.py
Normal file
122
src/piracyshield_component/utils/time.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
import datetime
|
||||
import time
|
||||
|
||||
import pytz
|
||||
|
||||
class Time:
|
||||
|
||||
"""
|
||||
Handy utility for date and/or time generation.
|
||||
"""
|
||||
|
||||
TIMEZONE = 'Europe/Rome'
|
||||
|
||||
@staticmethod
|
||||
def now() -> str:
|
||||
"""
|
||||
Returns the current date and time.
|
||||
|
||||
:return: the current date and time as a string.
|
||||
"""
|
||||
|
||||
timezone = pytz.timezone(Time.TIMEZONE)
|
||||
|
||||
utc = datetime.datetime.now()
|
||||
|
||||
return utc.astimezone(timezone)
|
||||
|
||||
def now_format(datetime_format: str) -> str:
|
||||
"""
|
||||
Returns the current date and time using a custom format.
|
||||
|
||||
:return: the current date and time as a string.
|
||||
"""
|
||||
|
||||
now = Time.now()
|
||||
|
||||
return now.strftime(datetime_format)
|
||||
|
||||
@staticmethod
|
||||
def now_iso8601() -> str:
|
||||
"""
|
||||
Returns the current date and time in ISO 8601 format.
|
||||
|
||||
:return: the current date and time as a string.
|
||||
"""
|
||||
|
||||
now = Time.now()
|
||||
|
||||
return now.isoformat()
|
||||
|
||||
@staticmethod
|
||||
def timestamp():
|
||||
"""
|
||||
Returns the current Unix timestamp as an integer.
|
||||
|
||||
:return: the current Unix timestamp.
|
||||
"""
|
||||
|
||||
now = Time.now()
|
||||
|
||||
return int(now.timestamp())
|
||||
|
||||
@staticmethod
|
||||
def timestamp_to_datetime(timestamp: int) -> bool | Exception:
|
||||
try:
|
||||
# convert the timestamp to a datetime with timezone information
|
||||
return datetime.datetime.fromtimestamp(timestamp, tz = pytz.timezone(Time.TIMEZONE))
|
||||
|
||||
except ValueError:
|
||||
raise TimeValueException()
|
||||
|
||||
@staticmethod
|
||||
def is_expired(date: int, expiration_time: int) -> bool:
|
||||
"""
|
||||
Checks if the provided date is expired.
|
||||
|
||||
:param date: a date in ISO8601 format.
|
||||
:param expiration_time: the distance in seconds for the date to expiry.
|
||||
:return: true if expired.
|
||||
"""
|
||||
|
||||
# convert the date string to a datetime object
|
||||
datetime_object = datetime.datetime.fromisoformat(date)
|
||||
|
||||
# sum the expired time to the date
|
||||
converted_date = datetime_object + datetime.timedelta(seconds = expiration_time)
|
||||
|
||||
current_date = Time.now()
|
||||
|
||||
return current_date > converted_date
|
||||
|
||||
def is_valid_iso8601(date: int) -> bool | Exception:
|
||||
"""
|
||||
Validates the date against the ISO8601 format.
|
||||
|
||||
:param date: a date in ISO8601 format.
|
||||
:return: true if the date format is correct.
|
||||
"""
|
||||
|
||||
try:
|
||||
datetime.datetime.fromisoformat(date)
|
||||
|
||||
return True
|
||||
|
||||
except:
|
||||
raise TimeFormatException()
|
||||
|
||||
class TimeValueException(Exception):
|
||||
|
||||
"""
|
||||
Wrong input.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class TimeFormatException(Exception):
|
||||
|
||||
"""
|
||||
Wrong date format.
|
||||
"""
|
||||
|
||||
pass
|
1
src/piracyshield_component/validation/__init__.py
Normal file
1
src/piracyshield_component/validation/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
|
33
src/piracyshield_component/validation/rule.py
Normal file
33
src/piracyshield_component/validation/rule.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
class Rule(ABC):
|
||||
|
||||
"""
|
||||
Basic rule class.
|
||||
"""
|
||||
|
||||
errors = []
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize errors list.
|
||||
"""
|
||||
|
||||
self.errors = []
|
||||
|
||||
@abstractmethod
|
||||
def __call__(self):
|
||||
"""
|
||||
Method invoked for the rule processing.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def register_error(self, message) -> None:
|
||||
"""
|
||||
Registers all of the errors occurred during the validation.
|
||||
|
||||
:param message: error message.
|
||||
"""
|
||||
|
||||
self.errors.append(message)
|
1
src/piracyshield_component/validation/rules/__init__.py
Normal file
1
src/piracyshield_component/validation/rules/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
|
31
src/piracyshield_component/validation/rules/as_code.py
Normal file
31
src/piracyshield_component/validation/rules/as_code.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from piracyshield_component.validation.rule import Rule
|
||||
|
||||
import re
|
||||
|
||||
class ASCode(Rule):
|
||||
|
||||
"""
|
||||
Rule that checks for a valid AS code.
|
||||
"""
|
||||
|
||||
message = 'AS code not valid'
|
||||
|
||||
expression = r'^(AS)?[0-9]{1,10}$'
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize parent __init__.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
def __call__(self, value: str) -> None:
|
||||
"""
|
||||
Checks the validity of the AS code.
|
||||
It's flexible enough to allow also non AS/A prefix strings.
|
||||
|
||||
:param value: a valid string.
|
||||
"""
|
||||
|
||||
if not re.search(self.expression, value):
|
||||
self.register_error(self.message)
|
30
src/piracyshield_component/validation/rules/dda.py
Normal file
30
src/piracyshield_component/validation/rules/dda.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from piracyshield_component.validation.rule import Rule
|
||||
|
||||
import re
|
||||
|
||||
class DDA(Rule):
|
||||
|
||||
"""
|
||||
Rule that checks for a valid DDA identifier.
|
||||
"""
|
||||
|
||||
message = 'DDA identifier not valid'
|
||||
|
||||
expression = r'^[0-9]{3}\/[0-9]{2}\/DDA$'
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize parent __init__.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
def __call__(self, value: str) -> None:
|
||||
"""
|
||||
Checks the validity of the DDA identifier.
|
||||
|
||||
:param value: a valid string.
|
||||
"""
|
||||
|
||||
if not re.search(self.expression, value):
|
||||
self.register_error(self.message)
|
31
src/piracyshield_component/validation/rules/email.py
Normal file
31
src/piracyshield_component/validation/rules/email.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from piracyshield_component.validation.rule import Rule
|
||||
|
||||
import re
|
||||
|
||||
class Email(Rule):
|
||||
|
||||
"""
|
||||
Rule that checks for a valid e-mail address.
|
||||
"""
|
||||
|
||||
message = 'The value must be a valid e-mail address'
|
||||
|
||||
# support also second level domains
|
||||
expression = r'^[a-z0-9]+[\._]?[a-z0-9]+[@]([a-z\-]+.)?\w+[.]\w{2,18}$'
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize parent __init__.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
def __call__(self, value: any) -> None:
|
||||
"""
|
||||
Matches our regular expression against the given value.
|
||||
|
||||
param: value: a valid string.
|
||||
"""
|
||||
|
||||
if not re.search(self.expression, value):
|
||||
self.register_error(self.message)
|
30
src/piracyshield_component/validation/rules/fqdn.py
Normal file
30
src/piracyshield_component/validation/rules/fqdn.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from piracyshield_component.validation.rule import Rule
|
||||
|
||||
import re
|
||||
|
||||
class FQDN(Rule):
|
||||
|
||||
"""
|
||||
Rule that checks for a valid fully qualified domain name.
|
||||
"""
|
||||
|
||||
message = 'FQDN not valid'
|
||||
|
||||
expression = r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize parent __init__.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
def __call__(self, value: any) -> None:
|
||||
"""
|
||||
Checks for a valid FQDN.
|
||||
|
||||
:param value: a valid string.
|
||||
"""
|
||||
|
||||
if not re.search(self.expression, value):
|
||||
self.register_error(self.message)
|
54
src/piracyshield_component/validation/rules/ipv4.py
Normal file
54
src/piracyshield_component/validation/rules/ipv4.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
from piracyshield_component.validation.rule import Rule
|
||||
|
||||
class IPv4(Rule):
|
||||
|
||||
"""
|
||||
Rule that checks for a valid IPv4.
|
||||
"""
|
||||
|
||||
octets_message = 'IPv4 not valid, expecting four octects, got {}'
|
||||
|
||||
octets_digits_message = 'IPv4 not valid, expecting four octets of digits'
|
||||
|
||||
octets_length_message = 'IPv4 not valid, one or more octet(s) too long'
|
||||
|
||||
octets_digits_size_message = 'IPv4 not valid, expecting digits from 0 to 255'
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize parent __init__.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
def __call__(self, value: str) -> None:
|
||||
"""
|
||||
Checks the validity of the passed string.
|
||||
Instead of relying on regular expression, is it better to check the single octects.
|
||||
This also allow for better error reporting.
|
||||
|
||||
:param value: a valid string.
|
||||
"""
|
||||
|
||||
octets = value.split('.')
|
||||
|
||||
octets_size = len(octets)
|
||||
|
||||
# we're expecting 4 octects
|
||||
if octets_size != 4:
|
||||
self.register_error(self.octets_message.format(octets_size))
|
||||
|
||||
for octet in octets:
|
||||
single_octet_size = len(octet)
|
||||
|
||||
# each octect must be an integer
|
||||
if not octet.isdigit():
|
||||
self.register_error(self.octets_digits_message)
|
||||
|
||||
# with a maximum length of 3
|
||||
if single_octet_size > 3:
|
||||
self.register_error(self.octets_length_message)
|
||||
|
||||
# between 0 and 255
|
||||
if single_octet_size < 0 or single_octet_size > 255:
|
||||
self.register_error(self.octets_digits_size_message)
|
60
src/piracyshield_component/validation/rules/ipv6.py
Normal file
60
src/piracyshield_component/validation/rules/ipv6.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
from piracyshield_component.validation.rule import Rule
|
||||
|
||||
class IPv6(Rule):
|
||||
|
||||
"""
|
||||
Rule that checks for a valid IPv6.
|
||||
"""
|
||||
|
||||
hextets_message = 'IPv6 not valid, expecting eight hextets, got {}'
|
||||
|
||||
hextets_digits_message = 'IPv6 not valid, expecting eight hextets of hexadecimal digits'
|
||||
|
||||
hextets_length_message = 'IPv6 not valid, one or more hextet(s) too long'
|
||||
|
||||
hextets_digits_size_message = 'IPv6 not valid, expecting hexadecimal digits from 0 to FFFF'
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize parent __init__.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
def __call__(self, value: str) -> None:
|
||||
"""
|
||||
Checks the validity of the passed string.
|
||||
Instead of relying on regular expression, it's better to check the individual hextets.
|
||||
This also allows for better error reporting.
|
||||
|
||||
:param value: a valid string.
|
||||
"""
|
||||
|
||||
hextets = value.split(':')
|
||||
|
||||
hextets_size = len(hextets)
|
||||
|
||||
# we're expecting 8 hextets
|
||||
if hextets_size != 8:
|
||||
self.register_error(self.hextets_message.format(hextets_size))
|
||||
|
||||
for hextet in hextets:
|
||||
single_hextet_size = len(hextet)
|
||||
|
||||
# each hextet must be composed of hexadecimal digits
|
||||
if not all(c in '0123456789ABCDEFabcdef' for c in hextet):
|
||||
self.register_error(self.hextets_digits_message)
|
||||
|
||||
# with a maximum length of 4
|
||||
if single_hextet_size > 4:
|
||||
self.register_error(self.hextets_length_message)
|
||||
|
||||
try:
|
||||
# convert the hextet to an integer in base 16
|
||||
int_value = int(hextet, 16)
|
||||
|
||||
# check if the integer value is within the valid range (0~0xFFFF)
|
||||
if not (0 <= int_value <= 0xFFFF):
|
||||
self.register_error(self.hextets_digits_size_message)
|
||||
|
||||
except ValueError:
|
||||
self.register_error(self.hextets_digits_size_message)
|
37
src/piracyshield_component/validation/rules/length.py
Normal file
37
src/piracyshield_component/validation/rules/length.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
from piracyshield_component.validation.rule import Rule
|
||||
|
||||
class Length(Rule):
|
||||
|
||||
"""
|
||||
Rule that checks the length of a value.
|
||||
"""
|
||||
|
||||
message = 'The value must be comprised between {} and {} characters'
|
||||
|
||||
minimum = None
|
||||
|
||||
maximum = None
|
||||
|
||||
def __init__(self, minimum: int, maximum: int):
|
||||
"""
|
||||
Initialize parent __init__ and set the minimum and maximum allowed length.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.minimum = minimum
|
||||
|
||||
self.maximum = maximum
|
||||
|
||||
def __call__(self, value: any) -> None:
|
||||
"""
|
||||
Check if our value has the correct specified length.
|
||||
|
||||
param: value: a valid string.
|
||||
"""
|
||||
|
||||
# TODO: might want to handle this in depth.
|
||||
length = len(value) if isinstance(value, str) else len(str(value))
|
||||
|
||||
if length < self.minimum or length > self.maximum:
|
||||
self.register_error(self.message.format(self.minimum, self.maximum))
|
36
src/piracyshield_component/validation/rules/required.py
Normal file
36
src/piracyshield_component/validation/rules/required.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from piracyshield_component.validation.rule import Rule
|
||||
|
||||
class Required(Rule):
|
||||
|
||||
"""
|
||||
Rule that checks if a string is empty.
|
||||
"""
|
||||
|
||||
message = 'A valid string is required'
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize parent __init__.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
def __call__(self, value: any) -> None:
|
||||
"""
|
||||
Stores and executes the code.
|
||||
|
||||
param: value: any value.
|
||||
"""
|
||||
|
||||
self.value = value
|
||||
|
||||
self.is_empty()
|
||||
|
||||
def is_empty(self) -> None:
|
||||
"""
|
||||
Check if the string is empty.
|
||||
No filters are applied in this phase as we want to avoid input and output misalignements.
|
||||
"""
|
||||
|
||||
if not self.value:
|
||||
self.register_error(self.message)
|
55
src/piracyshield_component/validation/rules/string.py
Normal file
55
src/piracyshield_component/validation/rules/string.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
from piracyshield_component.validation.rule import Rule
|
||||
|
||||
class String(Rule):
|
||||
|
||||
"""
|
||||
Rule that checks if a string has valid characters.
|
||||
"""
|
||||
|
||||
message = 'The value must be a string containing letters, numbers'
|
||||
|
||||
message_string = 'The value must be a valid string'
|
||||
|
||||
characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
|
||||
def __init__(self, allowed: str = ''):
|
||||
"""
|
||||
Initialize the parent __init__.
|
||||
|
||||
TODO: we might want to extend this to allow a full customization of the characters list
|
||||
(ie. when we don't really want all the characters spectrum)
|
||||
|
||||
:param allowed: a string containing allowed characters other than the default ones.
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
# join the allowed characters to our main list and convert them to a unique values dictionary
|
||||
self.characters = set(''.join([allowed, self.characters]))
|
||||
|
||||
def __call__(self, value: any) -> None:
|
||||
"""
|
||||
Stores and executes the code.
|
||||
|
||||
param: value: a valid string.
|
||||
"""
|
||||
|
||||
self.value = value
|
||||
|
||||
self.has_characters()
|
||||
|
||||
def has_characters(self) -> None:
|
||||
"""
|
||||
Checks if the value is a valid string instance and if the characters are allowed.
|
||||
"""
|
||||
|
||||
if not isinstance(self.value, str):
|
||||
self.register_error(self.message_string)
|
||||
|
||||
else:
|
||||
# converts the value into a dictionary with no duplicates
|
||||
exploded_value = set(self.value)
|
||||
|
||||
# check if the characters appear in our list
|
||||
if exploded_value.issubset(self.characters) == False:
|
||||
self.register_error(self.message)
|
69
src/piracyshield_component/validation/validator.py
Normal file
69
src/piracyshield_component/validation/validator.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from piracyshield_component.validation.rule import Rule
|
||||
|
||||
class Validator:
|
||||
|
||||
"""
|
||||
Validation utility class.
|
||||
"""
|
||||
|
||||
value = None
|
||||
|
||||
rules = None
|
||||
|
||||
errors = []
|
||||
|
||||
def __init__(self, value, rules: list):
|
||||
"""
|
||||
Sets the options.
|
||||
|
||||
:param value: the value we want to analyze.
|
||||
:param rules: a list of validation rule classes.
|
||||
"""
|
||||
|
||||
self.value = value
|
||||
|
||||
self.rules = rules
|
||||
|
||||
self.errors = []
|
||||
|
||||
self._check_rules()
|
||||
|
||||
def validate(self) -> None:
|
||||
"""
|
||||
Cycles through the set rules collecting errors, if any.
|
||||
"""
|
||||
|
||||
for rule in self.rules:
|
||||
rule(self.value)
|
||||
|
||||
# merge the errors lists
|
||||
self.errors = self.errors + rule.errors
|
||||
|
||||
def is_valid(self) -> list | bool:
|
||||
"""
|
||||
Returns true when the errors array is filled.
|
||||
"""
|
||||
|
||||
return not self.errors
|
||||
|
||||
def _check_rules(self) -> None | Exception:
|
||||
"""
|
||||
Ensures the validity of the passed rules.
|
||||
"""
|
||||
|
||||
for rule in self.rules:
|
||||
if not isinstance(rule, Rule):
|
||||
raise ValidatorRuleNonValidException()
|
||||
|
||||
# reset errors on each validation
|
||||
rule.errors = []
|
||||
|
||||
class ValidatorRuleNonValidException(Exception):
|
||||
|
||||
"""
|
||||
Raised if the class doesn't inherit the Rule class.
|
||||
"""
|
||||
|
||||
pass
|
Loading…
Reference in a new issue