def test_destroy() -> None: # Only executed if tests are run with --destroy flag if os.getenv("TEST_DESTROY_MODE", "0") != "1": log.info("Skipping destroy test, TEST_DESTROY_MODE not enabled") return # Always enable during core tests if not Connector.check_availability("authentication"): # pragma: no cover log.warning("Skipping authentication test: service not available") return # if Connector.check_availability("sqlalchemy"): # sql = sqlalchemy.get_instance() # # Close previous connections, otherwise the new create_app will hang # sql.session.remove() # sql.session.close_all() auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) assert user is not None create_app(mode=ServerModes.DESTROY) try: auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) assert user is None except ServiceUnavailable: pass
def test_login_management(faker: Faker) -> None: auth = Connector.get_authentication_instance() if BaseAuthentication.default_user: logins = auth.get_logins(BaseAuthentication.default_user) assert isinstance(logins, list) assert len(logins) > 0 auth.flush_failed_logins(BaseAuthentication.default_user) logins = auth.get_logins(BaseAuthentication.default_user, only_unflushed=True) assert len(logins) == 0 logins = auth.get_logins(BaseAuthentication.default_user, only_unflushed=False) assert len(logins) > 0 logins = auth.get_logins(faker.ascii_email()) assert isinstance(logins, list) assert len(logins) == 0 logins = auth.get_logins(faker.pystr()) assert isinstance(logins, list) assert len(logins) == 0
def post(self, username: str) -> Response: self.auth.verify_blocked_username(username) user = self.auth.get_user(username=username) # if user is None this endpoint does nothing but the response # remain the same to prevent any user guessing if user is not None: auth = Connector.get_authentication_instance() activation_token, payload = auth.create_temporary_token( user, auth.ACTIVATE_ACCOUNT) server_url = get_frontend_url() rt = activation_token.replace(".", "+") url = f"{server_url}/public/register/{rt}" sent = send_activation_link(user, url) if not sent: # pragma: no cover raise ServiceUnavailable("Error sending email, please retry") auth.save_token(user, activation_token, payload, token_type=auth.ACTIVATE_ACCOUNT) msg = ("We are sending an email to your email address where " "you will find the link to activate your account") return self.response(msg)
def test_destroy() -> None: auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) assert user is not None create_app(mode=ServerModes.DESTROY) if Connector.check_availability("sqlalchemy"): with pytest.raises(ServiceUnavailable): auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) else: auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) assert user is None
def generate_totp(email: Optional[str]) -> str: assert email is not None auth = Connector.get_authentication_instance() user = auth.get_user(username=email.lower()) secret = auth.get_totp_secret(user) return pyotp.TOTP(secret).now()
def test_init() -> None: auth = Connector.get_authentication_instance() if Connector.authentication_service == "sqlalchemy": # Re-init does not work with MySQL due to issues with previous connections # Considering that: # 1) this is a workaround to test the initialization # (not the normal workflow used by the application) # 2) the init is already tested with any other DB, included postgres # 3) MySQL is not used by any project # => there is no need to go crazy in debugging this issue! if auth.db.is_mysql(): # type: ignore return # sql = sqlalchemy.get_instance() if Connector.check_availability("sqlalchemy"): # Prevents errors like: # sqlalchemy.exc.ResourceClosedError: This Connection is closed Connector.disconnect_all() # sql = sqlalchemy.get_instance() # # Close previous connections, otherwise the new create_app will hang # sql.session.remove() # sql.session.close_all() try: create_app(mode=ServerModes.INIT) # This is only a rough retry to prevent random errors from sqlalchemy except Exception: # pragma: no cover create_app(mode=ServerModes.INIT) auth = Connector.get_authentication_instance() try: user = auth.get_user(username=BaseAuthentication.default_user) # SqlAlchemy sometimes can raise an: # AttributeError: 'NoneType' object has no attribute 'twophase' # due to the multiple app created... should be an issue specific of this test # In that case... simply retry. except AttributeError: # pragma: no cover user = auth.get_user(username=BaseAuthentication.default_user) assert user is not None
def test_ip_management() -> None: auth = Connector.get_authentication_instance() ip_data = auth.localize_ip("8.8.8.8") assert ip_data is not None # I don't know if this tests will be stable... assert ip_data == "United States" assert auth.localize_ip("8.8.8.8, 4.4.4.4") is None
def test_authentication_with_auth_callback(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping authentication tests") return auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) assert user is not None VALID = f"/tests/preloadcallback/{user.uuid}" INVALID = "/tests/preloadcallback/12345678-90ab-cdef-1234-567890abcdef" admin_headers, _ = self.do_login(client, None, None) # Verify both endpoint ... r = client.get(f"{API_URI}{VALID}", query_string={"test": True}, headers=admin_headers) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) assert len(content) == 1 assert "email" in content assert content["email"] == user.email r = client.get(f"{API_URI}{INVALID}", query_string={"test": True}, headers=admin_headers) assert r.status_code == 401 # and get_schema! r = client.get( f"{API_URI}{VALID}", query_string={"get_schema": True}, headers=admin_headers, ) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, list) assert len(content) == 1 assert content[0]["key"] == "test" assert content[0]["type"] == "boolean" r = client.get( f"{API_URI}{INVALID}", query_string={"get_schema": True}, headers=admin_headers, ) assert r.status_code == 401
def test_init() -> None: # Only executed if tests are run with --destroy flag if os.getenv("TEST_DESTROY_MODE", "0") != "1": log.info("Skipping destroy test, TEST_DESTROY_MODE not enabled") return # Always enable during core tests if not Connector.check_availability("authentication"): # pragma: no cover log.warning("Skipping authentication test: service not available") return auth = Connector.get_authentication_instance() if Connector.authentication_service == "sqlalchemy": # Re-init does not work with MySQL due to issues with previous connections # Considering that: # 1) this is a workaround to test the initialization # (not the normal workflow used by the application) # 2) the init is already tested with any other DB, included postgres # 3) MySQL is not used by any project # => there is no need to go crazy in debugging this issue! if auth.db.is_mysql(): # type: ignore return # sql = sqlalchemy.get_instance() create_app(mode=ServerModes.INIT) auth = Connector.get_authentication_instance() try: user = auth.get_user(username=BaseAuthentication.default_user) # SqlAlchemy sometimes can raise an: # AttributeError: 'NoneType' object has no attribute 'twophase' # due to the multiple app created... should be an issue specific of this test # In that case... simply retry. except AttributeError: # pragma: no cover user = auth.get_user(username=BaseAuthentication.default_user) assert user is not None
def test_totp_management() -> None: auth = Connector.get_authentication_instance() with pytest.raises(Unauthorized, match=r"Verification code is missing"): # NULL totp auth.verify_totp(None, None) user = auth.get_user(username=auth.default_user) secret = auth.get_totp_secret(user) totp = pyotp.TOTP(secret) # Verifiy current totp assert auth.verify_totp(user, totp.now()) now = datetime.now() t30s = timedelta(seconds=30) # Verify previous and next totp(s) assert auth.verify_totp(user, totp.at(now + t30s)) assert auth.verify_totp(user, totp.at(now - t30s)) # Verify second-previous and second-ntext totp(s) with pytest.raises(Unauthorized, match=r"Verification code is not valid"): # Future totp auth.verify_totp(user, totp.at(now + t30s + t30s)) with pytest.raises(Unauthorized, match=r"Verification code is not valid"): # Past totp auth.verify_totp(user, totp.at(now - t30s - t30s)) # Extend validity window auth.TOTP_VALIDITY_WINDOW = 2 # Verify again second-previous and second-ntext totp(s) assert auth.verify_totp(user, totp.at(now + t30s + t30s)) assert auth.verify_totp(user, totp.at(now - t30s - t30s)) # Verify second-second-previous and second-second-ntext totp(s) with pytest.raises(Unauthorized, match=r"Verification code is not valid"): # Future totp auth.verify_totp(user, totp.at(now + t30s + t30s + t30s)) with pytest.raises(Unauthorized, match=r"Verification code is not valid"): # Past totp auth.verify_totp(user, totp.at(now - t30s - t30s - t30s))
def test_database_exceptions(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping dabase exceptions tests") return # This is a special value. The endpoint will try to create a group without # shortname. A BadRequest is expected because the database should refuse the # entry due to the missing property r = client.post(f"{API_URI}/tests/database/400") assert r.status_code == 400 # This is the message of a DatabaseMissingRequiredProperty assert self.get_content(r) == "Missing property shortname required by Group" auth = Connector.get_authentication_instance() default_group = auth.get_group(name=DEFAULT_GROUP_NAME) assert default_group is not None # the /tests/database endpoint will change the default group fullname # as a side effect to the test the database_transaction decorator default_fullname = default_group.fullname random_name = faker.pystr() # This will create a new group with short/full name == random_name r = client.post(f"{API_URI}/tests/database/{random_name}") assert r.status_code == 200 default_group = auth.get_group(name=DEFAULT_GROUP_NAME) assert default_group is not None # As a side effect the fullname of defaut_group is changed... assert default_group.fullname != default_fullname # ... and this is the new name new_fullname = default_group.fullname # This will try to create again a group with short/full name == random_name # but this will fail due to unique keys r = client.post(f"{API_URI}/tests/database/{random_name}") assert r.status_code == 409 # This is the message of a DatabaseDuplicatedEntry self.get_content(r) == "A Group already exists with 'shortname': '400'" # The default group will not change again because the # database_transaction decorator will undo the change default_group = auth.get_group(name=DEFAULT_GROUP_NAME) assert default_group is not None assert default_group.fullname == new_fullname
def test_ip_management(self) -> None: # Always enable during core tests if not Connector.check_availability( "authentication"): # pragma: no cover log.warning("Skipping authentication test: service not available") return auth = Connector.get_authentication_instance() ip_data = auth.localize_ip("8.8.8.8") assert ip_data is not None # I don't know if this tests will be stable... assert ip_data == "United States" assert auth.localize_ip("8.8.8.8, 4.4.4.4") is None
def auth(self) -> BaseAuthentication: if not self.__auth: self.__auth = Connector.get_authentication_instance() return self.__auth
def test_authentication_abstract_methods(faker: Faker) -> None: # Super trick! # https://clamytoe.github.io/articles/2020/Mar/12/testing-abcs-with-abstract-methods-with-pytest abstractmethods = BaseAuthentication.__abstractmethods__ BaseAuthentication.__abstractmethods__ = frozenset() auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) group = auth.get_group(name=DEFAULT_GROUP_NAME) role = auth.get_roles()[0] auth = BaseAuthentication() # type: ignore assert (auth.get_user(username=faker.ascii_email(), user_id=faker.pystr()) is None) assert auth.get_users() is None assert auth.save_user(user=user) is None assert auth.delete_user(user=user) is None assert auth.get_group(group_id=faker.pystr(), name=faker.pystr()) is None assert auth.get_groups() is None assert auth.get_user_group(user=user) is None assert auth.get_group_members(group=group) is None assert auth.save_group(group=group) is None assert auth.delete_group(group=group) is None assert auth.get_tokens( user=user, token_jti=faker.pystr(), get_all=True) is None assert auth.verify_token_validity(jti=faker.pystr(), user=user) is None assert (auth.save_token(user=user, token=faker.pystr(), payload={}, token_type=faker.pystr()) is None) assert auth.invalidate_token(token=faker.pystr()) is None assert auth.get_roles() is None assert auth.get_roles_from_user(user=user) is None assert auth.create_role(name=faker.pystr(), description=faker.pystr()) is None assert auth.save_role(role=role) is None assert auth.create_user(userdata={}, roles=[faker.pystr()]) is None assert auth.link_roles(user=user, roles=[faker.pystr()]) is None assert auth.create_group(groupdata={}) is None assert auth.add_user_to_group(user=user, group=group) is None assert (auth.save_login( username=faker.ascii_email(), user=user, failed=True) is None) assert (auth.save_login( username=faker.ascii_email(), user=None, failed=True) is None) assert (auth.save_login( username=faker.ascii_email(), user=user, failed=False) is None) assert (auth.save_login( username=faker.ascii_email(), user=None, failed=False) is None) assert auth.get_logins(username=faker.ascii_email) is None assert auth.flush_failed_logins(username=faker.ascii_email) is None BaseAuthentication.__abstractmethods__ = abstractmethods
def test_password_management(self, faker: Faker) -> None: # Ensure name and surname longer than 3 name = self.get_first_name(faker) surname = self.get_last_name(faker) # Ensure an email not containing name and surname email = self.get_random_email(faker, name, surname) auth = Connector.get_authentication_instance() min_pwd_len = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 9999) pwd = faker.password(min_pwd_len - 1) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=pwd, email=email, name=name, surname=surname) assert not ret_val assert ret_text == "The new password cannot match the previous password" pwd = faker.password(min_pwd_len - 1) old_pwd = faker.password(min_pwd_len) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not ret_val error = f"Password is too short, use at least {min_pwd_len} characters" assert ret_text == error pwd = faker.password(min_pwd_len, low=False, up=True) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not ret_val assert ret_text == "Password is too weak, missing lower case letters" pwd = faker.password(min_pwd_len, low=True) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not ret_val assert ret_text == "Password is too weak, missing upper case letters" pwd = faker.password(min_pwd_len, low=True, up=True) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not ret_val assert ret_text == "Password is too weak, missing numbers" pwd = faker.password(min_pwd_len, low=True, up=True, digits=True) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not ret_val assert ret_text == "Password is too weak, missing special characters" pwd = faker.password(min_pwd_len, low=True, up=True, digits=True, symbols=True) ret_val, ret_text = auth.verify_password_strength(pwd=pwd, old_pwd=old_pwd, email=email, name=name, surname=surname) assert ret_val assert ret_text == "" password_with_name = [ name, surname, f"{faker.pystr()}{name}{faker.pystr()}" f"{faker.pystr()}{surname}{faker.pystr()}" f"{name}{faker.pyint(1, 99)}", ] for p in password_with_name: for pp in [p, p.lower(), p.upper(), p.title()]: # This is because with "strange characters" it is not ensured that: # str == str.upper().lower() # In that case let's skip the variant that alter the characters if p.lower() != pp.lower(): # pragma: no cover continue # This is to prevent failures for other reasons like length of chars pp += "+ABCabc123!" val, text = auth.verify_password_strength(pwd=pp, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not val assert text == "Password is too weak, can't contain your name" email_local = email.split("@")[0] password_with_email = [ email, email.replace(".", "").replace("_", ""), email_local, email_local.replace(".", "").replace("_", ""), f"{faker.pystr()}{email_local}{faker.pystr()}", ] for p in password_with_email: for pp in [p, p.lower(), p.upper(), p.title()]: # This is because with "strange characters" it is not ensured that: # str == str.upper().lower() # In that case let's skip the variant that alter the characters if p.lower() != pp.lower(): # pragma: no cover continue # This is to prevent failures for other reasons like length of chars pp += "+ABCabc123!" val, txt = auth.verify_password_strength(pwd=pp, old_pwd=old_pwd, email=email, name=name, surname=surname) assert not val assert txt == "Password is too weak, can't contain your email address" # Short names are not inspected for containing checks ret_val, ret_text = auth.verify_password_strength(pwd="Bob1234567!", old_pwd=old_pwd, email=email, name="Bob", surname=surname) assert ret_val assert ret_text == "" user = auth.get_user(username=BaseAuthentication.default_user) pwd = faker.password(min_pwd_len - 1) with pytest.raises(BadRequest, match=r"Missing new password"): # None password auth.change_password(user, pwd, None, None) with pytest.raises(BadRequest, match=r"Missing password confirmation"): # None password confirmation auth.change_password(user, pwd, pwd, None) with pytest.raises( Conflict, match=r"Your password doesn't match the confirmation"): # wrong confirmation auth.change_password(user, pwd, pwd, faker.password(strong=True)) with pytest.raises( Conflict, match=r"The new password cannot match the previous password", ): # Failed password strength checks auth.change_password(user, pwd, pwd, pwd) with pytest.raises( Conflict, match= rf"Password is too short, use at least {min_pwd_len} characters", ): # the first password parameter is only checked for new password strenght # i.e. is verified password != newpassword # pwd validity will be checked once completed checks on new password # => a random current password is ok here # Failed password strength checks auth.change_password(user, faker.password(), pwd, pwd) pwd1 = faker.password(strong=True) pwd2 = faker.password(strong=True) hash_1 = auth.get_password_hash(pwd1) assert len(hash_1) > 0 assert hash_1 != auth.get_password_hash(pwd2) with pytest.raises(Unauthorized, match=r"Invalid password"): # Hashing empty password auth.get_password_hash("") with pytest.raises(Unauthorized, match=r"Invalid password"): # Hashing a None password! auth.get_password_hash(None) assert auth.verify_password(pwd1, hash_1) with pytest.raises(TypeError): # Hashing a None password auth.verify_password(None, hash_1) # type: ignore assert not auth.verify_password(pwd1, None) # type: ignore assert not auth.verify_password(None, None) # type: ignore
def test_users_groups_roles(faker: Faker) -> None: auth = Connector.get_authentication_instance() user = auth.get_user(username=BaseAuthentication.default_user) group = auth.get_group(name="Default") assert user is not None user = auth.get_user(user_id=user.uuid) assert user is not None user = auth.get_user(username="******") assert user is None user = auth.get_user(user_id="invalid") assert user is None user = auth.get_user(username=None, user_id=None) assert user is None # Test the precedence, username valid and user invalid => user user = auth.get_user(username=BaseAuthentication.default_user, user_id="invalid") assert user is not None # Test the precedence, username invalid and user valid => None user = auth.get_user(username="******", user_id=user.uuid) assert user is None assert auth.get_user(None, None) is None user = auth.get_user(username=BaseAuthentication.default_user) assert user is not None assert not auth.save_user(None) assert auth.save_user(user) assert not auth.delete_user(None) assert auth.get_group(None, None) is None group = auth.get_group(name=DEFAULT_GROUP_NAME) assert group is not None assert not auth.save_group(None) assert auth.save_group(group) assert not auth.delete_group(None) # None user has no roles ... verify_roles will always be False assert not auth.verify_roles(None, ["A", "B"], required_roles="invalid") assert not auth.verify_roles(None, ["A", "B"], required_roles="ALL") assert not auth.verify_roles(None, ["A", "B"], required_roles="ANY") user = auth.get_user(username=BaseAuthentication.default_user) assert user is not None group = auth.get_group(name="Default") assert group is not None assert not auth.delete_user(None) assert not auth.delete_group(None) assert auth.delete_user(user) assert auth.delete_group(group) # Verify that user/group are now deleted assert auth.get_user(username=BaseAuthentication.default_user) is None assert auth.get_group(name="Default") is None # init_auth_db should restore missing default user and group. # But previous tests created additional users and groups, so that # the init auth db without force flags is not able to re-add # the missing and user and group if Env.get_bool("MAIN_LOGIN_ENABLE"): auth.init_auth_db({}) assert auth.get_user( username=BaseAuthentication.default_user) is None assert auth.get_group(name="Default") is None # Let's add the force flags to re-create the default user and group auth.init_auth_db({"force_user": True, "force_group": True}) assert auth.get_user( username=BaseAuthentication.default_user) is not None assert auth.get_group(name="Default") is not None # Let's save the current password to be checked later user = auth.get_user(username=BaseAuthentication.default_user) # expected_pwd = user.password # Let's verify that the user now is ADMIN assert Role.ADMIN.value in auth.get_roles_from_user(user) # Modify default user and group # # Change name, password and roles user.name = "Changed" # user.password = BaseAuthentication.get_password_hash("new-pwd#2!") auth.link_roles(user, [Role.USER.value]) auth.save_user(user) # Change fullname (not the shortname, since it is the primary key) group = auth.get_group(name="Default") group.fullname = "Changed" auth.save_group(group) # Verify that user and group are changed user = auth.get_user(username=BaseAuthentication.default_user) assert user.name == "Changed" # assert user.password != expected_pwd assert Role.ADMIN.value not in auth.get_roles_from_user(user) assert Role.USER.value in auth.get_roles_from_user(user) group = auth.get_group(name="Default") assert group.fullname == "Changed" roles: List[RoleObj] = auth.get_roles() assert isinstance(roles, list) assert len(roles) > 0 # Pick one of the default roles and change the description role: RoleObj = roles[0] assert role is not None default_name = role.name default_description = role.description new_description = faker.pystr() role.description = new_description assert auth.save_role(role) assert not auth.save_role(None) # Create a new custom role new_role_name = faker.pystr() new_role_descr = faker.pystr() auth.create_role(name=new_role_name, description=new_role_descr) # Verify the change on the roles and the creation of the new one for r in auth.get_roles(): if r.name == default_name: assert r.description == new_description assert r.description != default_description if r.name == new_role_name: assert r.description == new_role_descr # Verify that duplicated role names are refused at init time roles_data_backup = auth.roles_data auth.roles_data = { "admin_root": "Admin", "staff_user": "******", "group_coordinator": "Coordinator", "normal_user": "******", } with pytest.raises(SystemExit): auth.init_roles() auth.roles_data = roles_data_backup # Verify that init_roles restores description of default roles # While custom roles are not modified auth.init_roles() for r in auth.get_roles(): # default description restored for this default role if r.name == default_name: assert r.description != new_description assert r.description == default_description # custom additional role not modified by init roles if r.name == new_role_name: assert r.description == new_role_descr # Verify init without force flag will not restore default user and group auth.init_auth_db({}) user = auth.get_user(username=BaseAuthentication.default_user) assert user.name == "Changed" # assert user.password != expected_pwd assert Role.ADMIN.value not in auth.get_roles_from_user(user) assert Role.USER.value in auth.get_roles_from_user(user) group = auth.get_group(name="Default") assert group.fullname == "Changed" # Verify init with force flag will not restore the default user and group auth.init_auth_db({"force_user": True, "force_group": True}) user = auth.get_user(username=BaseAuthentication.default_user) assert user.name != "Changed" # assert user.password == expected_pwd assert Role.ADMIN.value in auth.get_roles_from_user(user) assert Role.USER.value in auth.get_roles_from_user(user) group = auth.get_group(name="Default") assert group.fullname != "Changed"
def test_tokens_management(self, client: FlaskClient, faker: Faker) -> None: auth = Connector.get_authentication_instance() # Just to verify that the function works verify_token_is_not_valid(auth, faker.pystr()) verify_token_is_not_valid(auth, faker.pystr(), auth.PWD_RESET) verify_token_is_not_valid(auth, faker.pystr(), auth.ACTIVATE_ACCOUNT) user = auth.get_user(username=BaseAuthentication.default_user) t1, payload1 = auth.create_temporary_token(user, auth.PWD_RESET) assert isinstance(t1, str) # not valid if not saved verify_token_is_not_valid(auth, t1, auth.PWD_RESET) auth.save_token(user, t1, payload1, token_type=auth.PWD_RESET) verify_token_is_not_valid(auth, t1) verify_token_is_not_valid(auth, t1, auth.FULL_TOKEN) verify_token_is_valid(auth, t1, auth.PWD_RESET) verify_token_is_not_valid(auth, t1, auth.ACTIVATE_ACCOUNT) verify_token_is_not_valid(auth, faker.ascii_email(), t1) # Create another type of temporary token => t1 is still valid t2, payload2 = auth.create_temporary_token(user, auth.ACTIVATE_ACCOUNT) assert isinstance(t2, str) # not valid if not saved verify_token_is_not_valid(auth, t2, auth.ACTIVATE_ACCOUNT) auth.save_token(user, t2, payload2, token_type=auth.ACTIVATE_ACCOUNT) verify_token_is_not_valid(auth, t2) verify_token_is_not_valid(auth, t2, auth.FULL_TOKEN) verify_token_is_not_valid(auth, t2, auth.PWD_RESET) verify_token_is_valid(auth, t2, auth.ACTIVATE_ACCOUNT) verify_token_is_not_valid(auth, faker.ascii_email(), t2) EXPIRATION = 3 # Create another token PWD_RESET, this will invalidate t1 t3, payload3 = auth.create_temporary_token(user, auth.PWD_RESET, duration=EXPIRATION) assert isinstance(t3, str) # not valid if not saved verify_token_is_not_valid(auth, t3, auth.PWD_RESET) auth.save_token(user, t3, payload3, token_type=auth.PWD_RESET) verify_token_is_valid(auth, t3, auth.PWD_RESET) verify_token_is_not_valid(auth, t1) verify_token_is_not_valid(auth, t1, auth.FULL_TOKEN) verify_token_is_not_valid(auth, t1, auth.PWD_RESET) verify_token_is_not_valid(auth, t1, auth.ACTIVATE_ACCOUNT) # Create another token ACTIVATE_ACCOUNT, this will invalidate t2 t4, payload4 = auth.create_temporary_token(user, auth.ACTIVATE_ACCOUNT, duration=EXPIRATION) assert isinstance(t4, str) # not valid if not saved verify_token_is_not_valid(auth, t4, auth.ACTIVATE_ACCOUNT) auth.save_token(user, t4, payload4, token_type=auth.ACTIVATE_ACCOUNT) verify_token_is_valid(auth, t4, auth.ACTIVATE_ACCOUNT) verify_token_is_not_valid(auth, t2) verify_token_is_not_valid(auth, t2, auth.FULL_TOKEN) verify_token_is_not_valid(auth, t2, auth.PWD_RESET) verify_token_is_not_valid(auth, t2, auth.ACTIVATE_ACCOUNT) # token expiration is only 3 seconds... let's test it time.sleep(EXPIRATION + 1) verify_token_is_not_valid(auth, t3, auth.PWD_RESET) verify_token_is_not_valid(auth, t4, auth.ACTIVATE_ACCOUNT) unpacked_token = auth.verify_token(None, raiseErrors=False) assert not unpacked_token[0] with pytest.raises(InvalidToken, match=r"Missing token"): auth.verify_token(None, raiseErrors=True) # Test token validiy _, token = self.do_login(client, None, None) tokens = auth.get_tokens(get_all=True) jti = None user = None for t in tokens: if t["token"] == token: jti = t["id"] user = t["user"] break assert jti is not None assert user is not None assert auth.verify_token_validity(jti, user) # Verify token against a wrong user another_user = auth.create_user( { "email": faker.ascii_email(), "name": "Default", "surname": "User", "password": faker.password(strong=True), "last_password_change": datetime.now(pytz.utc), }, # It will be expanded with the default role roles=[], ) auth.save_user(another_user) assert not auth.verify_token_validity(jti, another_user)
def test_password_management(self, faker: Faker) -> None: # Always enable during core tests if not Connector.check_availability( "authentication"): # pragma: no cover log.warning("Skipping authentication test: service not available") return auth = Connector.get_authentication_instance() min_pwd_len = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 9999) pwd = faker.password(min_pwd_len - 1) ret_val, ret_text = auth.verify_password_strength(pwd, pwd) assert not ret_val assert ret_text == "The new password cannot match the previous password" pwd = faker.password(min_pwd_len - 1) old_pwd = faker.password(min_pwd_len) ret_val, ret_text = auth.verify_password_strength(pwd, old_pwd) assert not ret_val error = f"Password is too short, use at least {min_pwd_len} characters" assert ret_text == error pwd = faker.password(min_pwd_len, low=False, up=True) ret_val, ret_text = auth.verify_password_strength(pwd, old_pwd) assert not ret_val assert ret_text == "Password is too weak, missing lower case letters" pwd = faker.password(min_pwd_len, low=True) ret_val, ret_text = auth.verify_password_strength(pwd, old_pwd) assert not ret_val assert ret_text == "Password is too weak, missing upper case letters" pwd = faker.password(min_pwd_len, low=True, up=True) ret_val, ret_text = auth.verify_password_strength(pwd, old_pwd) assert not ret_val assert ret_text == "Password is too weak, missing numbers" pwd = faker.password(min_pwd_len, low=True, up=True, digits=True) ret_val, ret_text = auth.verify_password_strength(pwd, old_pwd) assert not ret_val assert ret_text == "Password is too weak, missing special characters" pwd = faker.password(min_pwd_len, low=True, up=True, digits=True, symbols=True) ret_val, ret_text = auth.verify_password_strength(pwd, old_pwd) assert ret_val assert ret_text is None # How to retrieve a generic user? user = None pwd = faker.password(min_pwd_len - 1) try: auth.change_password(user, pwd, None, None) pytest.fail("None password!") # pragma: no cover except RestApiException as e: assert e.status_code == 400 assert str(e) == "Missing new password" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: auth.change_password(user, pwd, pwd, None) pytest.fail("None password!") # pragma: no cover except RestApiException as e: assert e.status_code == 400 assert str(e) == "Missing password confirmation" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: # wrong confirmation auth.change_password(user, pwd, pwd, faker.password(strong=True)) pytest.fail("wrong password confirmation!?") # pragma: no cover except RestApiException as e: assert e.status_code == 409 assert str(e) == "Your password doesn't match the confirmation" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: auth.change_password(user, pwd, pwd, pwd) pytest.fail("Password strength not verified") # pragma: no cover except RestApiException as e: assert e.status_code == 409 assert str( e) == "The new password cannot match the previous password" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: # the first password parameter is only checked for new password strenght # i.e. is verified password != newpassword # pwd validity will be checked once completed checks on new password # => a random current password is ok here auth.change_password(user, faker.password(), pwd, pwd) pytest.fail("Password strength not verified") # pragma: no cover except RestApiException as e: assert e.status_code == 409 assert ( str(e) == f"Password is too short, use at least {min_pwd_len} characters" ) except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: auth.verify_totp(None, None) # type: ignore pytest.fail("NULL totp accepted!") # pragma: no cover except RestApiException as e: assert e.status_code == 401 assert str(e) == "Verification code is missing" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") auth = Connector.get_authentication_instance() pwd1 = faker.password(strong=True) pwd2 = faker.password(strong=True) hash_1 = auth.get_password_hash(pwd1) assert len(hash_1) > 0 assert hash_1 != auth.get_password_hash(pwd2) try: auth.get_password_hash("") pytest.fail("Hashed a empty password!") # pragma: no cover except RestApiException as e: assert e.status_code == 401 assert str(e) == "Invalid password" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") try: auth.get_password_hash(None) pytest.fail("Hashed a None password!") # pragma: no cover except RestApiException as e: assert e.status_code == 401 assert str(e) == "Invalid password" except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") assert auth.verify_password(pwd1, hash_1) try: auth.verify_password(None, hash_1) # type: ignore pytest.fail("Hashed a None password!") # pragma: no cover except TypeError: pass except BaseException: # pragma: no cover pytest.fail("Unexpected exception raised") assert not auth.verify_password(pwd1, None) # type: ignore assert not auth.verify_password(None, None) # type: ignore
from restapi.config import get_project_configuration from restapi.connectors import Connector, smtp from restapi.endpoints.profile_activation import send_activation_link from restapi.env import Env from restapi.exceptions import Conflict, ServiceUnavailable from restapi.models import Schema, fields, validate from restapi.rest.definition import EndpointResource, Response from restapi.services.authentication import DEFAULT_GROUP_NAME from restapi.utilities.globals import mem # from restapi.utilities.logs import log # This endpoint requires the server to send the activation token via email if Connector.check_availability("smtp"): auth = Connector.get_authentication_instance() # Note that these are callables returning a model, not models! # They will be executed a runtime def getInputSchema(request): # as defined in Marshmallow.schema.from_dict attributes: Dict[str, Union[fields.Field, type]] = {} attributes["name"] = fields.Str(required=True) attributes["surname"] = fields.Str(required=True) attributes["email"] = fields.Email(required=True, label="Username (email address)") attributes["password"] = fields.Str( required=True, password=True,
def test_admin_users(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("MAIN_LOGIN_ENABLE") or not Env.get_bool( "AUTH_ENABLE"): log.warning("Skipping admin/users tests") return project_tile = get_project_configuration("project.title", default="YourProject") auth = Connector.get_authentication_instance() staff_role_enabled = Role.STAFF.value in [ r.name for r in auth.get_roles() ] for role in ( Role.ADMIN, Role.STAFF, ): if not staff_role_enabled: # pragma: no cover log.warning( "Skipping tests of admin/users endpoints, role Staff not enabled" ) continue else: log.warning("Testing admin/users endpoints as {}", role) if role == Role.ADMIN: user_email = BaseAuthentication.default_user user_password = BaseAuthentication.default_password elif role == Role.STAFF: _, user_data = self.create_user(client, roles=[Role.STAFF]) user_email = user_data.get("email") user_password = user_data.get("password") headers, _ = self.do_login(client, user_email, user_password) r = client.get(f"{API_URI}/admin/users", headers=headers) assert r.status_code == 200 schema = self.get_dynamic_input_schema(client, "admin/users", headers) data = self.buildData(schema) data["email_notification"] = True data["is_active"] = True data["expiration"] = None # Event 1: create r = client.post(f"{API_URI}/admin/users", json=data, headers=headers) assert r.status_code == 200 uuid = self.get_content(r) assert isinstance(uuid, str) # A new User is created events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.create.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].url == "/api/admin/users" assert "name" in events[0].payload assert "surname" in events[0].payload assert "email" in events[0].payload # Save it for the following tests event_target_id1 = events[0].target_id mail = self.read_mock_email() body = mail.get("body", "") # Subject: is a key in the MIMEText assert body is not None assert mail.get("headers") is not None assert f"Subject: {project_tile}: New credentials" in mail.get( "headers", "") assert data.get("email", "MISSING").lower() in body assert (data.get("password", "MISSING") in body or escape(str(data.get("password"))) in body) # Test the differences between post and put schema post_schema = {s["key"]: s for s in schema} tmp_schema = self.get_dynamic_input_schema(client, f"admin/users/{uuid}", headers, method="put") put_schema = {s["key"]: s for s in tmp_schema} assert "email" in post_schema assert post_schema["email"]["required"] assert "email" not in put_schema assert "name" in post_schema assert post_schema["name"]["required"] assert "name" in put_schema assert not put_schema["name"]["required"] assert "surname" in post_schema assert post_schema["surname"]["required"] assert "surname" in put_schema assert not put_schema["surname"]["required"] assert "password" in post_schema assert post_schema["password"]["required"] assert "password" in put_schema assert not put_schema["password"]["required"] assert "group" in post_schema assert post_schema["group"]["required"] assert "group" in put_schema assert not put_schema["group"]["required"] # Event 2: read r = client.get(f"{API_URI}/admin/users/{uuid}", headers=headers) assert r.status_code == 200 users_list = self.get_content(r) assert isinstance(users_list, dict) assert len(users_list) > 0 # email is saved lowercase assert users_list.get("email") == data.get("email", "MISSING").lower() # Access to the user events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.access.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id == event_target_id1 assert events[0].url == f"/api/admin/users/{event_target_id1}" assert len(events[0].payload) == 0 # Check duplicates r = client.post(f"{API_URI}/admin/users", json=data, headers=headers) assert r.status_code == 409 assert (self.get_content(r) == f"A User already exists with email: {data['email']}") data["email"] = BaseAuthentication.default_user r = client.post(f"{API_URI}/admin/users", json=data, headers=headers) assert r.status_code == 409 assert ( self.get_content(r) == f"A User already exists with email: {BaseAuthentication.default_user}" ) # Create another user data2 = self.buildData(schema) data2["email_notification"] = True data2["is_active"] = True data2["expiration"] = None # Event 3: create r = client.post(f"{API_URI}/admin/users", json=data2, headers=headers) assert r.status_code == 200 uuid2 = self.get_content(r) assert isinstance(uuid2, str) # Another User is created events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.create.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id != event_target_id1 assert events[0].url == "/api/admin/users" assert "name" in events[0].payload assert "surname" in events[0].payload assert "email" in events[0].payload # Save it for the following tests event_target_id2 = events[0].target_id mail = self.read_mock_email() body = mail.get("body", "") # Subject: is a key in the MIMEText assert body is not None assert mail.get("headers") is not None assert f"Subject: {project_tile}: New credentials" in mail.get( "headers", "") assert data2.get("email", "MISSING").lower() in body pwd = data2.get("password", "MISSING") assert pwd in body or escape(str(pwd)) in body # send and invalid user_id r = client.put( f"{API_URI}/admin/users/invalid", json={"name": faker.name()}, headers=headers, ) assert r.status_code == 404 # Event 4: modify r = client.put( f"{API_URI}/admin/users/{uuid}", json={"name": faker.name()}, headers=headers, ) assert r.status_code == 204 # User 1 modified (same target_id as above) events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.modify.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id == event_target_id1 assert events[0].url == f"/api/admin/users/{event_target_id1}" assert "name" in events[0].payload assert "surname" not in events[0].payload assert "email" not in events[0].payload assert "password" not in events[0].payload # email cannot be modified new_data = {"email": data.get("email")} r = client.put(f"{API_URI}/admin/users/{uuid2}", json=new_data, headers=headers) # from webargs >= 6 this endpoint no longer return a 204 but a 400 # because email is an unknown field # assert r.status_code == 204 assert r.status_code == 400 # Event 5: read r = client.get(f"{API_URI}/admin/users/{uuid2}", headers=headers) assert r.status_code == 200 users_list = self.get_content(r) assert isinstance(users_list, dict) assert len(users_list) > 0 # email is not modified -> still equal to data2, not data1 assert users_list.get("email") != data.get("email", "MISSING").lower() assert users_list.get("email") == data2.get("email", "MISSING").lower() # Access to user 2 events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.access.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id == event_target_id2 assert events[0].url == f"/api/admin/users/{event_target_id2}" assert len(events[0].payload) == 0 r = client.delete(f"{API_URI}/admin/users/invalid", headers=headers) assert r.status_code == 404 # Event 6: delete r = client.delete(f"{API_URI}/admin/users/{uuid}", headers=headers) assert r.status_code == 204 # User 1 is deleted (same target_id as above) events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.delete.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id == event_target_id1 assert events[0].url == f"/api/admin/users/{event_target_id1}" assert len(events[0].payload) == 0 r = client.get(f"{API_URI}/admin/users/{uuid}", headers=headers) assert r.status_code == 404 # change password of user2 # Event 7: modify newpwd = faker.password(strong=True) data = {"password": newpwd, "email_notification": True} r = client.put(f"{API_URI}/admin/users/{uuid2}", json=data, headers=headers) assert r.status_code == 204 # User 2 modified (same target_id as above) events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.modify.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id == event_target_id2 assert events[0].url == f"/api/admin/users/{event_target_id2}" assert "name" not in events[0].payload assert "surname" not in events[0].payload assert "email" not in events[0].payload assert "password" in events[0].payload assert "email_notification" in events[0].payload # Verify that the password is obfuscated in the log: assert events[0].payload["password"] == OBSCURE_VALUE mail = self.read_mock_email() # Subject: is a key in the MIMEText assert mail.get("body", "") is not None assert mail.get("headers", "") is not None assert f"Subject: {project_tile}: Password changed" in mail.get( "headers", "") assert data2.get("email", "MISSING").lower() in mail.get("body", "") assert newpwd in mail.get( "body", "") or escape(newpwd) in mail.get("body", "") # login with a newly created user headers2, _ = self.do_login(client, data2.get("email"), newpwd) # normal users cannot access to this endpoint r = client.get(f"{API_URI}/admin/users", headers=headers2) assert r.status_code == 401 r = client.get(f"{API_URI}/admin/users/{uuid}", headers=headers2) assert r.status_code == 401 r = client.post(f"{API_URI}/admin/users", json=data, headers=headers2) assert r.status_code == 401 r = client.put( f"{API_URI}/admin/users/{uuid}", json={"name": faker.name()}, headers=headers2, ) assert r.status_code == 401 r = client.delete(f"{API_URI}/admin/users/{uuid}", headers=headers2) assert r.status_code == 401 # Users are not authorized to /admin/tokens # These two tests should be moved in test_endpoints_tokens.py r = client.get(f"{API_URI}/admin/tokens", headers=headers2) assert r.status_code == 401 r = client.delete(f"{API_URI}/admin/tokens/xyz", headers=headers2) assert r.status_code == 401 # let's delete the second user # Event 8: delete r = client.delete(f"{API_URI}/admin/users/{uuid2}", headers=headers) assert r.status_code == 204 # User 2 is deleted (same target_id as above) events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.delete.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id == event_target_id2 assert events[0].url == f"/api/admin/users/{event_target_id2}" assert len(events[0].payload) == 0 # Restore the default password (changed due to FORCE_FIRST_PASSWORD_CHANGE) # or MAX_PASSWORD_VALIDITY errors r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) uuid = str(content.get("uuid")) data = { "password": user_password, # very important, otherwise the default user will lose its role "roles": orjson.dumps([role]).decode("UTF8"), } # Event 9: modify r = client.put(f"{API_URI}/admin/users/{uuid}", json=data, headers=headers) assert r.status_code == 204 # Default user is modified events = self.get_last_events(1, filters={"target_type": "User"}) assert events[0].event == Events.modify.value assert events[0].user == user_email assert events[0].target_type == "User" assert events[0].target_id != event_target_id1 assert events[0].target_id != event_target_id2 assert events[0].url != f"/api/admin/users/{event_target_id1}" assert events[0].url != f"/api/admin/users/{event_target_id2}" assert "name" not in events[0].payload assert "surname" not in events[0].payload assert "email" not in events[0].payload assert "password" in events[0].payload assert "roles" in events[0].payload assert "email_notification" not in events[0].payload # Verify that the password is obfuscated in the log: assert events[0].payload["password"] == OBSCURE_VALUE r = client.get(f"{AUTH_URI}/logout", headers=headers) assert r.status_code == 204
def test_02_unlock_token(self, client: FlaskClient) -> None: if not Env.get_bool("MAIN_LOGIN_ENABLE"): # pragma: no cover log.warning("Skipping admin/users tests") return uuid, data = self.create_user(client) self.delete_mock_email() for _ in range(0, max_login_attempts): self.do_login(client, data["email"], "wrong", status_code=401) token = self.verify_credentials_ban_notification() # This should fail headers, _ = self.do_login( client, data["email"], data["password"], status_code=403 ) assert headers is None auth = Connector.get_authentication_instance() logins = auth.get_logins(data["email"]) login = logins[-1] assert login.username == data["email"] assert login.failed assert not login.flushed logins = auth.get_logins(data["email"], only_unflushed=True) assert len(logins) > 0 # Check if token is valid r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 200 events = self.get_last_events(1) assert events[0].event == Events.login_unlock.value assert events[0].user == data["email"] assert events[0].target_type == "User" assert events[0].url == f"/auth/login/unlock/{token}" logins = auth.get_logins(data["email"]) login = logins[-1] assert login.username == data["email"] assert login.failed assert login.flushed logins = auth.get_logins(data["email"], only_unflushed=True) assert len(logins) == 0 # Now credentials are unlock again :-) headers, _ = self.do_login(client, data["email"], data["password"]) assert headers is not None # Unlock token can be used twice r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 # Verify that unlock tokens can't be used if the user is already unlocked for _ in range(0, max_login_attempts): self.do_login(client, data["email"], "wrong", status_code=401) token = self.verify_credentials_ban_notification() # This should fail headers, _ = self.do_login( client, data["email"], data["password"], status_code=403 ) assert headers is None time.sleep(ban_duration) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 # Verify that unlock tokens are invalidated by new tokens for _ in range(0, max_login_attempts): self.do_login(client, data["email"], "wrong", status_code=401) first_token = self.verify_credentials_ban_notification() # This should fail headers, _ = self.do_login( client, data["email"], data["password"], status_code=403 ) assert headers is None time.sleep(ban_duration) for _ in range(0, max_login_attempts): self.do_login(client, data["email"], "wrong", status_code=401) second_token = self.verify_credentials_ban_notification() assert first_token != second_token r = client.post(f"{AUTH_URI}/login/unlock/{first_token}") assert r.status_code == 400 r = client.post(f"{AUTH_URI}/login/unlock/{second_token}") assert r.status_code == 200 # Test invalid tokens # Token created for another user token = self.get_crafted_token("u") r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token" # Token created with a wrong algorithm token = self.get_crafted_token("u", wrong_algorithm=True) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token" # Token created with a wrong secret token = self.get_crafted_token("u", wrong_secret=True) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token" # Token created for another user headers, _ = self.do_login(client, None, None) r = client.get(f"{AUTH_URI}/profile", headers=headers) assert r.status_code == 200 response = self.get_content(r) assert isinstance(response, dict) uuid = str(response.get("uuid")) token = self.get_crafted_token("x", user_id=uuid) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token" # token created for the correct user, but from outside the system!! token = self.get_crafted_token("u", user_id=uuid) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token" # Immature token token = self.get_crafted_token("u", user_id=uuid, immature=True) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token" # Expired token token = self.get_crafted_token("u", user_id=uuid, expired=True) r = client.post(f"{AUTH_URI}/login/unlock/{token}") assert r.status_code == 400 c = self.get_content(r) assert c == "Invalid unlock token: this request is expired"
def test_staff_restrictions(self, client: FlaskClient, faker: Faker) -> None: if not Env.get_bool("MAIN_LOGIN_ENABLE") or not Env.get_bool( "AUTH_ENABLE"): log.warning("Skipping admin/users tests") return auth = Connector.get_authentication_instance() staff_role_enabled = Role.STAFF.value in [ r.name for r in auth.get_roles() ] if not staff_role_enabled: # pragma: no cover log.warning( "Skipping tests of admin/users restrictions, role Staff not enabled" ) return staff_uuid, staff_data = self.create_user(client, roles=[Role.STAFF]) staff_email = staff_data.get("email") staff_password = staff_data.get("password") staff_headers, _ = self.do_login(client, staff_email, staff_password) user_uuid, _ = self.create_user(client, roles=[Role.USER]) admin_headers, _ = self.do_login(client, None, None) r = client.get(f"{AUTH_URI}/profile", headers=admin_headers) assert r.status_code == 200 content = self.get_content(r) assert isinstance(content, dict) admin_uuid = content.get("uuid") # Staff users are not allowed to retrieve Admins' data r = client.get(f"{API_URI}/admin/users/{user_uuid}", headers=admin_headers) assert r.status_code == 200 r = client.get(f"{API_URI}/admin/users/{staff_uuid}", headers=admin_headers) assert r.status_code == 200 r = client.get(f"{API_URI}/admin/users/{admin_uuid}", headers=admin_headers) assert r.status_code == 200 r = client.get(f"{API_URI}/admin/users/{user_uuid}", headers=staff_headers) assert r.status_code == 200 r = client.get(f"{API_URI}/admin/users/{staff_uuid}", headers=staff_headers) assert r.status_code == 200 r = client.get(f"{API_URI}/admin/users/{admin_uuid}", headers=staff_headers) assert r.status_code == 404 content = self.get_content(r) assert content == "This user cannot be found or you are not authorized" # Staff users are not allowed to edit Admins r = client.put( f"{API_URI}/admin/users/{admin_uuid}", json={ "name": faker.name(), "roles": orjson.dumps([Role.STAFF]).decode("UTF8"), }, headers=staff_headers, ) assert r.status_code == 404 content = self.get_content(r) assert content == "This user cannot be found or you are not authorized" r = client.put( f"{API_URI}/admin/users/{staff_uuid}", json={ "name": faker.name(), "roles": orjson.dumps([Role.STAFF]).decode("UTF8"), }, headers=staff_headers, ) assert r.status_code == 204 r = client.put( f"{API_URI}/admin/users/{user_uuid}", json={ "name": faker.name(), "roles": orjson.dumps([Role.USER]).decode("UTF8"), }, headers=staff_headers, ) assert r.status_code == 204 # Admin role is not allowed for Staff users tmp_schema = self.get_dynamic_input_schema(client, "admin/users", admin_headers) post_schema = {s["key"]: s for s in tmp_schema} assert "roles" in post_schema assert "options" in post_schema["roles"] assert "normal_user" in post_schema["roles"]["options"] assert "admin_root" in post_schema["roles"]["options"] tmp_schema = self.get_dynamic_input_schema(client, f"admin/users/{user_uuid}", admin_headers, method="put") put_schema = {s["key"]: s for s in tmp_schema} assert "roles" in put_schema assert "options" in post_schema["roles"] assert "normal_user" in post_schema["roles"]["options"] assert "admin_root" in post_schema["roles"]["options"] tmp_schema = self.get_dynamic_input_schema(client, "admin/users", staff_headers) post_schema = {s["key"]: s for s in tmp_schema} assert "roles" in post_schema assert "options" in post_schema["roles"] assert "normal_user" in post_schema["roles"]["options"] assert "admin_root" not in post_schema["roles"]["options"] tmp_schema = self.get_dynamic_input_schema(client, f"admin/users/{user_uuid}", staff_headers, method="put") put_schema = {s["key"]: s for s in tmp_schema} assert "roles" in put_schema assert "options" in post_schema["roles"] assert "normal_user" in post_schema["roles"]["options"] assert "admin_root" not in post_schema["roles"]["options"] # Staff can't send role admin on put r = client.put( f"{API_URI}/admin/users/{user_uuid}", json={ "name": faker.name(), "roles": orjson.dumps([Role.ADMIN]).decode("UTF8"), }, headers=staff_headers, ) assert r.status_code == 400 # Staff can't send role admin on post schema = self.get_dynamic_input_schema(client, "admin/users", staff_headers) data = self.buildData(schema) data["email_notification"] = True data["is_active"] = True data["expiration"] = None data["roles"] = orjson.dumps([Role.ADMIN]).decode("UTF8") r = client.post(f"{API_URI}/admin/users", json=data, headers=staff_headers) assert r.status_code == 400 # Admin users are filtered out when asked from a Staff user r = client.get(f"{API_URI}/admin/users", headers=admin_headers) assert r.status_code == 200 users_list = self.get_content(r) assert isinstance(users_list, list) assert len(users_list) > 0 email_list = [u.get("email") for u in users_list] assert staff_email in email_list assert BaseAuthentication.default_user in email_list r = client.get(f"{API_URI}/admin/users", headers=staff_headers) assert r.status_code == 200 users_list = self.get_content(r) assert isinstance(users_list, list) assert len(users_list) > 0 email_list = [u.get("email") for u in users_list] assert staff_email in email_list assert BaseAuthentication.default_user not in email_list # Staff users are not allowed to delete Admins r = client.delete(f"{API_URI}/admin/users/{admin_uuid}", headers=staff_headers) assert r.status_code == 404 content = self.get_content(r) assert content == "This user cannot be found or you are not authorized" r = client.delete(f"{API_URI}/admin/users/{user_uuid}", headers=staff_headers) assert r.status_code == 204
def test_staff(self, client: FlaskClient) -> None: if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping staff authorizations tests") return auth = Connector.get_authentication_instance() auth.get_roles() if Role.STAFF.value not in [r.name for r in auth.get_roles() ]: # pragma: no cover log.warning( "Skipping authorization tests on role Staff (not enabled)") return # List of all paths to be tested. After each test a path will be removed. # At the end the list is expected to be empty paths = self.get_paths(client) uuid, data = self.create_user(client, roles=[Role.STAFF]) headers, _ = self.do_login(client, data.get("email"), data.get("password")) # These are public paths = self.check_endpoint(client, "GET", "/api/status", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/specs", headers, True, paths) paths = self.check_endpoint(client, "POST", "/auth/login", headers, True, paths) if Env.get_int("AUTH_MAX_LOGIN_ATTEMPTS") > 0: paths = self.check_endpoint(client, "POST", "/auth/login/unlock/<token>", headers, True, paths) if Env.get_bool("ALLOW_REGISTRATION"): paths = self.check_endpoint(client, "POST", "/auth/profile", headers, True, paths) paths = self.check_endpoint(client, "POST", "/auth/profile/activate", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/auth/profile/activate/<token>", headers, True, paths) if Env.get_bool("ALLOW_PASSWORD_RESET" ) and Connector.check_availability("smtp"): paths = self.check_endpoint(client, "POST", "/auth/reset", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/auth/reset/<token>", headers, True, paths) # These are allowed to each user paths = self.check_endpoint(client, "GET", "/auth/status", headers, True, paths) paths = self.check_endpoint(client, "GET", "/auth/profile", headers, True, paths) paths = self.check_endpoint(client, "PATCH", "/auth/profile", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/auth/profile", headers, True, paths) paths = self.check_endpoint(client, "GET", "/auth/tokens", headers, True, paths) paths = self.check_endpoint(client, "DELETE", "/auth/tokens/<token>", headers, True, paths) # These are allowed to coordinators paths = self.check_endpoint(client, "GET", "/api/group/users", headers, False, paths) # These are allowed to staff # ... none # These are allowed to admins paths = self.check_endpoint(client, "GET", "/api/admin/users", headers, False, paths) paths = self.check_endpoint(client, "GET", "/api/admin/users/<user_id>", headers, False, paths) paths = self.check_endpoint(client, "POST", "/api/admin/users", headers, False, paths) paths = self.check_endpoint(client, "PUT", "/api/admin/users/<user_id>", headers, False, paths) paths = self.check_endpoint(client, "DELETE", "/api/admin/users/<user_id>", headers, False, paths) paths = self.check_endpoint(client, "GET", "/api/admin/groups", headers, True, paths) paths = self.check_endpoint(client, "POST", "/api/admin/groups", headers, True, paths) paths = self.check_endpoint(client, "PUT", "/api/admin/groups/<group_id>", headers, True, paths) paths = self.check_endpoint(client, "DELETE", "/api/admin/groups/<group_id>", headers, True, paths) paths = self.check_endpoint(client, "GET", "/api/admin/logins", headers, False, paths) paths = self.check_endpoint(client, "GET", "/api/admin/tokens", headers, False, paths) paths = self.check_endpoint(client, "DELETE", "/api/admin/tokens/<token>", headers, False, paths) paths = self.check_endpoint(client, "GET", "/api/admin/stats", headers, False, paths) paths = self.check_endpoint(client, "POST", "/api/admin/mail", headers, False, paths) # logout MUST be the last one or the token will be invalidated!! :-) paths = self.check_endpoint(client, "GET", "/auth/logout", headers, True, paths) assert paths == [] self.delete_user(client, uuid)
def test_01_login(self, client: FlaskClient, faker: Faker) -> None: """Check that you can login and receive back your token""" if not Env.get_bool("AUTH_ENABLE"): log.warning("Skipping login tests") return log.info("*** VERIFY CASE INSENSITIVE LOGIN") # BaseAuthentication.load_default_user() # BaseAuthentication.load_roles() USER = BaseAuthentication.default_user or "just-to-prevent-None" PWD = BaseAuthentication.default_password or "just-to-prevent-None" # Login by using upper case username self.do_login(client, USER.upper(), PWD) events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == USER assert events[0].url == "/auth/login" auth = Connector.get_authentication_instance() logins = auth.get_logins(USER) login = logins[-1] assert login.username == USER # Wrong credentials # Off course PWD cannot be upper :D self.do_login(client, USER, PWD.upper(), status_code=401) events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].payload["username"] == USER assert events[0].url == "/auth/login" logins = auth.get_logins(USER) login = logins[-1] assert login.username == USER log.info("*** VERIFY valid credentials") # Login by using normal username (no upper case) headers, _ = self.do_login(client, None, None) events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == USER assert events[0].url == "/auth/login" time.sleep(5) # Verify MAX_PASSWORD_VALIDITY, if set headers, token = self.do_login(client, None, None) events = self.get_last_events(1) assert events[0].event == Events.login.value assert events[0].user == USER assert events[0].url == "/auth/login" self.save("auth_header", headers) self.save("auth_token", token) # Verify credentials r = client.get(f"{AUTH_URI}/status", headers=headers) assert r.status_code == 200 c = self.get_content(r) assert isinstance(c, bool) and c # this check verifies a BUG with neo4j causing crash of auth module # when using a non-email-username to authenticate log.info("*** VERIFY with a non-email-username") self.do_login( client, "notanemail", "[A-Za-z0-9]+", status_code=400, ) # Check failure log.info("*** VERIFY invalid credentials") random_email = faker.ascii_email() self.do_login( client, random_email, faker.password(strong=True), status_code=401, ) events = self.get_last_events(1) assert events[0].event == Events.failed_login.value assert events[0].payload["username"] == random_email assert events[0].url == "/auth/login"
def post( self, name: str, surname: str, email: str, password: str, password_confirm: str, **kwargs: Any, ) -> Response: """Register new user""" user = self.auth.get_user(username=email) if user is not None: raise Conflict(f"This user already exists: {email}") if password != password_confirm: raise Conflict("Your password doesn't match the confirmation") check, msg = self.auth.verify_password_strength( pwd=password, old_pwd=None, email=email, name=name, surname=surname, ) if not check: raise Conflict(msg) kwargs["name"] = name kwargs["surname"] = surname kwargs["email"] = email kwargs["password"] = password kwargs["is_active"] = False user = self.auth.create_user(kwargs, [self.auth.default_role]) default_group = self.auth.get_group(name=DEFAULT_GROUP_NAME) self.auth.add_user_to_group(user, default_group) self.auth.save_user(user) self.log_event(self.events.create, user, kwargs) try: auth = Connector.get_authentication_instance() activation_token, payload = auth.create_temporary_token( user, auth.ACTIVATE_ACCOUNT ) server_url = get_frontend_url() rt = activation_token.replace(".", "+") log.debug("Activation token: {}", rt) url = f"{server_url}/public/register/{rt}" sent = send_activation_link(user, url) if not sent: # pragma: no cover raise ServiceUnavailable("Error sending email, please retry") auth.save_token( user, activation_token, payload, token_type=auth.ACTIVATE_ACCOUNT ) # Sending an email to the administrator if Env.get_bool("REGISTRATION_NOTIFICATIONS"): send_registration_notification(user) except Exception as e: # pragma: no cover self.auth.delete_user(user) raise ServiceUnavailable(f"Errors during account registration: {e}") return self.response( "We are sending an email to your email address where " "you will find the link to activate your account" )