Пример #1
0
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
Пример #2
0
    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
Пример #3
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)
Пример #4
0
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
Пример #5
0
    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()
Пример #6
0
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
Пример #7
0
    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
Пример #8
0
    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
Пример #9
0
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
Пример #10
0
    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))
Пример #11
0
    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
Пример #12
0
    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
Пример #13
0
    def auth(self) -> BaseAuthentication:
        if not self.__auth:
            self.__auth = Connector.get_authentication_instance()

        return self.__auth
Пример #14
0
    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
Пример #15
0
    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
Пример #16
0
    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"
Пример #17
0
    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)
Пример #18
0
    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
Пример #19
0
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,
Пример #20
0
    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
Пример #21
0
        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"
Пример #22
0
    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
Пример #23
0
    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)
Пример #24
0
    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"
Пример #25
0
        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"
            )