Source code for operators.postgres.utils.mangle_users

"""
Mangle Users Operator

This module provides an operator for mangling user emails in a Postgres database.
Mangling prefixes emails and nulls passwords, effectively disabling login access
while preserving the user records for referential integrity.

Usage:
    from operators.postgres.utils.mangle_users import MangleUsersOperator

    mangle_prod_users = MangleUsersOperator(
        task_id="mangle_prod_users",
        postgres_conn_id="boss_grapi_stage",
        tenant="arkansas",
        exclude_emails=["admin@ready.net", "test@ready.net"],
        prefix="synced",
    )
"""

from __future__ import annotations

from typing import Sequence

from airflow.models import BaseOperator
from airflow.providers.postgres.hooks.postgres import PostgresHook
from airflow.utils.context import Context


################################################################################
## Constants
################################################################################

DEFAULT_PREFIX = "synced"
DEFAULT_DOMAIN = "ready.net"
STAGE_CONN_PATTERN = "stage"

SQL_MANGLE_USERS = """
UPDATE {tenant}."User"
SET
    email = CONCAT('{prefix}+', REGEXP_REPLACE(email, '@|\\+', '_', 'g'), '@{domain}'),
    password = NULL
WHERE email NOT LIKE '{prefix}+%%'
AND email NOT LIKE '%%@{domain}'
AND email != ALL(%(exclude_emails)s);
"""


################################################################################
## Operator
################################################################################


[docs] class MangleUsersOperator(BaseOperator): """ Mangle user emails by prefixing and nulling passwords. This disables login access while preserving user records for FK integrity. Emails are transformed: user@example.com -> synced+user_example.com@ready.net :param postgres_conn_id: Airflow connection ID for the Postgres database. :param tenant: The tenant schema name. :param exclude_emails: List of email addresses to exclude from mangling. :param prefix: Prefix to prepend to mangled emails (default: "synced"). :param domain: Domain to append to mangled emails (default: "ready.net"). """ ui_color = "#DEFACE" template_fields: Sequence[str] = ("postgres_conn_id", "tenant", "exclude_emails") def __init__( self, *, postgres_conn_id: str, tenant: str, exclude_emails: list[str] | None = None, prefix: str = DEFAULT_PREFIX, domain: str = DEFAULT_DOMAIN, **kwargs, ): super().__init__(**kwargs) self.postgres_conn_id = postgres_conn_id self.tenant = tenant self.exclude_emails = exclude_emails or [] self.prefix = prefix self.domain = domain ############################################################################ ## Execute ############################################################################
[docs] def execute(self, context: Context) -> int: if STAGE_CONN_PATTERN not in self.postgres_conn_id.lower(): raise ValueError( f"Safety check failed: '{self.postgres_conn_id}' does not appear to be a stage connection. " "This operator should only modify stage databases." ) hook = PostgresHook(postgres_conn_id=self.postgres_conn_id) sql = SQL_MANGLE_USERS.format( tenant=self.tenant, prefix=self.prefix, domain=self.domain, ) exclude_list = self.exclude_emails if self.exclude_emails else [] self.log.info("Mangling users in %s.User (excluding %d emails)", self.tenant, len(exclude_list)) self.log.debug("SQL: %s", sql) hook.run(sql, parameters={"exclude_emails": exclude_list}) self.log.info("Mangled users in %s.User", self.tenant) return 0