Esempio n. 1
0
def handle_response(response):

    response.headers["_RV"] = str(version)

    PROJECT_VERSION = get_project_configuration("project.version", default=None)
    if PROJECT_VERSION is not None:
        response.headers["Version"] = str(PROJECT_VERSION)
    # If it is an upload, DO NOT consume request.data or request.json,
    # otherwise the content gets lost
    try:
        if request.mimetype in ["application/octet-stream", "multipart/form-data"]:
            data = "STREAM_UPLOAD"
        elif request.data:
            data = handle_log_output(request.data)
        elif request.form:
            data = obfuscate_dict(request.form)
        else:
            data = ""

        if data:
            data = f" {data}"
    except Exception as e:  # pragma: no cover
        log.debug(e)
        data = ""

    url = obfuscate_query_parameters(request.url)

    if GZIP_ENABLE and "gzip" in request.headers.get("Accept-Encoding", "").lower():
        response.direct_passthrough = False
        content, headers = ResponseMaker.gzip_response(
            response.data,
            response.status_code,
            response.headers.get("Content-Encoding"),
            response.headers.get("Content-Type"),
        )
        if content:
            response.data = content

            try:
                response.headers.update(headers)
            # Back-compatibility for Werkzeug 0.16.1 as used in B2STAGE
            except AttributeError:  # pragma: no cover
                for k, v in headers.items():
                    response.headers.set(k, v)

    resp = str(response).replace("<Response ", "").replace(">", "")
    log.info(
        "{} {} {}{} -> {}",
        BaseAuthentication.get_remote_ip(),
        request.method,
        url,
        data,
        resp,
    )

    return response
Esempio n. 2
0
    def get_qrcode(self, user: User) -> str:

        secret = self.get_totp_secret(user)
        totp = pyotp.TOTP(secret)

        project_name = get_project_configuration("project.title", "No project name")

        otpauth_url = totp.provisioning_uri(project_name)
        qr_url = segno.make(otpauth_url)
        qr_stream = BytesIO()
        qr_url.save(qr_stream, kind="png", scale=5)
        return base64.b64encode(qr_stream.getvalue()).decode("utf-8")
Esempio n. 3
0
def send_notification(
    subject: str,
    template: str,
    # if None will be sent to the administrator
    to_address: Optional[str] = None,
    data: Optional[Dict[str, Any]] = None,
    user: Optional[User] = None,
    send_async: bool = False,
) -> bool:

    # Always enabled during tests
    if not Connector.check_availability("smtp"):  # pragma: no cover
        return False

    title = get_project_configuration("project.title", default="Unkown title")
    reply_to = Env.get("SMTP_NOREPLY", Env.get("SMTP_ADMIN", ""))

    if data is None:
        data = {}

    data.setdefault("project", title)
    data.setdefault("reply_to", reply_to)

    if user:
        data.setdefault("username", user.email)
        data.setdefault("name", user.name)
        data.setdefault("surname", user.surname)

    html_body, plain_body = get_html_template(template, data)

    if not html_body:  # pragma: no cover
        log.error("Can't load {}", template)
        return False

    subject = f"{title}: {subject}"

    if send_async:
        Mail.send_async(
            subject=subject,
            body=html_body,
            to_address=to_address,
            plain_body=plain_body,
        )
        return False

    smtp_client = smtp.get_instance()
    return smtp_client.send(
        subject=subject,
        body=html_body,
        to_address=to_address,
        plain_body=plain_body,
    )
Esempio n. 4
0
        def post(self, **kwargs: Any) -> Response:
            """ Register new user """

            email = kwargs.get("email")
            user = self.auth.get_user(username=email)
            if user is not None:
                raise Conflict(f"This user already exists: {email}")

            password_confirm = kwargs.pop("password_confirm")
            if kwargs.get("password") != password_confirm:
                raise Conflict("Your password doesn't match the confirmation")

            if self.auth.VERIFY_PASSWORD_STRENGTH:

                check, msg = self.auth.verify_password_strength(
                    kwargs.get("password"), None)

                if not check:
                    raise Conflict(msg)

            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:
                smtp_client = smtp.get_instance()
                if Env.get_bool("REGISTRATION_NOTIFICATIONS"):
                    # Sending an email to the administrator
                    title = get_project_configuration("project.title",
                                                      default="Unkown title")
                    subject = f"{title} New credentials requested"
                    body = f"New credentials request from {user.email}"

                    smtp_client.send(body, subject)

                send_activation_link(smtp_client, self.auth, user)

            except BaseException 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")
Esempio n. 5
0
def handle_response(response: FlaskResponse) -> FlaskResponse:

    response.headers["_RV"] = str(version)

    PROJECT_VERSION = get_project_configuration("project.version", default="0")
    if PROJECT_VERSION is not None:
        response.headers["Version"] = str(PROJECT_VERSION)

    data_string = get_data_from_request()

    url = obfuscate_query_parameters(request.url)

    if (GZIP_ENABLE and not response.is_streamed
            and "gzip" in request.headers.get("Accept-Encoding", "").lower()):
        response.direct_passthrough = False
        content, headers = ResponseMaker.gzip_response(
            response.data,
            response.status_code,
            response.headers.get("Content-Encoding"),
            response.headers.get("Content-Type"),
        )
        if content:
            response.data = content
            response.headers.update(headers)

    resp = str(response).replace("<Response ", "").replace(">", "")
    ip = BaseAuthentication.get_remote_ip(raise_warnings=False)

    is_healthcheck = (ip == "127.0.0.1" and request.method == "GET"
                      and url == "/api/status")
    if is_healthcheck:
        log.debug(
            "{} {} {}{} -> {} [HEALTHCHECK]",
            ip,
            request.method,
            url,
            data_string,
            resp,
        )
    else:
        log.info(
            "{} {} {}{} -> {}",
            ip,
            request.method,
            url,
            data_string,
            resp,
        )

    return response
Esempio n. 6
0
    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
            )
Esempio n. 7
0
        def post(self, reset_email: str) -> Response:

            reset_email = reset_email.lower()

            self.auth.verify_blocked_username(reset_email)

            user = self.auth.get_user(username=reset_email)

            if user is None:
                raise Forbidden(
                    f"Sorry, {reset_email} is not recognized as a valid username",
                )

            self.auth.verify_user_status(user)

            title = get_project_configuration("project.title",
                                              default="Unkown title")

            reset_token, payload = self.auth.create_temporary_token(
                user, self.auth.PWD_RESET)

            server_url = get_frontend_url()

            rt = reset_token.replace(".", "+")

            uri = Env.get("RESET_PASSWORD_URI", "/public/reset")
            complete_uri = f"{server_url}{uri}/{rt}"

            smtp_client = smtp.get_instance()
            send_password_reset_link(smtp_client, complete_uri, title,
                                     reset_email)

            ##################
            # Completing the reset task
            self.auth.save_token(user,
                                 reset_token,
                                 payload,
                                 token_type=self.auth.PWD_RESET)

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

            self.log_event(self.events.reset_password_request, user=user)
            return self.response(msg)
Esempio n. 8
0
def send_activation_link(smtp, auth, user):

    title = get_project_configuration("project.title", default="Unkown title")

    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}"
    body: Optional[str] = f"Follow this link to activate your account: {url}"

    # customized template
    template_file = "activate_account.html"
    html_body = get_html_template(
        template_file,
        {
            "url": url,
            "username": user.email,
            "name": user.name,
            "surname": user.surname,
        },
    )
    if html_body is None:
        html_body = body
        body = None

    default_subject = f"{title} account activation"
    subject = os.getenv("EMAIL_ACTIVATION_SUBJECT", default_subject)

    sent = smtp.send(html_body, subject, user.email, plain_body=body)
    if not sent:  # pragma: no cover
        raise BaseException("Error sending email, please retry")

    auth.save_token(user,
                    activation_token,
                    payload,
                    token_type=auth.ACTIVATE_ACCOUNT)
Esempio n. 9
0
        def verify_credentials_ban_notification(self) -> str:

            # Verify email sent to notify credentials block,
            # + extract and return the unlock url
            mail = self.read_mock_email()
            body = mail.get("body", "")
            project_tile = get_project_configuration(
                "project.title", default="YourProject"
            )

            assert body is not None
            assert mail.get("headers", "") is not None
            title = "Your credentials have been blocked"
            assert f"Subject: {project_tile}: {title}" in mail.get("headers", "")
            # Body can't be asserted if can be changed at project level...
            # assert "this email is to inform you that your credentials have been "
            # "temporarily due to the number of failed login attempts" in body
            # assert "inspect the list below to detect any unwanted login" in body
            # assert "Your credentials will be automatically unlocked in" in body

            token = self.get_token_from_body(body)
            assert token is not None
            return token
Esempio n. 10
0
    def wrapper(self, *args, **kwargs):

        try:
            return func(self, *args, **kwargs)

        except BaseException:

            task_id = self.request.id
            task_name = self.request.task

            log.error("Celery task {} failed ({})", task_id, task_name)
            arguments = str(self.request.args)
            log.error("Failed task arguments: {}", arguments[0:256])
            log.error("Task error: {}", traceback.format_exc())

            if Connector.check_availability("smtp"):
                log.info("Sending error report by email", task_id, task_name)

                body = f"""
Celery task {task_id} failed

Name: {task_name}

Arguments: {self.request.args}

Error: {traceback.format_exc()}
"""

                project = get_project_configuration(
                    "project.title",
                    default="Unkown title",
                )
                subject = f"{project}: task {task_name} failed"
                from restapi.connectors import smtp

                smtp_client = smtp.get_instance()
                smtp_client.send(body, subject)
Esempio n. 11
0
def send_notification(smtp, user, unhashed_password, is_update=False):

    title = get_project_configuration("project.title", default="Unkown title")

    if is_update:
        subject = f"{title}: password changed"
        template = "update_credentials.html"
    else:
        subject = f"{title}: new credentials"
        template = "new_credentials.html"

    replaces = {"username": user.email, "password": unhashed_password}

    html = get_html_template(template, replaces)

    body = f"""
Username: {user.email}
Password: {unhashed_password}
    """

    if html is None:
        smtp.send(body, subject, user.email)
    else:
        smtp.send(html, subject, user.email, plain_body=body)
    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))
Esempio n. 13
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
Esempio n. 14
0
    def test_admin_users(self, client: FlaskClient, faker: Faker) -> None:

        if not Env.get_bool("MAIN_LOGIN_ENABLE"):  # pragma: no cover
            log.warning("Skipping admin/users tests")
            return

        project_tile = get_project_configuration("project.title",
                                                 default="YourProject")

        headers, _ = self.do_login(client, None, None)
        r = client.get(f"{API_URI}/admin/users", headers=headers)
        assert r.status_code == 200

        schema = self.getDynamicInputSchema(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", data=data, headers=headers)
        assert r.status_code == 200
        uuid = self.get_content(r)

        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}: new credentials" in mail.get(
            "headers")
        assert f"Username: {data.get('email', 'MISSING').lower()}" in mail.get(
            "body")
        assert f"Password: {data.get('password')}" in mail.get("body")

        # 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 len(users_list) > 0
        # email is saved lowercase
        assert users_list[0].get("email") == data.get("email",
                                                      "MISSING").lower()

        # Check duplicates
        r = client.post(f"{API_URI}/admin/users", data=data, headers=headers)
        assert r.status_code == 409

        # 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", data=data2, headers=headers)
        assert r.status_code == 200
        uuid2 = self.get_content(r)

        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}: new credentials" in mail.get(
            "headers")
        assert f"Username: {data2.get('email', 'MISSING').lower()}" in mail.get(
            "body")
        assert f"Password: {data2.get('password')}" in mail.get("body")

        # send and invalid user_id
        r = client.put(
            f"{API_URI}/admin/users/invalid",
            data={"name": faker.name()},
            headers=headers,
        )
        assert r.status_code == 404

        # Event 4: modify
        r = client.put(
            f"{API_URI}/admin/users/{uuid}",
            data={"name": faker.name()},
            headers=headers,
        )
        assert r.status_code == 204

        # email cannot be modified
        new_data = {"email": data.get("email")}
        r = client.put(f"{API_URI}/admin/users/{uuid2}",
                       data=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 len(users_list) > 0
        # email is not modified -> still equal to data2, not data1
        assert users_list[0].get("email") != data.get("email",
                                                      "MISSING").lower()
        assert users_list[0].get("email") == data2.get("email",
                                                       "MISSING").lower()

        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

        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}",
                       data=data,
                       headers=headers)
        assert r.status_code == 204

        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 f"Username: {data2.get('email', 'MISSING').lower()}" in mail.get(
            "body")
        assert f"Password: {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", data=data, headers=headers2)
        assert r.status_code == 401

        r = client.put(
            f"{API_URI}/admin/users/{uuid}",
            data={"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

        # Restore the default password, if it 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
        uuid = self.get_content(r).get("uuid")

        data = {
            "password": BaseAuthentication.default_password,
            # very important, otherwise the default user will lose its admin role
            "roles": json.dumps(["admin_root"]),
        }
        # Event 9: modify
        r = client.put(f"{API_URI}/admin/users/{uuid}",
                       data=data,
                       headers=headers)
        assert r.status_code == 204

        r = client.get(f"{AUTH_URI}/logout", headers=headers)
        assert r.status_code == 204
Esempio n. 15
0
def create_app(
    name: str = __name__,
    mode: ServerModes = ServerModes.NORMAL,
    options: Optional[Dict[str, bool]] = None,
) -> Flask:
    """ Create the server istance for Flask application """

    if PRODUCTION and TESTING and not FORCE_PRODUCTION_TESTS:  # pragma: no cover
        print_and_exit("Unable to execute tests in production")

    # TERM is not catched by Flask
    # https://github.com/docker/compose/issues/4199#issuecomment-426109482
    # signal.signal(signal.SIGTERM, teardown_handler)
    # SIGINT is registered as STOPSIGNAL in Dockerfile
    signal.signal(signal.SIGINT, teardown_handler)

    # Flask app instance
    # template_folder = template dir for output in HTML
    microservice = Flask(
        name, template_folder=os.path.join(ABS_RESTAPI_PATH, "templates")
    )

    # CORS
    if not PRODUCTION:
        cors = CORS(
            allow_headers=[
                "Content-Type",
                "Authorization",
                "X-Requested-With",
                "x-upload-content-length",
                "x-upload-content-type",
                "content-range",
            ],
            supports_credentials=["true"],
            methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
        )

        cors.init_app(microservice)
        log.debug("CORS Injected")

    # Flask configuration from config file
    microservice.config.from_object(config)
    log.debug("Flask app configured")

    if PRODUCTION:
        log.info("Production server mode is ON")

    endpoints_loader = EndpointsLoader()
    mem.configuration = endpoints_loader.load_configuration()

    mem.initializer = Meta.get_class("initialization", "Initializer")
    if not mem.initializer:  # pragma: no cover
        print_and_exit("Invalid Initializer class")

    mem.customizer = Meta.get_instance("customization", "Customizer")
    if not mem.customizer:  # pragma: no cover
        print_and_exit("Invalid Customizer class")

    if not isinstance(mem.customizer, BaseCustomizer):  # pragma: no cover
        print_and_exit("Invalid Customizer class, it should inherit BaseCustomizer")

    Connector.init_app(app=microservice, worker_mode=(mode == ServerModes.WORKER))

    # Initialize reading of all files
    mem.geo_reader = geolite2.reader()
    # when to close??
    # geolite2.close()

    if mode == ServerModes.INIT:
        Connector.project_init(options=options)

    if mode == ServerModes.DESTROY:
        Connector.project_clean()

    # Restful plugin with endpoint mapping (skipped in INIT|DESTROY|WORKER modes)
    if mode == ServerModes.NORMAL:

        logging.getLogger("werkzeug").setLevel(logging.ERROR)
        # ignore warning messages from apispec
        warnings.filterwarnings(
            "ignore", message="Multiple schemas resolved to the name "
        )
        mem.cache = Cache.get_instance(microservice)

        endpoints_loader.load_endpoints()
        mem.authenticated_endpoints = endpoints_loader.authenticated_endpoints
        mem.private_endpoints = endpoints_loader.private_endpoints

        # Triggering automatic mapping of REST endpoints
        rest_api = Api(catch_all_404s=True)

        for endpoint in endpoints_loader.endpoints:
            # Create the restful resource with it;
            # this method is from RESTful plugin
            rest_api.add_resource(endpoint.cls, *endpoint.uris)

        # HERE all endpoints will be registered by using FlaskRestful
        rest_api.init_app(microservice)

        # APISpec configuration
        api_url = get_backend_url()
        scheme, host = api_url.rstrip("/").split("://")

        spec = APISpec(
            title=get_project_configuration(
                "project.title", default="Your application name"
            ),
            version=get_project_configuration("project.version", default="0.0.1"),
            openapi_version="2.0",
            # OpenApi 3 not working with FlaskApiSpec
            # -> Duplicate parameter with name body and location body
            # https://github.com/jmcarp/flask-apispec/issues/170
            # Find other warning like this by searching:
            # **FASTAPI**
            # openapi_version="3.0.2",
            plugins=[MarshmallowPlugin()],
            host=host,
            schemes=[scheme],
            tags=endpoints_loader.tags,
        )
        # OpenAPI 3 changed the definition of the security level.
        # Some changes needed here?
        api_key_scheme = {"type": "apiKey", "in": "header", "name": "Authorization"}
        spec.components.security_scheme("Bearer", api_key_scheme)

        microservice.config.update(
            {
                "APISPEC_SPEC": spec,
                # 'APISPEC_SWAGGER_URL': '/api/swagger',
                "APISPEC_SWAGGER_URL": None,
                # 'APISPEC_SWAGGER_UI_URL': '/api/swagger-ui',
                # Disable Swagger-UI
                "APISPEC_SWAGGER_UI_URL": None,
            }
        )

        mem.docs = FlaskApiSpec(microservice)

        # Clean app routes
        ignore_verbs = {"HEAD", "OPTIONS"}

        for rule in microservice.url_map.iter_rules():

            endpoint = microservice.view_functions[rule.endpoint]
            if not hasattr(endpoint, "view_class"):
                continue

            newmethods = ignore_verbs.copy()
            rulename = str(rule)

            for verb in rule.methods - ignore_verbs:
                method = verb.lower()
                if method in endpoints_loader.uri2methods[rulename]:
                    # remove from flask mapping
                    # to allow 405 response
                    newmethods.add(verb)

            rule.methods = newmethods

        # Register swagger. Note: after method mapping cleaning
        with microservice.app_context():
            for endpoint in endpoints_loader.endpoints:
                try:
                    mem.docs.register(endpoint.cls)
                except TypeError as e:  # pragma: no cover
                    print(e)
                    log.error("Cannot register {}: {}", endpoint.cls.__name__, e)

    # marshmallow errors handler
    microservice.register_error_handler(422, handle_marshmallow_errors)

    # Logging responses
    microservice.after_request(handle_response)

    if SENTRY_URL is not None:  # pragma: no cover

        if PRODUCTION:
            sentry_sdk.init(
                dsn=SENTRY_URL,
                # already catched by handle_marshmallow_errors
                ignore_errors=[werkzeug.exceptions.UnprocessableEntity],
                integrations=[FlaskIntegration()],
            )
            log.info("Enabled Sentry {}", SENTRY_URL)
        else:
            # Could be enabled in print mode
            # sentry_sdk.init(transport=print)
            log.info("Skipping Sentry, only enabled in PRODUCTION mode")

    log.info("Boot completed")

    return microservice
Esempio n. 16
0
def create_app(
    name: str = __name__,
    mode: ServerModes = ServerModes.NORMAL,
    options: Optional[Dict[str, bool]] = None,
) -> Flask:
    """Create the server istance for Flask application"""

    if PRODUCTION and TESTING and not FORCE_PRODUCTION_TESTS:  # pragma: no cover
        print_and_exit("Unable to execute tests in production")

    # TERM is not catched by Flask
    # https://github.com/docker/compose/issues/4199#issuecomment-426109482
    # signal.signal(signal.SIGTERM, teardown_handler)
    # SIGINT is registered as STOPSIGNAL in Dockerfile
    signal.signal(signal.SIGINT, teardown_handler)

    # Flask app instance
    # template_folder = template dir for output in HTML
    flask_app = Flask(name, template_folder=str(ABS_RESTAPI_PATH.joinpath("templates")))

    # CORS
    if not PRODUCTION:

        if TESTING:
            cors_origin = "*"
        else:  # pragma: no cover
            cors_origin = get_frontend_url()
            # Beware, this only works because get_frontend_url never append a port
            cors_origin += ":*"

        CORS(
            flask_app,
            allow_headers=[
                "Content-Type",
                "Authorization",
                "X-Requested-With",
                "x-upload-content-length",
                "x-upload-content-type",
                "content-range",
            ],
            supports_credentials=["true"],
            methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
            resources={r"*": {"origins": cors_origin}},
        )

        log.debug("CORS Enabled")

    # Flask configuration from config file
    flask_app.config.from_object(config)
    flask_app.json_encoder = ExtendedJSONEncoder

    # Used to force flask to avoid json sorting and ensure that
    # the output to reflect the order of field in the Marshmallow schema
    flask_app.config["JSON_SORT_KEYS"] = False

    log.debug("Flask app configured")

    if PRODUCTION:
        log.info("Production server mode is ON")

    endpoints_loader = EndpointsLoader()

    if HOST_TYPE == DOCS:  # pragma: no cover
        log.critical("Creating mocked configuration")
        mem.configuration = {}

        log.critical("Loading Mocked Initializer and Customizer classes")
        from restapi.mocks import Customizer, Initializer

        mem.initializer = Initializer
        mem.customizer = Customizer()

    else:

        mem.configuration = endpoints_loader.load_configuration()
        mem.initializer = Meta.get_class("initialization", "Initializer")
        if not mem.initializer:  # pragma: no cover
            print_and_exit("Invalid Initializer class")

        customizer = Meta.get_class("customization", "Customizer")
        if not customizer:  # pragma: no cover
            print_and_exit("Invalid Customizer class")
        mem.customizer = customizer()

    if not isinstance(mem.customizer, BaseCustomizer):  # pragma: no cover
        print_and_exit("Invalid Customizer class, it should inherit BaseCustomizer")

    Connector.init_app(app=flask_app, worker_mode=(mode == ServerModes.WORKER))

    # Initialize reading of all files
    mem.geo_reader = geolite2.reader()
    # when to close??
    # geolite2.close()

    if mode == ServerModes.INIT:
        Connector.project_init(options=options)

    if mode == ServerModes.DESTROY:
        Connector.project_clean()

    # Restful plugin with endpoint mapping (skipped in INIT|DESTROY|WORKER modes)
    if mode == ServerModes.NORMAL:

        logging.getLogger("werkzeug").setLevel(logging.ERROR)

        # warnings levels:
        # default  # Warn once per call location
        # error    # Convert to exceptions
        # always   # Warn every time
        # module   # Warn once per calling module
        # once     # Warn once per Python process
        # ignore   # Never warn

        # Types of warnings:
        # Warning: This is the base class of all warning category classes
        # UserWarning: The default category for warn().
        # DeprecationWarning: Base category for warnings about deprecated features when
        #                     those warnings are intended for other Python developers
        # SyntaxWarning: Base category for warnings about dubious syntactic features.
        # RuntimeWarning: Base category for warnings about dubious runtime features.
        # FutureWarning: Base category for warnings about deprecated features when those
        #                warnings are intended for end users
        # PendingDeprecationWarning: Base category for warnings about features that will
        #                            be deprecated in the future (ignored by default).
        # ImportWarning: Base category for warnings triggered during the process of
        #                importing a module
        # UnicodeWarning: Base category for warnings related to Unicode.
        # BytesWarning: Base category for warnings related to bytes and bytearray.
        # ResourceWarning: Base category for warnings related to resource usage

        if TESTING:
            warnings.simplefilter("always", Warning)
            warnings.simplefilter("error", UserWarning)
            warnings.simplefilter("error", DeprecationWarning)
            warnings.simplefilter("error", SyntaxWarning)
            warnings.simplefilter("error", RuntimeWarning)
            warnings.simplefilter("error", FutureWarning)
            # warnings about features that will be deprecated in the future
            warnings.simplefilter("default", PendingDeprecationWarning)
            warnings.simplefilter("error", ImportWarning)
            warnings.simplefilter("error", UnicodeWarning)
            warnings.simplefilter("error", BytesWarning)
            # Can't set this an error due to false positives with downloads
            # a lot of issues like: https://github.com/pallets/flask/issues/2468
            warnings.simplefilter("always", ResourceWarning)
            warnings.simplefilter("default", Neo4jExperimentalWarning)

            # Remove me in a near future, this is due to hypothesis with pytest 7
            # https://github.com/HypothesisWorks/hypothesis/issues/3222
            warnings.filterwarnings(
                "ignore", message="A private pytest class or function was used."
            )

        elif PRODUCTION:  # pragma: no cover
            warnings.simplefilter("ignore", Warning)
            warnings.simplefilter("always", UserWarning)
            warnings.simplefilter("default", DeprecationWarning)
            warnings.simplefilter("ignore", SyntaxWarning)
            warnings.simplefilter("ignore", RuntimeWarning)
            warnings.simplefilter("ignore", FutureWarning)
            warnings.simplefilter("ignore", PendingDeprecationWarning)
            warnings.simplefilter("ignore", ImportWarning)
            warnings.simplefilter("ignore", UnicodeWarning)
            warnings.simplefilter("ignore", BytesWarning)
            warnings.simplefilter("ignore", ResourceWarning)
            # even if ignore it is raised once
            # because of the imports executed before setting this to ignore
            warnings.simplefilter("ignore", Neo4jExperimentalWarning)
        else:  # pragma: no cover
            warnings.simplefilter("default", Warning)
            warnings.simplefilter("always", UserWarning)
            warnings.simplefilter("always", DeprecationWarning)
            warnings.simplefilter("default", SyntaxWarning)
            warnings.simplefilter("default", RuntimeWarning)
            warnings.simplefilter("always", FutureWarning)
            warnings.simplefilter("default", PendingDeprecationWarning)
            warnings.simplefilter("default", ImportWarning)
            warnings.simplefilter("default", UnicodeWarning)
            warnings.simplefilter("default", BytesWarning)
            warnings.simplefilter("always", ResourceWarning)
            # even if ignore it is raised once
            # because of the imports executed before setting this to ignore
            warnings.simplefilter("ignore", Neo4jExperimentalWarning)

        # ignore warning messages from apispec
        warnings.filterwarnings(
            "ignore", message="Multiple schemas resolved to the name "
        )

        # ignore warning messages on flask socket after teardown
        warnings.filterwarnings("ignore", message="unclosed <socket.socket")

        # from flask_caching 1.10.1 with python 3.10 on core tests...
        # try to remove this once upgraded flask_caching in a near future
        warnings.filterwarnings(
            "ignore",
            message="_SixMetaPathImporter.find_spec",
        )

        # Raised from sentry_sdk 1.5.11 with python 3.10 events
        warnings.filterwarnings(
            "ignore",
            message="SelectableGroups dict interface is deprecated. Use select.",
        )

        mem.cache = Cache.get_instance(flask_app)

        endpoints_loader.load_endpoints()
        mem.authenticated_endpoints = endpoints_loader.authenticated_endpoints
        mem.private_endpoints = endpoints_loader.private_endpoints

        for endpoint in endpoints_loader.endpoints:
            ename = endpoint.cls.__name__.lower()
            endpoint_view = endpoint.cls.as_view(ename)
            for url in endpoint.uris:
                flask_app.add_url_rule(url, view_func=endpoint_view)

        # APISpec configuration
        api_url = get_backend_url()
        scheme, host = api_url.rstrip("/").split("://")

        spec = APISpec(
            title=get_project_configuration(
                "project.title", default="Your application name"
            ),
            version=get_project_configuration("project.version", default="0.0.1"),
            openapi_version="2.0",
            # OpenApi 3 not working with FlaskApiSpec
            # -> Duplicate parameter with name body and location body
            # https://github.com/jmcarp/flask-apispec/issues/170
            # Find other warning like this by searching:
            # **FASTAPI**
            # openapi_version="3.0.2",
            plugins=[MarshmallowPlugin()],
            host=host,
            schemes=[scheme],
            tags=endpoints_loader.tags,
        )
        # OpenAPI 3 changed the definition of the security level.
        # Some changes needed here?

        if Env.get_bool("AUTH_ENABLE"):
            api_key_scheme = {"type": "apiKey", "in": "header", "name": "Authorization"}
            spec.components.security_scheme("Bearer", api_key_scheme)

        flask_app.config.update(
            {
                "APISPEC_SPEC": spec,
                # 'APISPEC_SWAGGER_URL': '/api/swagger',
                "APISPEC_SWAGGER_URL": None,
                # 'APISPEC_SWAGGER_UI_URL': '/api/swagger-ui',
                # Disable Swagger-UI
                "APISPEC_SWAGGER_UI_URL": None,
            }
        )

        mem.docs = FlaskApiSpec(flask_app)

        # Clean app routes
        ignore_verbs = {"HEAD", "OPTIONS"}

        for rule in flask_app.url_map.iter_rules():

            view_function = flask_app.view_functions[rule.endpoint]
            if not hasattr(view_function, "view_class"):
                continue

            newmethods = ignore_verbs.copy()
            rulename = str(rule)

            if rule.methods:
                for verb in rule.methods - ignore_verbs:
                    method = verb.lower()
                    if method in endpoints_loader.uri2methods[rulename]:
                        # remove from flask mapping
                        # to allow 405 response
                        newmethods.add(verb)

            rule.methods = newmethods

        # Register swagger. Note: after method mapping cleaning
        with flask_app.app_context():
            for endpoint in endpoints_loader.endpoints:
                try:
                    mem.docs.register(endpoint.cls)
                except TypeError as e:  # pragma: no cover
                    print(e)
                    log.error("Cannot register {}: {}", endpoint.cls.__name__, e)

    # marshmallow errors handler
    # Can't get the typing to work with flask 2.1
    flask_app.register_error_handler(422, handle_marshmallow_errors)  # type: ignore
    flask_app.register_error_handler(400, handle_http_errors)  # type: ignore
    flask_app.register_error_handler(404, handle_http_errors)  # type: ignore
    flask_app.register_error_handler(405, handle_http_errors)  # type: ignore
    flask_app.register_error_handler(500, handle_http_errors)  # type: ignore

    # flask_app.before_request(inspect_request)
    # Logging responses
    # Can't get the typing to work with flask 2.1
    flask_app.after_request(handle_response)  # type: ignore

    if SENTRY_URL is not None:  # pragma: no cover

        if PRODUCTION:
            sentry_sdk_init(
                dsn=SENTRY_URL,
                # already catched by handle_marshmallow_errors
                ignore_errors=[werkzeug.exceptions.UnprocessableEntity],
                integrations=[FlaskIntegration()],
            )
            log.info("Enabled Sentry {}", SENTRY_URL)
        else:
            # Could be enabled in print mode
            # sentry_sdk_init(transport=print)
            log.info("Skipping Sentry, only enabled in PRODUCTION mode")

    log.info("Boot completed")
    if PRODUCTION and not TESTING and name == MAIN_SERVER_NAME:  # pragma: no cover
        save_event_log(
            event=Events.server_startup,
            payload={"server": name},
            user=None,
            target=None,
        )

    return flask_app
Esempio n. 17
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"
Esempio n. 18
0
def test_celery(app: Flask, faker: Faker) -> None:

    log.info("Executing {} tests", CONNECTOR)

    obj = connector.get_instance()
    assert obj is not None

    task = obj.celery_app.send_task("test_task", args=("myinput", ))

    assert task is not None
    assert task.id is not None

    # Mocked task
    task_output = BaseTests.send_task(app, "test_task", "myinput")

    # As defined in task template
    assert task_output == "Task executed!"

    # wrong is a special value included in tasks template
    with pytest.raises(Ignore):
        BaseTests.send_task(app, "test_task", "wrong")

    project_title = get_project_configuration("project.title",
                                              default="YourProject")

    mail = BaseTests.read_mock_email()

    body = mail.get("body")
    headers = mail.get("headers")
    assert body is not None
    assert headers is not None
    assert f"Subject: {project_title}: Task test_task failed" in headers
    assert "this email is to notify you that a Celery task failed!" in body
    # fixed-id is a mocked value set in TESTING mode by @task in Celery connector
    assert "Task ID: fixed-id" in body
    assert "Task name: test_task" in body
    assert "Arguments: ('wrong',)" in body
    assert "Error Stack" in body
    assert "Traceback (most recent call last):" in body

    exc = (
        "AttributeError: "
        "You can raise exceptions to stop the task execution in case of errors"
    )
    assert exc in body

    # celery.exceptions.Ignore exceptions are ignored

    BaseTests.delete_mock_email()
    # ignore is a special value included in tasks template
    with pytest.raises(Ignore):
        BaseTests.send_task(app, "test_task", "ignore")
    # the errors decorator re-raise the Ignore exception, without any further action
    # No email is sent in case of Ignore exceptions
    with pytest.raises(FileNotFoundError):
        mail = BaseTests.read_mock_email()

    # retry is a special value included in tasks template
    with pytest.raises(CeleryRetryTask):
        BaseTests.send_task(app, "test_task", "retry")

    mail = BaseTests.read_mock_email()

    body = mail.get("body")
    headers = mail.get("headers")
    assert body is not None
    assert headers is not None
    assert f"Subject: {project_title}: Task test_task failed (failure #1)" in headers
    assert "this email is to notify you that a Celery task failed!" in body
    # fixed-id is a mocked value set in TESTING mode by @task in Celery connector
    assert "Task ID: fixed-id" in body
    assert "Task name: test_task" in body
    assert "Arguments: ('retry',)" in body
    assert "Error Stack" in body
    assert "Traceback (most recent call last):" in body

    exc = "CeleryRetryTask: Force the retry of this task"
    assert exc in body

    # retry2 is a special value included in tasks template
    # Can't easily import the custom exception defined in the task...
    # a generic exception is enough here
    with pytest.raises(Exception):
        BaseTests.send_task(app, "test_task", "retry2")

    mail = BaseTests.read_mock_email()

    body = mail.get("body")
    headers = mail.get("headers")
    assert body is not None
    assert headers is not None
    assert f"Subject: {project_title}: Task test_task failed (failure #1)" in headers
    assert "this email is to notify you that a Celery task failed!" in body
    # fixed-id is a mocked value set in TESTING mode by @task in Celery connector
    assert "Task ID: fixed-id" in body
    assert "Task name: test_task" in body
    assert "Arguments: ('retry2',)" in body
    assert "Error Stack" in body
    assert "Traceback (most recent call last):" in body

    exc = "MyException: Force the retry of this task by using a custom exception"
    assert exc in body

    with pytest.raises(AttributeError, match=r"Task not found"):
        BaseTests.send_task(app, "does-not-exist")

    if obj.variables.get("backend_service") == "RABBIT":
        log.warning(
            "Due to limitations on RABBIT backend task results will not be tested"
        )
    else:
        try:
            r = task.get(timeout=10)
            assert r is not None
            # This is the task output, as defined in task_template.py.j2
            assert r == "Task executed!"
            assert task.status == "SUCCESS"
            assert task.result == "Task executed!"
        except celery.exceptions.TimeoutError:  # pragma: no cover
            pytest.fail(
                f"Task timeout, result={task.result}, status={task.status}")

    obj.disconnect()

    # a second disconnect should not raise any error
    obj.disconnect()

    # Create new connector with short expiration time
    obj = connector.get_instance(expiration=2, verification=1)
    obj_id = id(obj)

    # Connector is expected to be still valid
    obj = connector.get_instance(expiration=2, verification=1)
    assert id(obj) == obj_id

    time.sleep(1)

    # The connection should have been checked and should be still valid
    obj = connector.get_instance(expiration=2, verification=1)
    assert id(obj) == obj_id

    time.sleep(1)

    # Connection should have been expired and a new connector been created
    obj = connector.get_instance(expiration=2, verification=1)
    assert id(obj) != obj_id

    assert obj.is_connected()
    obj.disconnect()
    assert not obj.is_connected()

    # ... close connection again ... nothing should happen
    obj.disconnect()

    with connector.get_instance() as obj:
        assert obj is not None

    app = create_app(mode=ServerModes.WORKER)
    assert app is not None