diff --git a/docker-compose.yml b/docker-compose.yml index a229d7b..84eb9cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: - ./lumi2/__init__.py:/app/lumi2/__init__.py:ro - ./lumi2/exceptions.py:/app/lumi2/exceptions.py:ro - ./lumi2/ldap.py:/app/lumi2/ldap.py:ro + - ./lumi2/usermodel.py:/app/lumi2/usermodel.py:ro - ./lumi2/usermanager.py:/app/lumi2/usermanager.py:ro - ./lumi2/static/:/app/lumi2/static/:ro - ./lumi2/templates/:/app/lumi2/templates/:ro diff --git a/lumi2/ldap.py b/lumi2/ldap.py index b18c38d..5073cf7 100644 --- a/lumi2/ldap.py +++ b/lumi2/ldap.py @@ -596,7 +596,7 @@ def get_user(connection: Connection, uid: str) -> User: 'sn', 'displayName', 'uid', - 'password', + 'userPassword', 'jpegPhoto', 'mail', ] diff --git a/lumi2/usermodel.py b/lumi2/usermodel.py index 6b3e418..e663c70 100644 --- a/lumi2/usermodel.py +++ b/lumi2/usermodel.py @@ -4,6 +4,10 @@ Also provides methods to convert LDAP user/group entries into user/group objects and vice versa. """ +from string import ascii_lowercase, ascii_uppercase, digits, whitespace +from base64 import b64decode +from binascii import Error as Base64DecodeError + from PIL import Image class User: @@ -12,23 +16,17 @@ class User: Attributes ---------- username : str - The user's username. Can contain only uppercase/lowercase latin characters, - numbers, hyphens, underscores and periods. Must start with a latin character. - Minimum length is 1, maximum length is 64 characters. + The user's username. password_hash : str Base64-encoded SHA512 hash of the user's password. email : str The user's email address. - Must contain an '@'-character, may not contain any whitespace. first_name : str The user's first name. - May not contain any whitespace. last_name : str The user's last name. - May not contain any whitespace. display_name : str The user's display name (as required by some LDAP-enabled applications). - May not contain any whitespace. picture : PIL.Image The user's profile picture as a PIL Image object. """ @@ -62,17 +60,28 @@ class User: if not isinstance(input_str, str): raise TypeError(f"Expected a string but got: '{type(input_str)}'.") - # TODO implement - return False + if not len(input_str): + return False + if len(input_str) > 64: + return False + + if input_str[0] not in ascii_lowercase + ascii_uppercase: + return False + + valid_chars = ascii_lowercase + ascii_uppercase + digits + "-_." + for char in input_str: + if char not in valid_chars: + return False + + return True @staticmethod def is_valid_password_hash(input_str: str) -> bool: """Checks whether the input string is a valid password hash. - Password hashes are base64-encoded bytes, and the hashing algorithm used - to create the hash is hard to determine, but we can verify that the - provided hash is a non-empty string containing base64-decodeable text. + A valid password hash is a non-empty string containing base64-decodeable + text. Parameters ---------- @@ -93,15 +102,24 @@ class User: if not isinstance(input_str, str): raise TypeError(f"Expected a string but got: '{type(input_str)}'.") - # TODO implement - return False + if not len(input_str): + return False + + try: + b64decode(input_str, validate=True) + return True + except Base64DecodeError: + return False @staticmethod def is_valid_email(input_str: str) -> bool: """Checks whether the input string is a valid email address. - A valid email address contains no whitespace and at least one '@'-char. + WARNING: this validation is very rudimentary. Proper validation requires + 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 '@'. Parameters ---------- @@ -122,8 +140,18 @@ class User: if not isinstance(input_str, str): raise TypeError(f"Expected a string but got: '{type(input_str)}'.") - # TODO implement - return False + if '@' not in input_str: + return False + if '.' not in input_str: + return False + if '.' not in input_str.split('@')[1]: + return False + + for char in input_str: + if char in whitespace: + return False + + return True @staticmethod @@ -152,8 +180,14 @@ class User: if not isinstance(input_str, str): raise TypeError(f"Expected a string but got: '{type(input_str)}'.") - # TODO implement - return False + if not len(input_str): + return False + + for char in input_str: + if char in whitespace: + return False + + return True @staticmethod @@ -181,6 +215,10 @@ class User: if not isinstance(input_image, Image): raise TypeError(f"Expected a PIL Image but got: '{type(input_image)}'.") + # TODO implement + return True + + def __init__( self, username: str, password_hash: str, email: str, diff --git a/tests/test_usermodel.py b/tests/test_usermodel.py index e7db13e..40b8661 100644 --- a/tests/test_usermodel.py +++ b/tests/test_usermodel.py @@ -41,8 +41,7 @@ def test_is_valid_password_hash(): valid_hashes = [ # can contain [A-Za-z0-9+/] and up to two '=' at the end - "foobar", "EzM3", - "abcABC123+/=", "a", + "EzM3", "abcABC123+/=", ] for valid_hash in valid_hashes: assert User.is_valid_password_hash(valid_hash) @@ -55,7 +54,7 @@ def test_is_valid_email(): invalid_emails = [ "", " ", "\t", "\n", - " alice@example.com", "alice", "@", "@.", + " alice@example.com", "alice", "@", "alice@com" "alice@example.com ", "alice@ex ample.com" ] for invalid_email in invalid_emails: