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, )
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.", )
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
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
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, )
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
def test_get_only_str_param_value(self) -> None: self.assertEqual(params.get_only_str_param_value("batch_id", PARAMS), "12345")