def test_06_token_ip_validity(self, client: FlaskClient,
                                  faker: Faker) -> None:

        if Env.get_bool("MAIN_LOGIN_ENABLE"):
            if Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD") < 10:
                headers, _ = self.do_login(client, None, None)

                r = client.get(f"{AUTH_URI}/status", headers=headers)
                assert r.status_code == 200

                r = client.get(
                    f"{AUTH_URI}/status",
                    headers=headers,
                    environ_base={"REMOTE_ADDR": faker.ipv4()},
                )
                assert r.status_code == 200

                time.sleep(Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD"))

                r = client.get(
                    f"{AUTH_URI}/status",
                    headers=headers,
                    environ_base={"REMOTE_ADDR": faker.ipv4()},
                )
                log.error(
                    "DEBUG CODE: this 401 should be due to invalid IP address")
                assert r.status_code == 401

                # After the failure the token is still valid if used from the corret IP
                r = client.get(f"{AUTH_URI}/status", headers=headers)
                assert r.status_code == 200

                # Another option to provide IP is through the header passed by nginx
                headers["X-Forwarded-For"] = faker.ipv4()  # type: ignore
                r = client.get(f"{AUTH_URI}/status", headers=headers)
                assert r.status_code == 401
Exemplo n.º 2
0
    def test_06_token_ip_validity(self, client: FlaskClient, faker: Faker) -> None:

        if Env.get_bool("MAIN_LOGIN_ENABLE") and Env.get_bool("AUTH_ENABLE"):
            if Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD") < 10:
                headers, _ = self.do_login(client, None, None)

                r = client.get(f"{AUTH_URI}/status", headers=headers)
                assert r.status_code == 200

                r = client.get(
                    f"{AUTH_URI}/status",
                    headers=headers,
                    environ_base={"REMOTE_ADDR": faker.ipv4()},
                )
                assert r.status_code == 200

                time.sleep(Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD"))

                r = client.get(
                    f"{AUTH_URI}/status",
                    headers=headers,
                    environ_base={"REMOTE_ADDR": faker.ipv4()},
                )
                assert r.status_code == 401

                # After the failure the token is still valid if used from the correct IP
                r = client.get(f"{AUTH_URI}/status", headers=headers)
                assert r.status_code == 200

                # Another option to provide IP is through the header passed by nginx
                # This only works if PROXIED_CONNECTION is on
                # (disabled by default, for security purpose)
                if Env.get_bool("PROXIED_CONNECTION"):
                    headers["X-Forwarded-For"] = faker.ipv4()  # type: ignore
                    r = client.get(f"{AUTH_URI}/status", headers=headers)
                    assert r.status_code == 401
Exemplo n.º 3
0
"""
This will used credentials created at the beginning of the suite
to verify that unused credentialas are banned
"""
from faker import Faker

from restapi.env import Env
from restapi.tests import AUTH_URI, BaseTests, FlaskClient
from restapi.utilities.logs import Events

if Env.get_int("AUTH_DISABLE_UNUSED_CREDENTIALS_AFTER") > 0:

    class TestApp1(BaseTests):
        def test_test_unused_credentials(self, client: FlaskClient,
                                         faker: Faker) -> None:

            assert BaseTests.unused_credentials is not None
            assert len(BaseTests.unused_credentials) == 3

            data = {
                "username": BaseTests.unused_credentials[0],
                "password": faker.password(strong=True),
            }

            # Credentials are verified before the inactivity check
            r = client.post(f"{AUTH_URI}/login", data=data)
            assert r.status_code == 401
            resp = self.get_content(r)
            assert resp == "Invalid access credentials"

            data = {
Exemplo n.º 4
0
MODELS_DIR = "models"
CONF_PATH = Path("confs")
# Also configured in controller
EXTENDED_PROJECT_DISABLED = "no_extended_project"
BACKEND_PACKAGE = "restapi"  # package inside rapydo-http

CUSTOM_PACKAGE = Env.get("PROJECT_NAME", "custom")
EXTENDED_PACKAGE = Env.get("EXTENDED_PACKAGE", EXTENDED_PROJECT_DISABLED)

SENTRY_URL = Env.get("SENTRY_URL", "").strip() or None

ABS_RESTAPI_PATH = Path(__file__).resolve().parent

GZIP_ENABLE = Env.get_bool("GZIP_COMPRESSION_ENABLE")
GZIP_THRESHOLD = max(0, Env.get_int("GZIP_COMPRESSION_THRESHOLD"))
GZIP_LEVEL = max(1, min(9, Env.get_int("GZIP_COMPRESSION_LEVEL")))


@lru_cache
def get_project_configuration(key: str, default: str) -> str:
    return glom(mem.configuration, key, default=default)


@lru_cache
def get_backend_url() -> str:

    BACKEND_URL = Env.get("BACKEND_URL", "")

    if BACKEND_URL:
        return BACKEND_URL
Exemplo n.º 5
0
from restapi.connectors import celery, neo4j
from restapi.env import Env
from restapi.utilities.logs import log

log.info("Starting init pipeline cron")
# get the list of datasets ready to be analysed
graph = neo4j.get_instance()

datasets_to_analise = graph.Dataset.nodes.filter(
    status="UPLOAD COMPLETED").all()

# get all the dataset uuid
datasets_uuid = [x.uuid for x in datasets_to_analise]

chunks_limit = Env.get_int("CHUNKS_LIMIT", 16)

for chunk in [
        datasets_uuid[i:i + chunks_limit]
        for i in range(0, len(datasets_uuid), chunks_limit)
]:
    log.info("Sending pipeline for datasets: {}", chunk)
    # pass the chunk to the celery task
    c = celery.get_instance()
    task = c.celery_app.send_task(
        "launch_pipeline",
        args=(chunk, ),
        countdown=1,
    )
    log.info("{} datasets sent to task {}", len(chunk), task)
    # mark the related datasets as "QUEUED"
    def test_registration(self, client: FlaskClient, faker: Faker) -> None:

        # Always enabled during core tests
        if not Env.get_bool("ALLOW_REGISTRATION"):  # pragma: no cover
            log.warning("User registration is disabled, skipping tests")
            return

        project_tile = get_project_configuration("project.title",
                                                 default="YourProject")
        proto = "https" if PRODUCTION else "http"

        # registration, empty input
        r = client.post(f"{AUTH_URI}/profile")
        assert r.status_code == 400

        # registration, missing information
        r = client.post(f"{AUTH_URI}/profile", data={"x": "y"})
        assert r.status_code == 400
        registration_data = {}
        registration_data["password"] = faker.password(5)
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        assert r.status_code == 400
        registration_data["email"] = BaseAuthentication.default_user
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        assert r.status_code == 400
        registration_data["name"] = faker.first_name()
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        assert r.status_code == 400

        registration_data["surname"] = faker.last_name()
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        assert r.status_code == 400

        registration_data["password_confirm"] = faker.password(strong=True)
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        assert r.status_code == 400

        min_pwd_len = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 9999)

        registration_data["password"] = faker.password(min_pwd_len - 1)
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        assert r.status_code == 400

        registration_data["password"] = faker.password(min_pwd_len)
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        assert r.status_code == 409
        m = f"This user already exists: {BaseAuthentication.default_user}"
        assert self.get_content(r) == m

        registration_data["email"] = faker.ascii_email()
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        assert r.status_code == 409
        assert self.get_content(
            r) == "Your password doesn't match the confirmation"

        registration_data["password"] = faker.password(min_pwd_len,
                                                       low=False,
                                                       up=True)
        registration_data["password_confirm"] = registration_data["password"]
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        assert r.status_code == 409
        m = "Password is too weak, missing lower case letters"
        assert self.get_content(r) == m

        registration_data["password"] = faker.password(min_pwd_len, low=True)
        registration_data["password_confirm"] = registration_data["password"]
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        assert r.status_code == 409
        m = "Password is too weak, missing upper case letters"
        assert self.get_content(r) == m

        registration_data["password"] = faker.password(min_pwd_len,
                                                       low=True,
                                                       up=True)
        registration_data["password_confirm"] = registration_data["password"]
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        assert r.status_code == 409
        m = "Password is too weak, missing numbers"
        assert self.get_content(r) == m

        registration_data["password"] = faker.password(min_pwd_len,
                                                       low=True,
                                                       up=True,
                                                       digits=True)
        registration_data["password_confirm"] = registration_data["password"]
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        assert r.status_code == 409
        m = "Password is too weak, missing special characters"
        assert self.get_content(r) == m

        registration_data["password"] = faker.password(strong=True)
        registration_data["password_confirm"] = registration_data["password"]
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        # now the user is created but INACTIVE, activation endpoint is needed
        assert r.status_code == 200
        registration_message = "We are sending an email to your email address where "
        registration_message += "you will find the link to activate your account"
        assert self.get_content(r) == registration_message

        events = self.get_last_events(1)
        assert events[0].event == Events.create.value
        assert events[0].user == "-"
        assert events[0].target_type == "User"
        assert "name" in events[0].payload
        assert "password" in events[0].payload
        assert events[0].payload["password"] == OBSCURE_VALUE

        mail = self.read_mock_email()
        body = mail.get("body")
        assert body is not None
        assert mail.get("headers") is not None
        # Subject: is a key in the MIMEText
        assert f"Subject: {project_tile} account activation" in mail.get(
            "headers")
        assert f"{proto}://localhost/public/register/" in body

        # This will fail because the user is not active
        _, error = self.do_login(
            client,
            registration_data["email"],
            registration_data["password"],
            status_code=403,
        )
        assert error == "Sorry, this account is not active"

        # Also password reset is not allowed
        data = {"reset_email": registration_data["email"]}
        r = client.post(f"{AUTH_URI}/reset", data=data)
        assert r.status_code == 403
        assert self.get_content(r) == "Sorry, this account is not active"

        events = self.get_last_events(2)
        assert events[0].event == Events.refused_login.value
        assert events[0].payload["username"] == data["reset_email"]
        assert events[0].payload["motivation"] == "account not active"

        assert events[1].event == Events.refused_login.value
        assert events[1].payload["username"] == data["reset_email"]
        assert events[1].payload["motivation"] == "account not active"

        # Activation, missing or wrong information
        r = client.post(f"{AUTH_URI}/profile/activate")
        assert r.status_code == 400
        r = client.post(f"{AUTH_URI}/profile/activate", data=faker.pydict(2))
        assert r.status_code == 400
        # It isn't an email
        invalid = faker.pystr(10)
        r = client.post(f"{AUTH_URI}/profile/activate",
                        data={"username": invalid})
        assert r.status_code == 400

        headers, _ = self.do_login(client, None, None)

        activation_message = "We are sending an email to your email address where "
        activation_message += "you will find the link to activate your account"
        # request activation, wrong username
        r = client.post(f"{AUTH_URI}/profile/activate",
                        data={"username": faker.ascii_email()})
        # return is 200, but no token will be generated and no mail will be sent
        # but it respond with the activation msg and hides the non existence of the user
        assert r.status_code == 200
        assert self.get_content(r) == activation_message

        events = self.get_last_events(1)
        assert events[0].event != Events.activation.value

        assert self.read_mock_email() is None

        # request activation, correct username
        r = client.post(
            f"{AUTH_URI}/profile/activate",
            data={"username": registration_data["email"]},
        )
        assert r.status_code == 200
        assert self.get_content(r) == activation_message

        mail = self.read_mock_email()
        body = mail.get("body")
        assert body is not None
        assert mail.get("headers") is not None
        # Subject: is a key in the MIMEText
        assert f"Subject: {project_tile} account activation" in mail.get(
            "headers")
        assert f"{proto}://localhost/public/register/" in body

        token = self.get_token_from_body(body)
        assert token is not None

        # profile activation
        r = client.put(f"{AUTH_URI}/profile/activate/thisisatoken")
        # this token is not valid
        assert r.status_code == 400

        # profile activation
        r = client.put(f"{AUTH_URI}/profile/activate/{token}")
        assert r.status_code == 200
        assert self.get_content(r) == "Account activated"

        events = self.get_last_events(1)
        assert events[0].event == Events.activation.value
        assert events[0].user == registration_data["email"]
        assert events[0].target_type == "User"

        # Activation token is no longer valid
        r = client.put(f"{AUTH_URI}/profile/activate/{token}")
        assert r.status_code == 400
        assert self.get_content(r) == "Invalid activation token"

        # Token created for another user
        token = self.get_crafted_token("a")
        r = client.put(f"{AUTH_URI}/profile/activate/{token}")
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid activation token"

        # Token created for another user
        token = self.get_crafted_token("a", wrong_algorithm=True)
        r = client.put(f"{AUTH_URI}/profile/activate/{token}")
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid activation token"

        # Token created for another user
        token = self.get_crafted_token("a", wrong_secret=True)
        r = client.put(f"{AUTH_URI}/profile/activate/{token}")
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid activation token"

        headers, _ = self.do_login(client, None, None)
        r = client.get(f"{AUTH_URI}/profile", headers=headers)
        assert r.status_code == 200
        uuid = self.get_content(r).get("uuid")

        token = self.get_crafted_token("x", user_id=uuid)
        r = client.put(f"{AUTH_URI}/profile/activate/{token}")
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid activation token"

        # token created for the correct user, but from outside the system!!
        token = self.get_crafted_token("a", user_id=uuid)
        r = client.put(f"{AUTH_URI}/profile/activate/{token}")
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid activation token"

        # Immature token
        token = self.get_crafted_token("a", user_id=uuid, immature=True)
        r = client.put(f"{AUTH_URI}/profile/activate/{token}")
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid activation token"

        # Expired token
        token = self.get_crafted_token("a", user_id=uuid, expired=True)
        r = client.put(f"{AUTH_URI}/profile/activate/{token}")
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid activation token: this request is expired"

        # Testing the following use case:
        # 1 - user registration
        # 2 - user activation using unconventional channel, e.g. by admins
        # 3 - user tries to activate and fails because already active

        registration_data["email"] = faker.ascii_email()
        r = client.post(f"{AUTH_URI}/profile", data=registration_data)
        # now the user is created but INACTIVE, activation endpoint is needed
        assert r.status_code == 200

        mail = self.read_mock_email()
        body = mail.get("body")
        assert body is not None
        assert mail.get("headers") is not None
        assert f"{proto}://localhost/public/register/" in body

        token = self.get_token_from_body(body)
        assert token is not None

        headers, _ = self.do_login(client, None, None)

        r = client.get(f"{API_URI}/admin/users", headers=headers)
        assert r.status_code == 200
        users = self.get_content(r)
        uuid = None
        for u in users:
            if u.get("email") == registration_data["email"]:
                uuid = u.get("uuid")
                break

        assert uuid is not None
        r = client.put(f"{API_URI}/admin/users/{uuid}",
                       data={"is_active": True},
                       headers=headers)
        assert r.status_code == 204

        r = client.put(f"{AUTH_URI}/profile/activate/{token}")
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid activation token: this request is no longer valid"

        r = client.get(f"{API_URI}/admin/tokens", headers=headers)
        content = self.get_content(r)

        for t in content:
            if t.get("token") == token:  # pragma: no cover
                pytest.fail(
                    "Token not properly invalidated, still bount to user {}",
                    t.get(id))
Exemplo n.º 7
0
class BaseAuthentication(metaclass=abc.ABCMeta):

    """
    An almost abstract class with methods
    to be implemented with a new service
    that aims to store credentials of users and roles.
    """

    # Secret loaded from secret.key file
    JWT_SECRET: Optional[bytes] = None
    # JWT_ALGO = 'HS256'
    # Should be faster on 64bit machines
    JWT_ALGO = "HS512"

    # 1 month in seconds
    DEFAULT_TOKEN_TTL = Env.get_int("AUTH_JWT_TOKEN_TTL", 2_592_000)
    # Grace period before starting to evaluate IP address on token validation
    GRACE_PERIOD = timedelta(seconds=Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD", 7200))
    SAVE_LAST_ACCESS_EVERY = timedelta(
        seconds=Env.get_int("AUTH_TOKEN_SAVE_FREQUENCY", 60)
    )

    FULL_TOKEN = "f"
    PWD_RESET = "r"
    ACTIVATE_ACCOUNT = "a"
    TOTP = "TOTP"
    MIN_PASSWORD_LENGTH = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 8)

    SECOND_FACTOR_AUTHENTICATION = Env.get_bool(
        "AUTH_SECOND_FACTOR_AUTHENTICATION", False
    )

    # enabled if explicitly set or for 2FA is enabled
    FORCE_FIRST_PASSWORD_CHANGE = SECOND_FACTOR_AUTHENTICATION or Env.get_bool(
        "AUTH_FORCE_FIRST_PASSWORD_CHANGE", False
    )

    # enabled if explicitly set or for 2FA is enabled
    VERIFY_PASSWORD_STRENGTH = SECOND_FACTOR_AUTHENTICATION or Env.get_bool(
        "AUTH_VERIFY_PASSWORD_STRENGTH", False
    )
    MAX_PASSWORD_VALIDITY: Optional[timedelta] = get_timedelta(
        Env.get_int("AUTH_MAX_PASSWORD_VALIDITY", 0),
        MAX_PASSWORD_VALIDITY_MIN_TESTNIG_VALUE,
    )

    DISABLE_UNUSED_CREDENTIALS_AFTER: Optional[timedelta] = get_timedelta(
        Env.get_int("AUTH_DISABLE_UNUSED_CREDENTIALS_AFTER", 0),
        # min 60 seconds are required when testing
        DISABLE_UNUSED_CREDENTIALS_AFTER_MIN_TESTNIG_VALUE,
    )

    MAX_LOGIN_ATTEMPTS = get_max_login_attempts(
        Env.get_int("AUTH_MAX_LOGIN_ATTEMPTS", 0)
    )

    FAILED_LOGINS_EXPIRATION: timedelta = timedelta(
        seconds=Env.get_int("AUTH_LOGIN_BAN_TIME", 3600)
    )

    default_user: Optional[str] = None
    default_password: Optional[str] = None
    roles: List[str] = []
    roles_data: Dict[str, str] = {}
    default_role: str = Role.USER.value

    # To be stored on DB
    failed_logins: Dict[str, List[FailedLogin]] = {}

    # Executed once by Connector in init_app
    @classmethod
    def module_initialization(cls) -> None:
        cls.load_default_user()
        cls.load_roles()
        cls.import_secret(SECRET_KEY_FILE)

    @staticmethod
    def load_default_user() -> None:

        BaseAuthentication.default_user = Env.get("AUTH_DEFAULT_USERNAME")
        BaseAuthentication.default_password = Env.get("AUTH_DEFAULT_PASSWORD")
        if (
            BaseAuthentication.default_user is None
            or BaseAuthentication.default_password is None
        ):  # pragma: no cover
            print_and_exit("Default credentials are unavailable!")

    @staticmethod
    def load_roles() -> None:
        BaseAuthentication.roles_data = get_project_configuration(
            "variables.roles"
        ).copy()
        if not BaseAuthentication.roles_data:  # pragma: no cover
            print_and_exit("No roles configured")

        BaseAuthentication.default_role = BaseAuthentication.roles_data.pop("default")

        BaseAuthentication.roles = []
        for role, description in BaseAuthentication.roles_data.items():
            if description != ROLE_DISABLED:
                BaseAuthentication.roles.append(role)

        if not BaseAuthentication.default_role:  # pragma: no cover
            print_and_exit(
                "Default role {} not available!", BaseAuthentication.default_role
            )

    def make_login(self, username: str, password: str) -> Tuple[str, Payload, User]:
        """ The method which will check if credentials are good to go """

        try:
            user = self.get_user(username=username)
        except ValueError as e:  # pragma: no cover
            # SqlAlchemy can raise the following error:
            # A string literal cannot contain NUL (0x00) characters.
            log.error(e)
            raise BadRequest("Invalid input received")
        except BaseException as e:  # pragma: no cover
            log.error("Unable to connect to auth backend\n[{}] {}", type(e), e)

            raise ServiceUnavailable("Unable to connect to auth backend")

        if user is None:
            self.register_failed_login(username)

            self.log_event(
                Events.failed_login,
                payload={"username": username},
                user=user,
            )

            raise Unauthorized("Invalid access credentials", is_warning=True)

        # Check if Oauth2 is enabled
        if user.authmethod != "credentials":  # pragma: no cover
            raise BadRequest("Invalid authentication method")

        # New hashing algorithm, based on bcrypt
        if self.verify_password(password, user.password):
            # Token expiration is capped by the user expiration date, if set
            payload, full_payload = self.fill_payload(user, expiration=user.expiration)
            token = self.create_token(payload)

            self.log_event(Events.login, user=user)
            return token, full_payload, user

        self.log_event(
            Events.failed_login,
            payload={"username": username},
            user=user,
        )
        self.register_failed_login(username)
        raise Unauthorized("Invalid access credentials", is_warning=True)

    @classmethod
    def import_secret(cls, abs_filename: str) -> None:
        try:
            cls.JWT_SECRET = open(abs_filename, "rb").read()
        except OSError:  # pragma: no cover
            print_and_exit("Jwt secret file {} not found", abs_filename)

    # #####################
    # # Password handling #
    ####################
    @staticmethod
    def verify_password(plain_password: str, hashed_password: str) -> bool:
        try:
            return cast(bool, pwd_context.verify(plain_password, hashed_password))
        except ValueError as e:  # pragma: no cover
            log.error(e)

            return False

    @staticmethod
    def get_password_hash(password):
        if not password:
            raise Unauthorized("Invalid password")
        return pwd_context.hash(password)

    @staticmethod
    def get_remote_ip() -> str:
        try:
            if forwarded_ips := request.headers.getlist("X-Forwarded-For"):
                # it can be something like: ['IP1, IP2']
                return str(forwarded_ips[-1].split(",")[0].strip())

            if PRODUCTION and not TESTING:  # pragma: no cover
                log.warning(
                    "Production mode is enabled, but X-Forwarded-For header is missing"
                )

            if request.remote_addr:
                return request.remote_addr

        # Raised when get_remote_ip is executed outside request context
        # For example when creating tokens in initialize_testing_environment
        except RuntimeError as e:
            log.debug(e)
Exemplo n.º 8
0
    def connect(self, **kwargs: str) -> "FTPExt":

        variables = self.variables.copy()

        variables.update(kwargs)

        if (host := variables.get("host")) is None:  # pragma: no cover
            raise ServiceUnavailable("Missing hostname")

        if (user := variables.get("user")) is None:  # pragma: no cover
            raise ServiceUnavailable("Missing credentials")

        if (password := variables.get("password")) is None:  # pragma: no cover
            raise ServiceUnavailable("Missing credentials")

        port = Env.get_int(variables.get("port"), 21)

        ssl_enabled = Env.to_bool(variables.get("ssl_enabled"))

        if ssl_enabled:
            context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

            context.load_default_certs()

            # Disable certificate verification:
            # context.verify_mode = ssl.CERT_NONE
            # Enable certificate verification:
            context.verify_mode = ssl.CERT_REQUIRED
            context.check_hostname = False
            # Path to pem file to verify self signed certificates
            context.load_verify_locations(cafile=SSL_CERTIFICATE)
Exemplo n.º 9
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
Exemplo n.º 10
0
        diagnose=False,
        filter=print_message_on_stderr,
    )

    setattr(set_logger, "log_id", log_id)


set_logger(log_level)

# Logs utilities

# Can't understand why mypy is unable to understand Env.get_int, since it is annotated
# with `-> int` .. but mypy raises:
# Cannot determine type of 'get_int'
# mypy: ignore-errors
MAX_CHAR_LEN = Env.get_int("MAX_LOGS_LENGTH", 200)
OBSCURE_VALUE = "****"
OBSCURED_FIELDS = [
    "password",
    "pwd",
    "token",
    "access_token",
    "file",
    "filename",
    "new_password",
    "password_confirm",
    "totp",
    "totp_code",
]

Exemplo n.º 11
0
class BaseAuthentication(metaclass=ABCMeta):
    """
    An almost abstract class with methods
    to be implemented with a new service
    that aims to store credentials of users and roles.
    """

    JWT_SECRET: str = import_secret(JWT_SECRET_FILE).decode()
    fernet = Fernet(import_secret(TOTP_SECRET_FILE))

    # JWT_ALGO = 'HS256'
    # Should be faster on 64bit machines
    JWT_ALGO = "HS512"

    # 1 month in seconds
    DEFAULT_TOKEN_TTL = Env.get_int("AUTH_JWT_TOKEN_TTL", 2_592_000)
    # Grace period before starting to evaluate IP address on token validation
    GRACE_PERIOD = timedelta(
        seconds=Env.get_int("AUTH_TOKEN_IP_GRACE_PERIOD", 7200))
    SAVE_LAST_ACCESS_EVERY = timedelta(
        seconds=Env.get_int("AUTH_TOKEN_SAVE_FREQUENCY", 60))

    FULL_TOKEN = "f"
    PWD_RESET = "r"
    ACTIVATE_ACCOUNT = "a"
    UNLOCK_CREDENTIALS = "u"
    TOTP = "TOTP"
    MIN_PASSWORD_LENGTH = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 8)

    SECOND_FACTOR_AUTHENTICATION = Env.get_bool(
        "AUTH_SECOND_FACTOR_AUTHENTICATION", False)

    TOTP_VALIDITY_WINDOW = Env.get_int("AUTH_TOTP_VALIDITY_WINDOW", 1)

    # enabled if explicitly set or for 2FA is enabled
    FORCE_FIRST_PASSWORD_CHANGE = SECOND_FACTOR_AUTHENTICATION or Env.get_bool(
        "AUTH_FORCE_FIRST_PASSWORD_CHANGE", False)

    MAX_PASSWORD_VALIDITY: Optional[timedelta] = get_timedelta(
        Env.get_int("AUTH_MAX_PASSWORD_VALIDITY", 0),
        MAX_PASSWORD_VALIDITY_MIN_TESTNIG_VALUE,
    )

    DISABLE_UNUSED_CREDENTIALS_AFTER: Optional[timedelta] = get_timedelta(
        Env.get_int("AUTH_DISABLE_UNUSED_CREDENTIALS_AFTER", 0),
        # min 60 seconds are required when testing
        DISABLE_UNUSED_CREDENTIALS_AFTER_MIN_TESTNIG_VALUE,
    )

    MAX_LOGIN_ATTEMPTS = get_max_login_attempts(
        Env.get_int("AUTH_MAX_LOGIN_ATTEMPTS", 8))

    FAILED_LOGINS_EXPIRATION: timedelta = timedelta(
        seconds=get_login_ban_time(Env.get_int("AUTH_LOGIN_BAN_TIME", 3600)))

    default_user: Optional[str] = None
    default_password: Optional[str] = None
    roles: List[str] = []
    roles_data: Dict[str, str] = {}
    default_role: str = Role.USER.value
    role_descriptions: Dict[str, str] = {}

    # This is to let inform mypy about the existence of self.db
    def __init__(self) -> None:  # pragma: no cover
        self.db: "Connector"

    # Executed once by Connector in init_app
    @classmethod
    def module_initialization(cls) -> None:
        cls.load_default_user()
        cls.load_roles()

    @staticmethod
    def load_default_user() -> None:

        BaseAuthentication.default_user = Env.get("AUTH_DEFAULT_USERNAME", "")
        BaseAuthentication.default_password = Env.get("AUTH_DEFAULT_PASSWORD",
                                                      "")
        if (not BaseAuthentication.default_user or
                not BaseAuthentication.default_password):  # pragma: no cover
            print_and_exit("Default credentials are unavailable!")

    @staticmethod
    def load_roles() -> None:

        empty_dict: Dict[str, str] = {}
        BaseAuthentication.roles_data = glom(mem.configuration,
                                             "variables.roles",
                                             default=empty_dict).copy()
        if not BaseAuthentication.roles_data:  # pragma: no cover
            print_and_exit("No roles configured")

        BaseAuthentication.default_role = BaseAuthentication.roles_data.pop(
            "default", "")

        BaseAuthentication.role_descriptions = glom(
            mem.configuration,
            "variables.roles_descriptions",
            default=empty_dict).copy()

        if not BaseAuthentication.default_role:  # pragma: no cover
            print_and_exit("Default role not available!")

        BaseAuthentication.roles = []
        for role, description in BaseAuthentication.roles_data.items():
            if description != ROLE_DISABLED:
                BaseAuthentication.roles.append(role)

    def make_login(self, username: str, password: str,
                   totp_code: Optional[str]) -> Tuple[str, Payload, User]:

        self.verify_blocked_username(username)

        try:
            user = self.get_user(username=username)
        except ValueError as e:  # pragma: no cover
            # SqlAlchemy can raise the following error:
            # A string literal cannot contain NUL (0x00) characters.
            log.error(e)
            raise BadRequest("Invalid input received")
        except Exception as e:  # pragma: no cover
            log.error("Unable to connect to auth backend\n[{}] {}", type(e), e)

            raise ServiceUnavailable("Unable to connect to auth backend")

        if user is None:
            self.register_failed_login(username, user=None)

            self.log_event(
                Events.failed_login,
                payload={"username": username},
                user=user,
            )

            raise Unauthorized("Invalid access credentials", is_warning=True)

        # Currently only credentials are allowed
        if user.authmethod != "credentials":  # pragma: no cover
            raise BadRequest("Invalid authentication method")

        if not self.verify_password(password, user.password):
            self.log_event(
                Events.failed_login,
                payload={"username": username},
                user=user,
            )
            self.register_failed_login(username, user=user)
            raise Unauthorized("Invalid access credentials", is_warning=True)

        self.verify_user_status(user)

        if self.SECOND_FACTOR_AUTHENTICATION and not totp_code:
            raise AuthMissingTOTP()

        if totp_code:
            self.verify_totp(user, totp_code)

        # Token expiration is capped by the user expiration date, if set
        payload, full_payload = self.fill_payload(user,
                                                  expiration=user.expiration)
        token = self.create_token(payload)

        self.save_login(username, user, failed=False)
        self.log_event(Events.login, user=user)
        return token, full_payload, user

    # #####################
    # # Password handling #
    ####################
    @staticmethod
    def verify_password(plain_password: str, hashed_password: str) -> bool:
        try:
            return cast(bool,
                        pwd_context.verify(plain_password, hashed_password))
        except ValueError as e:  # pragma: no cover
            log.error(e)

            return False

    @staticmethod
    def get_password_hash(password: Optional[str]) -> str:
        if not password:
            raise Unauthorized("Invalid password")
        # CryptContext is no typed.. but this is a string!
        return cast(str, pwd_context.hash(password))

    @staticmethod
    def get_remote_ip(raise_warnings: bool = True) -> str:
        try:

            # Syntax: X-Forwarded-For: <client>, <proxy1>, <proxy2>
            #   <client> The client IP address
            #   <proxy1>, <proxy2> If a request goes through multiple proxies, the
            #        IP addresses of each successive proxy is listed. This means, the
            #        right-most IP address is the IP address of the most recent proxy
            #        and the left-most IP address is the IP address of the originating
            #        client.
            if PROXIED_CONNECTION:
                header_key = "X-Forwarded-For"
                if forwarded_ips := request.headers.getlist(header_key):
                    # it can be something like: ['IP1, IP2']
                    return str(forwarded_ips[0].split(",")[0].strip())
            # Standard (and more secure) way to obtain remote IP
            else:
Exemplo n.º 12
0
    def test_admin(self, client: FlaskClient) -> None:

        if not Env.get_bool("AUTH_ENABLE"):
            log.warning("Skipping admin authorizations tests")
            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.ADMIN])
        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,
                                    True, paths)
        paths = self.check_endpoint(client, "GET",
                                    "/api/admin/users/<user_id>", headers,
                                    True, paths)
        paths = self.check_endpoint(client, "POST", "/api/admin/users",
                                    headers, True, paths)
        paths = self.check_endpoint(client, "PUT",
                                    "/api/admin/users/<user_id>", headers,
                                    True, paths)
        paths = self.check_endpoint(client, "DELETE",
                                    "/api/admin/users/<user_id>", headers,
                                    True, 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, True, paths)
        paths = self.check_endpoint(client, "GET", "/api/admin/tokens",
                                    headers, True, paths)
        paths = self.check_endpoint(client, "DELETE",
                                    "/api/admin/tokens/<token>", headers, True,
                                    paths)
        paths = self.check_endpoint(client, "GET", "/api/admin/stats", headers,
                                    True, paths)
        paths = self.check_endpoint(client, "POST", "/api/admin/mail", headers,
                                    True, 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)
Exemplo n.º 13
0
    def test_password_reset(self, client: FlaskClient, faker: Faker) -> None:

        if not Env.get_bool("ALLOW_PASSWORD_RESET") or not Env.get_bool("AUTH_ENABLE"):
            log.warning("Password reset is disabled, skipping tests")
            return

        project_tile = get_project_configuration("project.title", default="YourProject")
        proto = "https" if PRODUCTION else "http"

        # Request password reset, missing information
        r = client.post(f"{AUTH_URI}/reset")
        assert r.status_code == 400

        # Request password reset, missing information
        r = client.post(f"{AUTH_URI}/reset", json=faker.pydict(2))
        assert r.status_code == 400

        headers, _ = self.do_login(client, None, None)

        # Request password reset, wrong email
        wrong_email = faker.ascii_email()
        data = {"reset_email": wrong_email}
        r = client.post(f"{AUTH_URI}/reset", json=data)
        assert r.status_code == 403
        msg = f"Sorry, {wrong_email} is not recognized as a valid username"
        assert self.get_content(r) == msg

        # Request password reset, correct email
        data = {"reset_email": BaseAuthentication.default_user}
        r = client.post(f"{AUTH_URI}/reset", json=data)
        assert r.status_code == 200

        events = self.get_last_events(1)
        assert events[0].event == Events.reset_password_request.value
        assert events[0].user == data["reset_email"]
        assert events[0].url == "/auth/reset"

        resetmsg = "We'll send instructions to the email provided "
        resetmsg += "if it's associated with an account. "
        resetmsg += "Please check your spam/junk folder."

        assert self.get_content(r) == resetmsg

        mail = self.read_mock_email()
        body = mail.get("body")
        assert body is not None
        assert mail.get("headers") is not None
        # Subject: is a key in the MIMEText
        assert f"Subject: {project_tile}: Password Reset" in mail.get("headers", "")
        assert f"{proto}://localhost/public/reset/" in body

        token = self.get_token_from_body(body)
        assert token is not None

        # Do password reset
        r = client.put(f"{AUTH_URI}/reset/thisisatoken", json={})
        # this token is not valid
        assert r.status_code == 400

        # Check if token is valid
        r = client.put(f"{AUTH_URI}/reset/{token}", json={})
        assert r.status_code == 204

        # Token is still valid because no password still sent
        r = client.put(f"{AUTH_URI}/reset/{token}", json={})
        assert r.status_code == 204

        # Missing information
        data = {
            "new_password": BaseAuthentication.default_password,
        }
        r = client.put(f"{AUTH_URI}/reset/{token}", json=data)
        assert r.status_code == 400
        assert self.get_content(r) == "Invalid password"

        data = {
            "password_confirm": BaseAuthentication.default_password,
        }
        r = client.put(f"{AUTH_URI}/reset/{token}", json=data)
        assert r.status_code == 400
        assert self.get_content(r) == "Invalid password"

        # Request with old password
        data = {
            "new_password": BaseAuthentication.default_password,
            "password_confirm": BaseAuthentication.default_password,
        }
        r = client.put(f"{AUTH_URI}/reset/{token}", json=data)
        assert r.status_code == 409
        error = "The new password cannot match the previous password"
        assert self.get_content(r) == error

        min_pwd_len = Env.get_int("AUTH_MIN_PASSWORD_LENGTH", 9999)

        # Password too short
        data["new_password"] = faker.password(min_pwd_len - 1)
        data["password_confirm"] = faker.password(min_pwd_len - 1)
        r = client.put(f"{AUTH_URI}/reset/{token}", json=data)
        assert r.status_code == 400
        data["password_confirm"] = data["new_password"]
        r = client.put(f"{AUTH_URI}/reset/{token}", json=data)
        assert r.status_code == 400

        data["new_password"] = faker.password(min_pwd_len, strong=True)
        data["password_confirm"] = faker.password(min_pwd_len, strong=True)
        r = client.put(f"{AUTH_URI}/reset/{token}", json=data)
        assert r.status_code == 400
        assert self.get_content(r) == "New password does not match with confirmation"

        new_pwd = faker.password(min_pwd_len, strong=True)
        data["new_password"] = new_pwd
        data["password_confirm"] = new_pwd
        r = client.put(f"{AUTH_URI}/reset/{token}", json=data)
        assert r.status_code == 200

        # After a change password a spam of delete Token is expected
        # Reverse the list and skip all delete tokens to find the change password event
        events = self.get_last_events(100)
        events.reverse()
        for event in events:
            if event.event == Events.delete.value:
                assert event.target_type == "Token"
                continue

            assert event.event == Events.change_password.value
            assert event.user == BaseAuthentication.default_user
            break

        self.do_login(client, None, None, status_code=401)
        headers, _ = self.do_login(client, None, new_pwd)

        # Token is no longer valid
        r = client.put(f"{AUTH_URI}/reset/{token}", json={})
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid reset token"

        # Restore the default password
        if Env.get_bool("AUTH_SECOND_FACTOR_AUTHENTICATION"):
            data["totp_code"] = BaseTests.generate_totp(BaseAuthentication.default_user)

        data["password"] = new_pwd
        data["new_password"] = BaseAuthentication.default_password
        data["password_confirm"] = data["new_password"]
        r = client.put(f"{AUTH_URI}/profile", json=data, headers=headers)
        assert r.status_code == 204

        # After a change password a spam of delete Token is expected
        # Reverse the list and skip all delete tokens to find the change password event
        events = self.get_last_events(100)
        events.reverse()
        for event in events:
            if event.event == Events.delete.value:
                assert event.target_type == "Token"
                continue

            assert event.event == Events.change_password.value
            assert event.user == BaseAuthentication.default_user
            break

        self.do_login(client, None, new_pwd, status_code=401)
        self.do_login(client, None, None)

        # Token created for another user
        token = self.get_crafted_token("r")
        r = client.put(f"{AUTH_URI}/reset/{token}", json={})
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid reset token"

        # Token created for another user
        token = self.get_crafted_token("r", wrong_algorithm=True)
        r = client.put(f"{AUTH_URI}/reset/{token}", json={})
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid reset token"

        # Token created for another user
        token = self.get_crafted_token("r", wrong_secret=True)
        r = client.put(f"{AUTH_URI}/reset/{token}", json={})
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid reset token"

        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 = response.get("uuid")

        token = self.get_crafted_token("x", user_id=uuid)
        r = client.put(f"{AUTH_URI}/reset/{token}", json={})
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid reset token"

        # token created for the correct user, but from outside the system!!
        token = self.get_crafted_token("r", user_id=uuid)
        r = client.put(f"{AUTH_URI}/reset/{token}", json={})
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid reset token"

        # Immature token
        token = self.get_crafted_token("r", user_id=uuid, immature=True)
        r = client.put(f"{AUTH_URI}/reset/{token}", json={})
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid reset token"

        # Expired token
        token = self.get_crafted_token("r", user_id=uuid, expired=True)
        r = client.put(f"{AUTH_URI}/reset/{token}", json={})
        assert r.status_code == 400
        c = self.get_content(r)
        assert c == "Invalid reset token: this request is expired"
Exemplo n.º 14
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
Exemplo n.º 15
0
def test_bot() -> None:

    if not Env.get_bool("TELEGRAM_ENABLE"):
        log.warning("Skipping BOT tests: service not available")
        return

    runner = CliRunner()

    start_timeout(3)
    try:
        runner.invoke(cli.bot, [])
    except Timeout:  # pragma: no cover
        pass

    stop_timeout()

    from restapi.services.telegram import bot

    # Your API ID, hash and session string here
    # How to generate StringSessions:
    # https://docs.telethon.dev/en/latest/concepts/sessions.html#string-sessions
    api_id = Env.get_int("TELEGRAM_APP_ID")
    api_hash = Env.get("TELEGRAM_APP_HASH", "") or None
    session_str = Env.get("TELETHON_SESSION", "") or None
    botname = Env.get("TELEGRAM_BOTNAME", "") or None

    # use TelegramClient as a type once released the typed version 2 (issue #1195)
    async def send_command(client: Any, command: str) -> str:
        await client.send_message(botname, command)
        sleep(1)
        messages = await client.get_messages(botname)
        # TelegramClient is not typed => message is Any
        return messages[0].message  # type: ignore

    async def test() -> None:
        client = TelegramClient(StringSession(session_str), api_id, api_hash)
        await client.start()

        message = await send_command(client, "/me")
        assert re.match(r"^Hello .*, your Telegram ID is [0-9]+", message)

        message = await send_command(client, "/invalid")
        assert message == "Invalid command, ask for /help"

        message = await send_command(client, "/help")
        assert "Available Commands:" in message
        assert "- /help print this help" in message
        assert "- /me info about yourself" in message
        assert "- /status get server status" in message
        assert "- /monitor get server monitoring stats" in message

        # commands requiring APIs can only be tested in PRODUCTION MODE
        if PRODUCTION:

            message = await send_command(client, "/status")
            assert message == "Server is alive"

            message = await send_command(client, "/monitor")
            assert message == "Please select the type of monitor"

            message = await send_command(client, "/monitor x")
            assert message == "Please select the type of monitor"

            message = await send_command(client, "/monitor disk")
            error = "Missing credentials in headers, e.g. Authorization: 'Bearer TOKEN'"
            assert error in message

            message = await send_command(client, "/monitor disk 2")
            assert message == "Too many inputs"

        # # ############################# #
        # #          TEST USER            #
        # # ############################# #
        bot.users = bot.admins
        bot.admins = []
        message = await send_command(client, "/me")
        assert message == PERMISSION_DENIED

        message = await send_command(client, "/invalid")
        assert message == "Invalid command, ask for /help"

        message = await send_command(client, "/help")
        assert "Available Commands:" in message
        assert "- /help print this help" in message
        assert "- /me info about yourself" in message
        assert "- /status get server status" in message
        assert "- /monitor get server monitoring stats" in message

        # commands requiring APIs can only be tested in PRODUCTION MODE
        if PRODUCTION:

            message = await send_command(client, "/status")
            assert message == "Server is alive"

            message = await send_command(client, "/monitor")
            assert message == PERMISSION_DENIED

        # # ############################# #
        # #        TEST UNAUTHORIZED      #
        # # ############################# #
        bot.admins = []
        bot.users = []

        message = await send_command(client, "/me")
        assert message == PERMISSION_DENIED

        message = await send_command(client, "/invalid")
        assert message == PERMISSION_DENIED

        message = await send_command(client, "/help")
        assert message == PERMISSION_DENIED

        message = await send_command(client, "/status")
        assert message == PERMISSION_DENIED

        message = await send_command(client, "/monitor")
        assert message == PERMISSION_DENIED

    asyncio.run(test())

    bot.shutdown()
Exemplo n.º 16
0
import time

from faker import Faker

from restapi.config import PRODUCTION
from restapi.env import Env
from restapi.services.authentication import BaseAuthentication
from restapi.tests import AUTH_URI, BaseTests, FlaskClient
from restapi.utilities.logs import OBSCURE_VALUE, Events, log

max_login_attempts = BaseAuthentication.MAX_LOGIN_ATTEMPTS
ban_duration = Env.get_int("AUTH_LOGIN_BAN_TIME", 10)

BAN_MESSAGE = ("Sorry, this account is temporarily blocked " +
               "due to the number of failed login attempts.")

if max_login_attempts == 0:

    class TestApp1(BaseTests):
        def test_01_login_ban_not_enabled(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)
            # Login attempts are not registered, let's try to fail the login many times
            for _ in range(0, 10):
                self.do_login(client, data["email"], "wrong", status_code=401)

            events = self.get_last_events(1)