"""Plan legacy identities from a snapshot and emit guarded PostgreSQL SQL. This module never opens the production SQLite database or executes target SQL. The caller must execute the returned SQL inside its migration transaction. """ from __future__ import annotations import hashlib import json import re import sqlite3 import uuid SOURCE = "yawan-workbench" TARGET_DATABASE = "yawan_native_acceptance_20260915" TARGET_TENANT = 10059 SOURCE_ORGANIZATION = 1 ADMIN_TARGET_ID = "bad2d456-be74-458e-9037-661a371285b8" ADMIN_TARGET_EMAIL = "admin@yawan.local" ENABLED_SOURCE_IDS = {1, 3, 8} NAMESPACE = uuid.UUID("6ac76fbe-e61f-4a44-9597-893884aa5c63") PREFERENCE_KEY = "yawan_identity_migration" ORGANIZATION_LEDGER = "yawan_legacy_organization_mappings" IDENTITY_LEDGER = "yawan_legacy_identity_mappings" PENDING = ["permission_review_required", "password_reset_required", "placeholder_email_requires_review"] def _stable_id(source_id: int, tenant_id: int) -> str: return str(uuid.uuid5(NAMESPACE, f"{SOURCE}:tenant:{tenant_id}:organization:{SOURCE_ORGANIZATION}:user:{source_id}")) def _source_user(row: tuple, permissions: list[str]) -> dict: return dict(zip(("id", "username", "display_name", "role", "organization_id", "enabled"), row), permissions=permissions) def identity_plan(db: sqlite3.Connection, tenant_id: int) -> dict: """Read the caller's consistent SQLite snapshot without reading passwords.""" if type(tenant_id) is not int or tenant_id != TARGET_TENANT: raise ValueError(f"Identity migration is restricted to tenant {TARGET_TENANT}") organizations = db.execute("SELECT id, name, slug, enabled FROM organization ORDER BY id").fetchall() if len(organizations) != 1 or organizations[0][0] != SOURCE_ORGANIZATION or organizations[0][3] != 1: raise ValueError("Expected exactly one enabled source organization with id 1") organization = dict(zip(("id", "name", "slug", "enabled"), organizations[0])) permissions_by_role = {} role_definitions = [] for role_row in db.execute("SELECT id, role, label, description, permissions, builtin, organization_id, updated_at FROM roledefinition ORDER BY id"): role_id, role, label, description, raw_permissions, builtin, organization_id, updated_at = role_row if organization_id != SOURCE_ORGANIZATION: raise ValueError("Role belongs to a different source organization") if role in permissions_by_role: raise ValueError(f"Duplicate source role: {role}") permissions = json.loads(raw_permissions) if not isinstance(permissions, list) or not all(isinstance(p, str) for p in permissions): raise ValueError(f"Invalid source permission list for role {role}") permissions_by_role[role] = sorted(set(permissions)) role_definitions.append({"id": role_id, "role": role, "label": label, "description": description, "permissions": permissions, "builtin": builtin, "organization_id": organization_id, "updated_at": updated_at}) users, skipped = [], [] seen_names = set() for row in db.execute("SELECT id, username, display_name, role, organization_id, enabled FROM appuser ORDER BY id"): source_id, username, _, role, organization_id, enabled = row if organization_id != SOURCE_ORGANIZATION: raise ValueError(f"User {source_id} belongs to a different source organization") if role not in permissions_by_role: raise ValueError(f"Missing permission definition for role {role}") source = _source_user(row, permissions_by_role[role]) if enabled == 0: skipped.append({"source_user": source, "reason": "source_account_disabled"}) continue if enabled != 1 or type(source_id) is not int or source_id <= 0: raise ValueError("Invalid source user identity or enabled flag") if not username or username != username.strip() or len(username) > 100: raise ValueError(f"Invalid source username for user {source_id}") folded = username.casefold() if folded in seen_names: raise ValueError("Conflicting source usernames after normalization") seen_names.add(folded) if role == "admin": if source_id != 1 or username != "yawan_admin": raise ValueError("Only the explicitly mapped legacy administrator is supported") users.append({"source_user": source, "target_id": ADMIN_TARGET_ID, "action": "map_existing_admin", "target_email": ADMIN_TARGET_EMAIL, "target_membership_role": "admin", "pending_activation": [], "permission_differences": ["existing_admin_identity_mapped_without_modification", "legacy_admin_permissions_not_copied"]}) continue if role != "editor": raise ValueError(f"Unsupported enabled source role: {role}") email = f"legacy-{SOURCE}-t{tenant_id}-o{organization_id}-u{source_id}@migration.invalid" provenance = {"schema_version": 1, "source": SOURCE, "target_tenant_id": tenant_id, "source_organization": organization, "source_user": source, "email_is_placeholder": True, "pending_activation": PENDING, "permission_mapping": "editor_to_contributor_requires_review"} users.append({"source_user": source, "target_id": _stable_id(source_id, tenant_id), "action": "create_suspended", "target_username": username, "target_email": email, "email_is_placeholder": True, "target_membership_role": "contributor", "is_active": False, "membership_status": "suspended", "is_system_admin": False, "can_access_all_tenants": False, "preferences": {PREFERENCE_KEY: provenance}, "pending_activation": list(PENDING), "permission_differences": ["legacy_feature_allowlist_has_no_equivalent", "legacy_resource_grants_not_inferred", "contributor_scope_requires_review"]}) if not any(user["action"] == "map_existing_admin" for user in users): raise ValueError("The explicitly mapped enabled legacy administrator is missing") result = {"schema_version": 1, "source": SOURCE, "target_database": TARGET_DATABASE, "target_tenant_id": tenant_id, "source_organization": organization, "source_role_definitions": role_definitions, "users": users, "skipped": skipped, "password_policy": "no_source_password_read_or_copied; random_non_bcrypt_placeholder_in_sql", "repeat_policy": "durable_ledger_identity; preserve_password_activation_and_contact_state", "mapping_ledger": {"organizations": ORGANIZATION_LEDGER, "identities": IDENTITY_LEDGER}, "preferences_warning": "preferences_are_informational; durable_ledger_retains_source_identity"} _validate_plan(result) return result def _validate_plan(plan: dict) -> None: if (plan.get("schema_version") != 1 or plan.get("source") != SOURCE or plan.get("target_database") != TARGET_DATABASE or plan.get("target_tenant_id") != TARGET_TENANT or plan.get("source_organization", {}).get("id") != SOURCE_ORGANIZATION): raise ValueError("Identity plan has an unsupported source or target scope") definitions = plan.get("source_role_definitions", []) roles = {definition.get("role") for definition in definitions} if roles != {"admin", "editor", "reviewer", "viewer"} or len(definitions) != len(roles): raise ValueError("Identity plan must preserve all four reviewed source role definitions") for definition in definitions: if definition.get("organization_id") != SOURCE_ORGANIZATION: raise ValueError("Identity role definition has a different source organization") if not isinstance(definition.get("permissions"), list) or not all(isinstance(p, str) for p in definition["permissions"]): raise ValueError("Identity role definition has invalid source permissions") role_permissions = {definition["role"]: sorted(set(definition["permissions"])) for definition in definitions} seen_ids, seen_names, seen_emails = set(), set(), set() admin_count = 0 for user in plan.get("users", []): source = user["source_user"] if source["organization_id"] != SOURCE_ORGANIZATION or source["enabled"] != 1: raise ValueError("Identity plan includes a disabled or cross-organization user") if source.get("permissions") != role_permissions.get(source["role"]): raise ValueError("Identity user permissions differ from the preserved source role") if user["action"] == "map_existing_admin": admin_count += 1 if (source["id"] != 1 or source["username"] != "yawan_admin" or source["role"] != "admin" or user["target_id"] != ADMIN_TARGET_ID or user["target_email"] != ADMIN_TARGET_EMAIL or user["target_membership_role"] != "admin"): raise ValueError("Identity plan changes the pinned administrator mapping") elif user["action"] == "create_suspended": expected_email = f"legacy-{SOURCE}-t{TARGET_TENANT}-o{SOURCE_ORGANIZATION}-u{source['id']}@migration.invalid" if (source["role"] != "editor" or user["target_id"] != _stable_id(source["id"], TARGET_TENANT) or user["target_username"] != source["username"] or user["target_email"] != expected_email or user["is_active"] is not False or user["is_system_admin"] is not False or user["can_access_all_tenants"] is not False or user["membership_status"] != "suspended" or user["target_membership_role"] != "contributor" or user["pending_activation"] != PENDING): raise ValueError("Identity plan changes the suspended account policy") metadata = user["preferences"].get(PREFERENCE_KEY, {}) if (metadata.get("source") != SOURCE or metadata.get("target_tenant_id") != TARGET_TENANT or metadata.get("source_user") != source or metadata.get("source_organization") != plan["source_organization"] or metadata.get("pending_activation") != PENDING or metadata.get("email_is_placeholder") is not True): raise ValueError("Identity plan has inconsistent source provenance") name = user["target_username"].strip().casefold() email = user["target_email"].strip().casefold() if name in seen_names or email in seen_emails: raise ValueError("Identity plan has duplicate usernames or emails") seen_names.add(name) seen_emails.add(email) else: raise ValueError("Unknown identity plan action") if user["target_id"] in seen_ids: raise ValueError("Identity plan has duplicate target user IDs") seen_ids.add(user["target_id"]) if admin_count != 1: raise ValueError("Identity plan must map exactly one existing administrator") if {user["source_user"]["id"] for user in plan["users"]} != ENABLED_SOURCE_IDS: raise ValueError("Enabled source identities differ from the reviewed migration batch") def identity_sql(plan: dict) -> str: """Emit SQL with fail-closed identity checks and no transaction boundaries.""" _validate_plan(plan) digest = plan.get("source_snapshot_sha256") if not isinstance(digest, str) or not re.fullmatch(r"[a-f0-9]{64}", digest): raise ValueError("Identity SQL requires the caller's source_snapshot_sha256") payload = json.dumps(plan, ensure_ascii=True, sort_keys=True, separators=(",", ":")) literal = "'" + payload.replace("'", "''") + "'::jsonb" tag = "identity_" + hashlib.sha256(payload.encode()).hexdigest()[:16] return f"""SET LOCAL standard_conforming_strings = on; DO ${tag}$ DECLARE migration jsonb := {literal}; item jsonb; existing_user users%ROWTYPE; existing_member tenant_members%ROWTYPE; mapped_organization record; mapped_user record; has_mapping boolean; has_user boolean; collision_count integer; BEGIN IF current_database() <> '{TARGET_DATABASE}' THEN RAISE EXCEPTION 'Identity migration target database mismatch'; END IF; PERFORM pg_advisory_xact_lock(10059, 11901); PERFORM 1 FROM tenants WHERE id = {TARGET_TENANT} AND status = 'active' AND deleted_at IS NULL FOR SHARE; IF NOT FOUND THEN RAISE EXCEPTION 'Identity migration requires active tenant {TARGET_TENANT}'; END IF; -- Lock identities so concurrent signups cannot race normalized-name checks. LOCK TABLE users IN SHARE ROW EXCLUSIVE MODE; LOCK TABLE tenant_members IN SHARE ROW EXCLUSIVE MODE; -- Independent records survive native preference saves and target deletion. -- No user foreign key: the historical mapping must not block native deletes. CREATE TABLE IF NOT EXISTS {ORGANIZATION_LEDGER} ( source_system text NOT NULL, source_organization_id bigint NOT NULL, target_tenant_id integer NOT NULL, source_snapshot jsonb NOT NULL, source_role_definitions jsonb NOT NULL, source_snapshot_sha256 varchar(64) NOT NULL CHECK (source_snapshot_sha256 ~ '^[a-f0-9]{{64}}$'), schema_version integer NOT NULL DEFAULT 1 CHECK (schema_version = 1), created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (source_system, source_organization_id), UNIQUE (source_system, target_tenant_id) ); CREATE TABLE IF NOT EXISTS {IDENTITY_LEDGER} ( source_system text NOT NULL, source_organization_id bigint NOT NULL, source_user_id bigint NOT NULL, target_tenant_id integer NOT NULL, target_user_id varchar(36) NOT NULL, migration_action text NOT NULL CHECK (migration_action IN ('map_existing_admin', 'create_suspended')), source_snapshot jsonb NOT NULL, source_snapshot_sha256 varchar(64) NOT NULL CHECK (source_snapshot_sha256 ~ '^[a-f0-9]{{64}}$'), initial_requested_username text NOT NULL, initial_requested_email text NOT NULL, schema_version integer NOT NULL DEFAULT 1 CHECK (schema_version = 1), created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (source_system, source_organization_id, source_user_id), UNIQUE (target_tenant_id, target_user_id) ); LOCK TABLE {ORGANIZATION_LEDGER}, {IDENTITY_LEDGER} IN SHARE ROW EXCLUSIVE MODE; SELECT * INTO mapped_organization FROM {ORGANIZATION_LEDGER} WHERE source_system = '{SOURCE}' AND source_organization_id = {SOURCE_ORGANIZATION}; IF FOUND THEN IF mapped_organization.target_tenant_id IS DISTINCT FROM {TARGET_TENANT} OR mapped_organization.schema_version IS DISTINCT FROM 1 OR mapped_organization.source_snapshot IS DISTINCT FROM migration->'source_organization' OR mapped_organization.source_role_definitions IS DISTINCT FROM migration->'source_role_definitions' THEN RAISE EXCEPTION 'Identity migration organization mapping conflict'; END IF; ELSE INSERT INTO {ORGANIZATION_LEDGER} (source_system, source_organization_id, target_tenant_id, source_snapshot, source_role_definitions, source_snapshot_sha256) VALUES ('{SOURCE}', {SOURCE_ORGANIZATION}, {TARGET_TENANT}, migration->'source_organization', migration->'source_role_definitions', migration->>'source_snapshot_sha256'); END IF; FOR item IN SELECT value FROM jsonb_array_elements(migration->'users') LOOP SELECT * INTO mapped_user FROM {IDENTITY_LEDGER} WHERE source_system = '{SOURCE}' AND source_organization_id = {SOURCE_ORGANIZATION} AND source_user_id = (item->'source_user'->>'id')::bigint; has_mapping := FOUND; IF has_mapping THEN IF mapped_user.target_tenant_id IS DISTINCT FROM {TARGET_TENANT} OR mapped_user.target_user_id IS DISTINCT FROM item->>'target_id' OR mapped_user.schema_version IS DISTINCT FROM 1 OR mapped_user.migration_action IS DISTINCT FROM item->>'action' OR mapped_user.source_snapshot IS DISTINCT FROM item->'source_user' OR mapped_user.initial_requested_username IS DISTINCT FROM COALESCE(item->>'target_username', item->'source_user'->>'username') OR mapped_user.initial_requested_email IS DISTINCT FROM item->>'target_email' THEN RAISE EXCEPTION 'Durable identity mapping conflict for source user %', item->'source_user'->>'id'; END IF; ELSE IF EXISTS (SELECT 1 FROM {IDENTITY_LEDGER} WHERE target_tenant_id = {TARGET_TENANT} AND target_user_id = item->>'target_id') THEN RAISE EXCEPTION 'Target identity is already mapped to another source user'; END IF; SELECT count(*) INTO collision_count FROM users WHERE id <> item->>'target_id' AND (lower(btrim(username)) = lower(btrim(COALESCE(item->>'target_username', item->'source_user'->>'username'))) OR lower(btrim(email)) = lower(btrim(item->>'target_email'))); IF collision_count <> 0 THEN RAISE EXCEPTION 'Identity migration username/email collision for source user %', item->'source_user'->>'id'; END IF; END IF; SELECT * INTO existing_user FROM users WHERE id = item->>'target_id' FOR UPDATE; has_user := FOUND; IF item->>'action' = 'map_existing_admin' THEN IF NOT has_user OR existing_user.email IS DISTINCT FROM '{ADMIN_TARGET_EMAIL}' OR existing_user.is_active IS DISTINCT FROM true OR existing_user.is_system_admin IS DISTINCT FROM true OR existing_user.can_access_all_tenants IS DISTINCT FROM false OR existing_user.deleted_at IS NOT NULL THEN RAISE EXCEPTION 'Pinned existing administrator failed identity checks'; END IF; PERFORM 1 FROM tenant_members WHERE user_id = '{ADMIN_TARGET_ID}' AND tenant_id = {TARGET_TENANT} AND role = 'admin' AND status = 'active' AND deleted_at IS NULL; IF NOT FOUND THEN RAISE EXCEPTION 'Pinned administrator is not active in migration tenant'; END IF; ELSIF has_user THEN IF existing_user.tenant_id IS DISTINCT FROM {TARGET_TENANT} OR existing_user.is_system_admin IS DISTINCT FROM false OR existing_user.can_access_all_tenants IS DISTINCT FROM false OR existing_user.deleted_at IS NOT NULL THEN RAISE EXCEPTION 'Identity migration ID/provenance collision for source user %', item->'source_user'->>'id'; END IF; -- Upgrade an exact pre-ledger import; never adopt an unmarked user. IF NOT has_mapping AND ( existing_user.username IS DISTINCT FROM item->>'target_username' OR existing_user.email IS DISTINCT FROM item->>'target_email' OR existing_user.preferences->'{PREFERENCE_KEY}' IS DISTINCT FROM item->'preferences'->'{PREFERENCE_KEY}') THEN RAISE EXCEPTION 'Unmapped existing identity lacks exact migration provenance'; END IF; SELECT * INTO existing_member FROM tenant_members WHERE user_id = existing_user.id AND tenant_id = {TARGET_TENANT} AND deleted_at IS NULL; IF NOT FOUND OR existing_member.role <> 'contributor' OR existing_member.status NOT IN ('suspended', 'active') THEN RAISE EXCEPTION 'Existing migrated membership does not match this batch'; END IF; IF EXISTS (SELECT 1 FROM tenant_members WHERE user_id = existing_user.id AND tenant_id <> {TARGET_TENANT} AND deleted_at IS NULL) THEN RAISE EXCEPTION 'Existing migrated identity has unexpected cross-tenant membership'; END IF; ELSE IF has_mapping THEN RAISE EXCEPTION 'Mapped target user is missing; refusing to recreate a deleted account'; END IF; IF EXISTS (SELECT 1 FROM tenant_members WHERE user_id = item->>'target_id') THEN RAISE EXCEPTION 'Orphan or deleted membership conflicts with new migrated identity'; END IF; INSERT INTO users (id, username, email, password_hash, tenant_id, is_active, is_system_admin, can_access_all_tenants, preferences, created_at, updated_at) VALUES (item->>'target_id', item->>'target_username', item->>'target_email', '!migration-login-disabled:' || gen_random_uuid()::text, {TARGET_TENANT}, false, false, false, item->'preferences', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); INSERT INTO tenant_members (user_id, tenant_id, role, status, joined_at, created_at, updated_at) VALUES (item->>'target_id', {TARGET_TENANT}, 'contributor', 'suspended', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); END IF; IF NOT has_mapping THEN INSERT INTO {IDENTITY_LEDGER} (source_system, source_organization_id, source_user_id, target_tenant_id, target_user_id, migration_action, source_snapshot, source_snapshot_sha256, initial_requested_username, initial_requested_email) VALUES ('{SOURCE}', {SOURCE_ORGANIZATION}, (item->'source_user'->>'id')::bigint, {TARGET_TENANT}, item->>'target_id', item->>'action', item->'source_user', migration->>'source_snapshot_sha256', COALESCE(item->>'target_username', item->'source_user'->>'username'), item->>'target_email'); END IF; END LOOP; END; ${tag}$; """ def identity_verification_sql(plan: dict) -> str: """Return a read-only query, one row per mapped user and no password fields.""" _validate_plan(plan) payload = json.dumps(plan, ensure_ascii=True, sort_keys=True, separators=(",", ":")) literal = "'" + payload.replace("'", "''") + "'::jsonb" return f"""WITH migration AS (SELECT {literal} AS body), planned AS (SELECT value AS item FROM migration, jsonb_array_elements(body->'users')) SELECT (item->'source_user'->>'id')::bigint AS source_user_id, item->>'target_id' AS expected_target_user_id, current_database() = '{TARGET_DATABASE}' AS database_matches, COALESCE(t.status = 'active' AND t.deleted_at IS NULL, false) AS tenant_matches, COALESCE(o.target_tenant_id = {TARGET_TENANT} AND o.schema_version = 1 AND o.source_snapshot = body->'source_organization' AND o.source_role_definitions = body->'source_role_definitions', false) AS organization_mapping_matches, COALESCE(l.target_tenant_id = {TARGET_TENANT} AND l.target_user_id = item->>'target_id' AND l.schema_version = 1 AND l.source_snapshot = item->'source_user' AND l.migration_action = item->>'action' AND l.initial_requested_username = COALESCE(item->>'target_username', item->'source_user'->>'username') AND l.initial_requested_email = item->>'target_email', false) AS identity_mapping_matches, COALESCE(u.deleted_at IS NULL AND u.id IS NOT NULL AND CASE WHEN item->>'action' = 'map_existing_admin' THEN u.email = '{ADMIN_TARGET_EMAIL}' AND u.is_system_admin AND u.is_active ELSE u.tenant_id = {TARGET_TENANT} AND NOT u.is_system_admin AND NOT EXISTS (SELECT 1 FROM tenant_members other_member WHERE other_member.user_id = u.id AND other_member.tenant_id <> {TARGET_TENANT} AND other_member.deleted_at IS NULL) END AND NOT u.can_access_all_tenants, false) AS account_scope_matches, COALESCE(m.role = item->>'target_membership_role' AND CASE WHEN item->>'action' = 'map_existing_admin' THEN m.status = 'active' ELSE m.status IN ('suspended', 'active') END, false) AS membership_matches, u.username, u.email, u.is_active, m.status AS membership_status, l.source_snapshot_sha256 AS original_source_snapshot_sha256 FROM planned CROSS JOIN migration LEFT JOIN tenants t ON t.id = {TARGET_TENANT} LEFT JOIN {ORGANIZATION_LEDGER} o ON o.source_system = '{SOURCE}' AND o.source_organization_id = {SOURCE_ORGANIZATION} LEFT JOIN {IDENTITY_LEDGER} l ON l.source_system = '{SOURCE}' AND l.source_organization_id = {SOURCE_ORGANIZATION} AND l.source_user_id = (item->'source_user'->>'id')::bigint LEFT JOIN users u ON u.id = item->>'target_id' LEFT JOIN tenant_members m ON m.user_id = u.id AND m.tenant_id = {TARGET_TENANT} AND m.deleted_at IS NULL ORDER BY source_user_id; """