refactor(usermodel): turn validation methods into assertions
This commit is contained in:
parent
7b1196f09d
commit
001e80977e
@ -4,3 +4,27 @@ class MissingConfigKeyError(RuntimeError):
|
||||
"""Raised when an expected appconfig key-value pair is not found."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidStringFormatException(Exception):
|
||||
"""Exception raised when an invalid string format is encountered."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidImageException(Exception):
|
||||
"""Exception raised when an invalid image is encountered."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AttributeNotFoundException(Exception):
|
||||
"""Exception raised when an entry's is unexpectedly not set."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAttributeException(Exception):
|
||||
"""Exception raised when an entry's is unexpectedly not set."""
|
||||
|
||||
pass
|
||||
|
@ -17,12 +17,7 @@ from ldap3 import Connection, Server, ALL, Reader, Writer, ObjectDef, MODIFY_REP
|
||||
from PIL import Image
|
||||
|
||||
from lumi2.usermodel import User, Group
|
||||
from lumi2.exceptions import MissingConfigKeyError
|
||||
|
||||
|
||||
class InvalidStringFormatException(Exception):
|
||||
"""Exception raised when an invalid string format is encountered."""
|
||||
pass
|
||||
from lumi2.exceptions import *
|
||||
|
||||
|
||||
class InvalidConnectionException(Exception):
|
||||
@ -40,15 +35,6 @@ class EntryNotFoundException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AttributeNotFoundException(Exception):
|
||||
"""Exception raised when an entry's is unexpectedly not set."""
|
||||
pass
|
||||
|
||||
class InvalidAttributeException(Exception):
|
||||
"""Exception raised when an entry's is unexpectedly not set."""
|
||||
pass
|
||||
|
||||
|
||||
def _assert_is_valid_base_dn(input_str) -> None:
|
||||
"""Checks whether the input string is a valid LDAP base DN.
|
||||
|
||||
|
@ -6,9 +6,11 @@ import hashlib
|
||||
from binascii import Error as Base64DecodeError
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
from PIL.Image import JpegImageFile, Image
|
||||
from flask import current_app
|
||||
|
||||
from lumi2.exceptions import InvalidStringFormatException, InvalidImageException
|
||||
|
||||
class User:
|
||||
"""Class model for a user.
|
||||
|
||||
@ -32,7 +34,7 @@ class User:
|
||||
|
||||
|
||||
@staticmethod
|
||||
def is_valid_username(input_str: str) -> bool:
|
||||
def assert_is_valid_username(input_str: str) -> None:
|
||||
"""Checks whether the input string is a valid username.
|
||||
|
||||
Valid usernames can contain only uppercase/lowercase latin characters,
|
||||
@ -45,38 +47,74 @@ class User:
|
||||
input_str : str
|
||||
The string whose validity as a username to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if input_str is a valid username and False otherwise.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If input_str is not of type string.
|
||||
InvalidStringFormatException
|
||||
If input_str is not a valid username.
|
||||
"""
|
||||
|
||||
if not isinstance(input_str, str):
|
||||
raise TypeError(f"Expected a string but got: '{type(input_str)}'.")
|
||||
|
||||
if not len(input_str):
|
||||
return False
|
||||
raise InvalidStringFormatException("Username must contain at least one character.")
|
||||
if len(input_str) > 64:
|
||||
return False
|
||||
raise InvalidStringFormatException("Username must not exceed 64 characters in length.")
|
||||
|
||||
if input_str[0] not in ascii_lowercase + ascii_uppercase:
|
||||
return False
|
||||
raise InvalidStringFormatException("Username must start with a letter.")
|
||||
|
||||
valid_chars = ascii_lowercase + ascii_uppercase + digits + "-_."
|
||||
for char in input_str:
|
||||
if char not in valid_chars:
|
||||
return False
|
||||
|
||||
return True
|
||||
raise InvalidStringFormatException(
|
||||
f"Invalid character in username: '{char}'."
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def is_valid_password_hash(input_str: str) -> bool:
|
||||
def assert_is_valid_password(input_str: str) -> None:
|
||||
"""Checks whether the input string is a valid password.
|
||||
|
||||
A valid password may not contain any whitespace.
|
||||
They must be at least 8 characters long and at 64 characters at most.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_str : str
|
||||
The string whose validity as a password to check.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If input_str is not of type string.
|
||||
InvalidStringFormatException
|
||||
If input_str is not a valid password.
|
||||
"""
|
||||
|
||||
if not isinstance(input_str, str):
|
||||
raise TypeError(f"Expected a string but got: '{type(input_str)}'.")
|
||||
|
||||
if len(input_str) < 8:
|
||||
raise InvalidStringFormatException(
|
||||
"Password must be at least 8 characters in length."
|
||||
)
|
||||
if len(input_str) > 64:
|
||||
raise InvalidStringFormatException(
|
||||
"Password may not be longer than 8 characters."
|
||||
)
|
||||
|
||||
for char in input_str:
|
||||
if char in whitespace:
|
||||
raise InvalidStringFormatException(
|
||||
"Password my not contain any whitespace."
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def assert_is_valid_password_hash(input_str: str) -> None:
|
||||
"""Checks whether the input string is a valid password hash.
|
||||
|
||||
A valid password hash is a non-empty string containing base64-decodeable
|
||||
@ -87,136 +125,143 @@ class User:
|
||||
input_str : str
|
||||
The string whose validity as a password hash to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if input_str is a valid password hash and False otherwise.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If input_str is not of type string.
|
||||
InvalidStringFormatException
|
||||
If input_str is not a valid password hash.
|
||||
"""
|
||||
|
||||
if not isinstance(input_str, str):
|
||||
raise TypeError(f"Expected a string but got: '{type(input_str)}'.")
|
||||
|
||||
if not len(input_str):
|
||||
return False
|
||||
raise InvalidStringFormatException(
|
||||
"Password hash must be at least one character in length."
|
||||
)
|
||||
|
||||
try:
|
||||
b64decode(input_str, validate=True)
|
||||
return True
|
||||
except Base64DecodeError:
|
||||
return False
|
||||
except Base64DecodeError as e:
|
||||
raise InvalidStringFormatException from e
|
||||
|
||||
|
||||
@staticmethod
|
||||
def is_valid_email(input_str: str) -> bool:
|
||||
def assert_is_valid_email(input_str: str) -> None:
|
||||
"""Checks whether the input string is a valid email address.
|
||||
|
||||
WARNING: this validation is very rudimentary. Proper validation requires
|
||||
Very rudimentary check, proper validation would require
|
||||
a validation email to be sent and confirmed by the user.
|
||||
A valid email address contains no whitespace, at least one '@' character,
|
||||
and a '.' character somewhere after the '@'.
|
||||
The maximum length for a valid email address is 64 characters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_str : str
|
||||
The string whose validity as an email address to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if input_str is a valid email address and False otherwise.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If input_str is not of type string.
|
||||
InvalidStringFormatException
|
||||
If input_str is not a valid email address.
|
||||
"""
|
||||
|
||||
if not isinstance(input_str, str):
|
||||
raise TypeError(f"Expected a string but got: '{type(input_str)}'.")
|
||||
|
||||
if '@' not in input_str:
|
||||
return False
|
||||
raise InvalidStringFormatException(
|
||||
"Invalid email address: no '@' found."
|
||||
)
|
||||
if '.' not in input_str:
|
||||
return False
|
||||
raise InvalidStringFormatException(
|
||||
"Invalid email address: no top-level-domain found."
|
||||
)
|
||||
if '.' not in input_str.split('@')[1]:
|
||||
return False
|
||||
raise InvalidStringFormatException(
|
||||
"Invalid email address: no top-level-domain found."
|
||||
)
|
||||
|
||||
if len(input_str) > 64:
|
||||
raise InvalidStringFormatException(
|
||||
"Invalid email address: may not be longer than 64 characters."
|
||||
)
|
||||
|
||||
for char in input_str:
|
||||
if char in whitespace:
|
||||
return False
|
||||
|
||||
return True
|
||||
raise InvalidStringFormatException(
|
||||
"Invalid email address: no whitespace permitted."
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def is_valid_person_name(input_str: str) -> bool:
|
||||
def assert_is_valid_name(input_str: str) -> None:
|
||||
"""Checks whether the input string is valid as a first/last/display name.
|
||||
|
||||
Valid names cannot contain whitespace and must be at least one character
|
||||
long.
|
||||
long, and 64 characters at most.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_str : str
|
||||
The string whose validity as a first-/last-/displayname to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if input_str is a valid name and False otherwise.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If input_str is not of type string.
|
||||
InvalidStringFormatException
|
||||
If input_str is not a valid person's name.
|
||||
"""
|
||||
|
||||
if not isinstance(input_str, str):
|
||||
raise TypeError(f"Expected a string but got: '{type(input_str)}'.")
|
||||
|
||||
if not len(input_str):
|
||||
return False
|
||||
raise InvalidStringFormatException(
|
||||
"Invalid name: must be at least 1 character in length."
|
||||
)
|
||||
if len(input_str) > 64:
|
||||
raise InvalidStringFormatException(
|
||||
"Invalid name: may not be longer than 64 characters."
|
||||
)
|
||||
|
||||
for char in input_str:
|
||||
if char in whitespace:
|
||||
return False
|
||||
|
||||
return True
|
||||
raise InvalidStringFormatException(
|
||||
"Invalid name: may not contain whitespace."
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def is_valid_picture(input_image: Image.Image) -> bool:
|
||||
def assert_is_valid_picture(input_image: JpegImageFile) -> None:
|
||||
"""Checks whether the input image is a valid Image object.
|
||||
|
||||
TBD - unsure which formats and filesizes to allow here.
|
||||
Valid images must be of type JpegImageFile.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_image : PIL.Image
|
||||
The Image whose validity to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if input_image is a valid Image and False otherwise.
|
||||
input_image : PIL.Image.JpegImageFile
|
||||
The Image object whose validity to check.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If input_image is not of type PIL.Image.
|
||||
InvalidImageException
|
||||
If the input image's type is not PIL.Image.JpegImageFile.
|
||||
"""
|
||||
|
||||
if not isinstance(input_image, Image.Image):
|
||||
if not isinstance(input_image, Image):
|
||||
raise TypeError(f"Expected a PIL Image but got: '{type(input_image)}'.")
|
||||
|
||||
# TODO implement some integrity checks
|
||||
# TODO implement some filesize restrictions
|
||||
return True
|
||||
if not isinstance(input_image, JpegImageFile):
|
||||
raise InvalidImageException(
|
||||
"User picture must be in JPEG format."
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
@ -271,38 +316,54 @@ class User:
|
||||
first_name: str, last_name: str, display_name = None,
|
||||
picture = None,
|
||||
):
|
||||
"""Constructor for User objects.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
username : str
|
||||
The username, valid as described by User.assert_is_valid_username().
|
||||
password_hash : str
|
||||
The user's base64-encoded SHA512-hashed password (without the
|
||||
'{SHA512}'-prefix expected by LDAP).
|
||||
Must be valid as described by User.assert_is_valid_password_hash().
|
||||
email : str
|
||||
The User's email address.
|
||||
Must be valid as described by User.assert_is_valid_email().
|
||||
first_name : str
|
||||
The User's first name.
|
||||
Must be valid as described by User.assert_is_valid_name().
|
||||
last_name : str
|
||||
The User's last name.
|
||||
Must be valid as described by User.assert_is_valid_name().
|
||||
display_name : str = first_name
|
||||
The User's nickname. If unspecified, gets set to the User's first
|
||||
name.
|
||||
Must be valid as described by User.assert_is_valid_name().
|
||||
picture : PIL.Image.JpegImageFile
|
||||
The User's JPEG picture. If unspecified, a default user picture is
|
||||
used. Must be valid as described by User.asser_is_valid_picture().
|
||||
"""
|
||||
|
||||
try:
|
||||
User.assert_is_valid_username(username)
|
||||
User.assert_is_valid_password_hash(password_hash)
|
||||
User.assert_is_valid_email(email)
|
||||
User.assert_is_valid_name(first_name)
|
||||
User.assert_is_valid_name(last_name)
|
||||
if display_name is not None:
|
||||
User.assert_is_valid_name(display_name)
|
||||
if picture is not None:
|
||||
User.assert_is_valid_picture(picture)
|
||||
except (InvalidStringFormatException, InvalidImageException) as e:
|
||||
raise ValueError from e
|
||||
|
||||
if not User.is_valid_username(username):
|
||||
raise ValueError(f"Not a valid username: '{username}'.")
|
||||
self.username = username
|
||||
|
||||
if not User.is_valid_password_hash(password_hash):
|
||||
raise ValueError(f"Not a valid password hash: '{password_hash}'.")
|
||||
self.password_hash = password_hash
|
||||
|
||||
if not User.is_valid_email(email):
|
||||
raise ValueError(f"Not a valid email address: '{email}'.")
|
||||
self.email = email
|
||||
|
||||
for name in [first_name, last_name]:
|
||||
if not User.is_valid_person_name(name):
|
||||
raise ValueError(f"Not a valid name: '{name}'.")
|
||||
self.first_name = first_name
|
||||
self.last_name = last_name
|
||||
|
||||
if display_name is not None:
|
||||
if not User.is_valid_person_name(display_name):
|
||||
raise ValueError(f"Not a valid display name: '{display_name}'.")
|
||||
self.display_name = display_name
|
||||
else:
|
||||
self.display_name = first_name
|
||||
|
||||
if picture is not None:
|
||||
if not User.is_valid_picture(picture):
|
||||
raise ValueError(f"Not a valid image: '{picture}'.")
|
||||
self.picture = picture
|
||||
else:
|
||||
self.picture = User._get_default_picture()
|
||||
self.display_name = display_name if display_name is not None else first_name
|
||||
self.picture = picture if picture is not None else User._get_default_picture()
|
||||
|
||||
|
||||
def _generate_static_images(self, force=False) -> None:
|
||||
@ -385,48 +446,61 @@ class Group:
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_valid_groupname(input_str: str) -> bool:
|
||||
def assert_is_valid_groupname(input_str: str) -> None:
|
||||
"""Checks whether the input string is a valid group name.
|
||||
|
||||
A valid group name consists of only alphanumeric characters, starts with
|
||||
a latin character and has minimum length 1.
|
||||
a latin character, has minimum length 1 and maximim length 64.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_str : str
|
||||
The string to check for validity as a group name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if input_str is a valid group name, and False otherwise.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If input_str is not of type string.
|
||||
InvalidStringFormatException
|
||||
If input_str is not a valid group name.
|
||||
"""
|
||||
|
||||
if not isinstance(input_str, str):
|
||||
raise TypeError(f"Expected a string but got: '{type(input_str)}'.")
|
||||
|
||||
if not len(input_str):
|
||||
return False
|
||||
|
||||
raise InvalidStringFormatException(
|
||||
"Invalid group name: must contain at least one character."
|
||||
)
|
||||
if input_str[0] not in ascii_lowercase + ascii_uppercase:
|
||||
return False
|
||||
|
||||
raise InvalidStringFormatException(
|
||||
"Invalid group name: must start with a letter."
|
||||
)
|
||||
for char in input_str:
|
||||
if not char in ascii_uppercase + ascii_lowercase + digits:
|
||||
return False
|
||||
|
||||
return True
|
||||
raise InvalidStringFormatException(
|
||||
f"Invalid character in group name: '{char}'."
|
||||
)
|
||||
|
||||
|
||||
def __init__(self, groupname: str, members: set[User]):
|
||||
"""Constructor for Group objects.
|
||||
|
||||
if not Group.is_valid_groupname(groupname):
|
||||
raise ValueError("Not a valid group name: '{groupname}'.")
|
||||
Groups must always have at least one member (an LDAP limitation).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
groupname : str
|
||||
The Group's name.
|
||||
Must be valid as described by Group.assert_is_valid_groupname().
|
||||
members : set[User]
|
||||
A set conaining Users who are members of this Group.
|
||||
The set must contain at least one member.
|
||||
"""
|
||||
try:
|
||||
Group.assert_is_valid_groupname(groupname)
|
||||
except InvalidStringFormatException as e:
|
||||
raise ValueError from e
|
||||
self.groupname = groupname
|
||||
|
||||
if not isinstance(members, set):
|
||||
|
Loading…
Reference in New Issue
Block a user