import re from typing import Optional from flask import current_app from flask_appbuilder.exceptions import PasswordComplexityValidationError from flask_appbuilder.models.base import BaseInterface from flask_babel import gettext from wtforms import Field, Form, ValidationError password_complexity_regex = re.compile( r"""( ^(?=.*[A-Z].*[A-Z]) # at least two capital letters (?=.*[^0-9a-zA-Z]) # at least one of these special characters (?=.*[0-9].*[0-9]) # at least two numeric digits (?=.*[a-z].*[a-z].*[a-z]) # at least three lower case letters .{10,} # at least 10 total characters $ )""", re.VERBOSE, ) class Unique: """ WTForm field validator. Checks if field value is unique against a specified table field. """ field_flags = {"unique": True} def __init__( self, datamodel: BaseInterface, col_name: str, message: Optional[str] = None ) -> None: """ :param datamodel: The datamodel class, abstract layer for backend :param col_name: The unique column name. :param message: The error message. """ self.datamodel = datamodel self.col_name = col_name self.message = message def __call__(self, form: Form, field: Field) -> None: filters = self.datamodel.get_filters().add_filter( self.col_name, self.datamodel.FilterEqual, field.data ) count, obj = self.datamodel.query(filters) if count > 0: # only test if Unique, if pk value is different on update. if not hasattr(form, "_id") or form._id != self.datamodel.get_keys(obj)[0]: if self.message is None: self.message = field.gettext("Already exists.") raise ValidationError(self.message) class PasswordComplexityValidator: """ WTForm field validator. Calls a custom password validator, useful for imposing password complexity for database Auth users. """ def __call__(self, form: Form, field: Field) -> None: if current_app.config.get("FAB_PASSWORD_COMPLEXITY_ENABLED", False): password_complexity_validator = current_app.config.get( "FAB_PASSWORD_COMPLEXITY_VALIDATOR", None ) if password_complexity_validator is not None: try: password_complexity_validator(field.data) except PasswordComplexityValidationError as exc: raise ValidationError(str(exc)) else: try: default_password_complexity(field.data) except PasswordComplexityValidationError as exc: raise ValidationError(str(exc)) def default_password_complexity(password: str) -> None: """ FAB's default password complexity validator, set FAB_PASSWORD_COMPLEXITY_ENABLED to True to enable it """ match = re.search(password_complexity_regex, password) if not match: raise PasswordComplexityValidationError( gettext( "Must have at least two capital letters," " one special character, two digits, three lower case letters and" " a minimal length of 10." ) )