def deliver_emails_for_batch() -> Tuple[str, HTTPStatus]:
    """Deliver a batch of generated emails.

    Validates email addresses provided in the query params.

    Query parameters:
        batch_id: (required) Identifier for this batch
        redirect_address: (optional) An email address to which all emails will be sent. This can be used for redirecting
        all of the reports to a supervisor.
        cc_address: (optional) An email address to which all emails will be CC'd. This can be used for sending
        a batch of reports to multiple recipients. Multiple cc_address params can be given.
            Example:
            ?batch_id=123&cc_address=cc-one%40test.org&cc_address=cc_two%40test.org&cc_address=cc_three%40test.org
        subject_override: (optional) Override for subject being sent.

    Returns:
        Text indicating the results of the run and an HTTP status

    Raises:
        Nothing.  Catch everything so that we can always return a response to the request
    """

    try:
        batch_id = get_only_str_param_value("batch_id", request.args)
        redirect_address = get_only_str_param_value("redirect_address",
                                                    request.args)
        cc_addresses = get_str_param_values("cc_address", request.args)
        subject_override = get_only_str_param_value("subject_override",
                                                    request.args,
                                                    preserve_case=True)

        validate_email_address(redirect_address)
        for cc_address in cc_addresses:
            validate_email_address(cc_address)
    except ValueError as error:
        logging.error(error)
        return str(error), HTTPStatus.BAD_REQUEST

    if not batch_id:
        msg = "Query parameter 'batch_id' not received"
        logging.error(msg)
        return msg, HTTPStatus.BAD_REQUEST

    success_count, failure_count = email_delivery.deliver(
        batch_id,
        redirect_address=redirect_address,
        cc_addresses=cc_addresses,
        subject_override=subject_override,
    )

    redirect_text = (f"to the redirect email address {redirect_address}"
                     if redirect_address else "")
    cc_addresses_text = (f"CC'd {','.join(email for email in cc_addresses)}."
                         if cc_addresses else "")

    return (
        f"Sent {success_count} emails {redirect_text}. {cc_addresses_text} "
        f"{failure_count} emails failed to send",
        HTTPStatus.OK,
    )
Beispiel #2
0
 def test_get_only_str_param_value_error(self) -> None:
     with self.assertRaises(ValueError) as error:
         params.get_only_str_param_value("region", PARAMS)
     self.assertEqual(
         str(error.exception),
         "Only one value can be provided for query param region.",
     )
Beispiel #3
0
def _handle_gcs_imports() -> Tuple[str, HTTPStatus]:
    """Exposes an endpoint that enqueues a Cloud Task to trigger the GCS imports."""
    filename = get_only_str_param_value("filename",
                                        request.args,
                                        preserve_case=True)
    if not filename:
        return "Must include a filename query parameter", HTTPStatus.BAD_REQUEST

    cloud_task_manager = CloudTaskQueueManager(
        queue_info_cls=CloudTaskQueueInfo,
        queue_name=CASE_TRIAGE_DB_OPERATIONS_QUEUE)
    cloud_task_manager.create_task(
        relative_uri="/case_triage_ops/run_gcs_imports",
        body={"filename": filename})
    logging.info("Enqueued gcs_import task to %s",
                 CASE_TRIAGE_DB_OPERATIONS_QUEUE)
    return "", HTTPStatus.OK
Beispiel #4
0
def handle_import_user_restrictions_csv_to_sql() -> Tuple[str, HTTPStatus]:
    region_code = get_only_str_param_value("region_code",
                                           request.args,
                                           preserve_case=True)
    if not region_code:
        return "Missing region_code param", HTTPStatus.BAD_REQUEST

    cloud_task_manager = CloudTaskQueueManager(
        queue_info_cls=CloudTaskQueueInfo,
        queue_name=CASE_TRIAGE_DB_OPERATIONS_QUEUE)
    cloud_task_manager.create_task(
        relative_uri="/auth/import_user_restrictions_csv_to_sql",
        body={"region_code": region_code},
    )
    logging.info(
        "Enqueued import_user_restrictions_csv_to_sql task to %s",
        CASE_TRIAGE_DB_OPERATIONS_QUEUE,
    )
    return "", HTTPStatus.OK
Beispiel #5
0
def update_auth0_user_metadata() -> Tuple[str, HTTPStatus]:
    """This endpoint is triggered from a GCS bucket when a new user restrictions file is created. It downloads the
     user restrictions file and updates each user's app_metadata with the restrictions from the file.

    This function first queries the Auth0 Management API to request all users and their user_ids by the
    list of email addresses that exist in the downloaded file. Then it iterates over those users and updates their
    app_metadata.

    Query parameters:
        region_code: (required) The region code that has the updated user restrictions file, e.g. US_MO

    Returns:
         Text indicating the results of the run and an HTTP status

    Raises:
        Nothing. Catch everything so that we can always return a response to the request
    """
    region_code = get_only_str_param_value("region_code",
                                           request.args,
                                           preserve_case=True)

    if not region_code:
        return (
            "Missing required region_code param",
            HTTPStatus.BAD_REQUEST,
        )

    try:
        _validate_region_code(region_code)
    except ValueError as error:
        logging.error(error)
        return str(error), HTTPStatus.BAD_REQUEST

    database_key = SQLAlchemyDatabaseKey.for_schema(
        schema_type=SchemaType.CASE_TRIAGE)
    with SessionFactory.using_database(database_key=database_key,
                                       autocommit=False) as session:
        try:
            all_user_restrictions = (session.query(
                DashboardUserRestrictions.restricted_user_email,
                DashboardUserRestrictions.allowed_supervision_location_ids,
                DashboardUserRestrictions.allowed_supervision_location_level,
                DashboardUserRestrictions.can_access_leadership_dashboard,
                DashboardUserRestrictions.can_access_case_triage,
            ).filter(DashboardUserRestrictions.state_code == region_code.upper(
            )).order_by(DashboardUserRestrictions.restricted_user_email).all())
            user_restrictions_by_email: Dict[str, Auth0AppMetadata] = {}
            for user_restrictions in all_user_restrictions:
                email = user_restrictions["restricted_user_email"].lower()
                user_restrictions_by_email[email] = _format_db_results(
                    user_restrictions)

            auth0 = Auth0Client()
            email_addresses = list(user_restrictions_by_email.keys())
            users = auth0.get_all_users_by_email_addresses(email_addresses)
            num_updated_users = 0

            for user in users:
                email = user.get("email", "")
                current_app_metadata = user.get("app_metadata", {})
                new_restrictions: Optional[
                    Auth0AppMetadata] = user_restrictions_by_email.get(email)
                current_restrictions = _normalize_current_restrictions(
                    current_app_metadata)

                if (new_restrictions is not None
                        and current_restrictions != new_restrictions):
                    num_updated_users += 1
                    auth0.update_user_app_metadata(
                        user_id=user.get("user_id", ""),
                        app_metadata=new_restrictions)

            return (
                f"Finished updating {num_updated_users} auth0 users with restrictions for region {region_code}",
                HTTPStatus.OK,
            )

        except Exception as error:
            logging.error(error)
            return (
                f"Error using Auth0 management API to update users: {error}",
                HTTPStatus.INTERNAL_SERVER_ERROR,
            )
Beispiel #6
0
def dashboard_user_restrictions_by_email(
) -> Tuple[Union[Auth0AppMetadata, str], HTTPStatus]:
    """This endpoint is accessed by a service account used by an Auth0 hook that is called at the pre-registration when
    a user first signs up for an account. Given a user email address in the request, it responds with
    the app_metadata that the hook will save on the user so that the UP dashboards can apply the appropriate
    restrictions.

    Query parameters:
        email_address: (required) The email address that requires a user restrictions lookup
        region_code: (required) The region code to use to lookup the user restrictions

    Returns:
         JSON response of the app_metadata associated with the given email address and an HTTP status

    Raises:
        Nothing. Catch everything so that we can always return a response to the request
    """
    email_address = get_only_str_param_value("email_address", request.args)
    region_code = get_only_str_param_value("region_code",
                                           request.args,
                                           preserve_case=True)

    try:
        if not email_address:
            return "Missing email_address param", HTTPStatus.BAD_REQUEST
        if not region_code:
            return "Missing region_code param", HTTPStatus.BAD_REQUEST
        _validate_region_code(region_code)
        validate_email_address(email_address)
    except ValueError as error:
        logging.error(error)
        return str(error), HTTPStatus.BAD_REQUEST

    database_key = SQLAlchemyDatabaseKey.for_schema(
        schema_type=SchemaType.CASE_TRIAGE)
    # TODO(#8046): Don't use the deprecated session fetcher
    session = SessionFactory.deprecated__for_database(
        database_key=database_key)
    try:
        user_restrictions = (session.query(
            DashboardUserRestrictions.allowed_supervision_location_ids,
            DashboardUserRestrictions.allowed_supervision_location_level,
            DashboardUserRestrictions.can_access_leadership_dashboard,
            DashboardUserRestrictions.can_access_case_triage,
        ).filter(
            DashboardUserRestrictions.state_code == region_code.upper(),
            func.lower(DashboardUserRestrictions.restricted_user_email) ==
            email_address.lower(),
        ).one())

        restrictions = _format_db_results(user_restrictions)

        return (restrictions, HTTPStatus.OK)

    except sqlalchemy.orm.exc.NoResultFound:
        return (
            f"User not found for email address {email_address} and region code {region_code}.",
            HTTPStatus.NOT_FOUND,
        )

    except Exception as error:
        logging.error(error)
        return (
            f"An error occurred while fetching dashboard user restrictions with the email {email_address} for "
            f"region_code {region_code}: {error}",
            HTTPStatus.INTERNAL_SERVER_ERROR,
        )

    finally:
        session.close()
def start_new_batch() -> Tuple[str, HTTPStatus]:
    """Start a new batch of email generation for the indicated state.

    Validates the test address provided in the params.

    Query parameters:
        state_code: (required) A valid state code for which reporting is enabled (ex: "US_ID")
        report_type: (required) A valid report type identifier (ex: "po_monthly_report)
        test_address: (optional) Should only be used for testing. When provided, the test_address is used to generate
            the email filenames, ensuring that all emails in the batch can only be delivered to the test_address and not
            to the usual recipients of the report. The email filenames will include the original recipient's email
            username, for example: [email protected].
        region_code: (optional) Indicates the sub-region of the state to generate emails for. If
            omitted, we generate emails for all sub-regions of the state.
        email_allowlist: (optional) A json list of emails we should generate emails for. Emails that do not exist in the
            report will be silently skipped.
        message_body: (optional) If included, overrides the default message body.

    Returns:
        Text indicating the results of the run and an HTTP status

    Raises:
        Nothing.  Catch everything so that we can always return a response to the request
    """
    try:
        state_code = get_only_str_param_value("state_code", request.args)
        report_type = get_only_str_param_value("report_type", request.args)
        test_address = get_only_str_param_value("test_address", request.args)
        region_code = get_only_str_param_value("region_code", request.args)
        raw_email_allowlist = get_only_str_param_value("email_allowlist",
                                                       request.args)
        message_body = get_only_str_param_value("message_body",
                                                request.args,
                                                preserve_case=True)

        validate_email_address(test_address)

        email_allowlist: Optional[List[str]] = (
            json.loads(raw_email_allowlist) if raw_email_allowlist else None)

        if email_allowlist is not None:
            for recipient_email in email_allowlist:
                validate_email_address(recipient_email)

    except (ValueError, JSONDecodeError) as error:
        logging.error(error)
        return str(error), HTTPStatus.BAD_REQUEST

    if not state_code or not report_type:
        msg = "Request does not include 'state_code' and/or 'report_type' parameters"
        logging.error(msg)
        return msg, HTTPStatus.BAD_REQUEST

    # Normalize query param inputs
    state_code = state_code.upper()
    if test_address == "":
        test_address = None
    region_code = None if not region_code else region_code.upper()

    try:
        batch_id = email_reporting_utils.generate_batch_id()
        failure_count, success_count = data_retrieval.start(
            state_code=state_code,
            report_type=report_type,
            batch_id=batch_id,
            test_address=test_address,
            region_code=region_code,
            email_allowlist=email_allowlist,
            message_body=message_body,
        )
    except InvalidRegionCodeException:
        return "Invalid region code provided", HTTPStatus.BAD_REQUEST
    else:
        test_address_text = (
            f"Emails generated for test address: {test_address}"
            if test_address else "")
        counts_text = f"Successfully generated {success_count} email(s)"
        if failure_count:
            counts_text += f" Failed to generate {failure_count} email(s)"

        return (
            f"New batch started for {state_code} and {report_type}.  Batch "
            f"id = {batch_id}. {test_address_text} {counts_text}"
        ), HTTPStatus.OK
Beispiel #8
0
 def test_get_only_str_param_value(self) -> None:
     self.assertEqual(params.get_only_str_param_value("batch_id", PARAMS),
                      "12345")