"""Preserve deleted owner 2 without reconstructing or activating an account. The caller supplies a consistent read-only SQLite snapshot and executes any returned SQL in its own transaction. No source credentials are read. This separate batch does not broaden the enabled-user identity migration. """ from __future__ import annotations import hashlib import json import re import sqlite3 from weknora_identity_etl import (IDENTITY_LEDGER, ORGANIZATION_LEDGER, PREFERENCE_KEY, SOURCE, SOURCE_ORGANIZATION, TARGET_DATABASE, TARGET_TENANT, _stable_id, identity_plan) SOURCE_USER_ID = 2 BATCH = "deleted-owner-2" TARGET_USERNAME = "\u5df2\u5220\u9664\u7684\u65e7\u7528\u6237 #2" TARGET_EMAIL = f"legacy-{SOURCE}-t{TARGET_TENANT}-o1-u2@migration.invalid" OWNER_REFERENCES = {"canvas": [1, 2], "trainingmap": [2, 3]} POLICY = "historical_owner_only; no_login; no_inferred_identity_permissions_or_grants" def _source_tombstone() -> dict: return {"id": SOURCE_USER_ID, "organization_id": SOURCE_ORGANIZATION, "tombstone": True, "source_record_missing": True, "owner_references": {key: list(value) for key, value in OWNER_REFERENCES.items()}} def _historical_user(organization: dict) -> dict: source = _source_tombstone() provenance = {"schema_version": 1, "source": SOURCE, "batch": BATCH, "target_tenant_id": TARGET_TENANT, "source_organization": organization, "source_user": source, "email_is_placeholder": True, "account_policy": POLICY, "pending_activation": [], "permission_mapping": "none_historical_owner_only"} return {"source_user": source, "target_id": _stable_id(SOURCE_USER_ID, TARGET_TENANT), "action": "create_suspended", "target_username": TARGET_USERNAME, "target_email": TARGET_EMAIL, "email_is_placeholder": True, "target_membership_role": "contributor", "is_active": False, "membership_status": "suspended", "is_system_admin": False, "can_access_all_tenants": False, "pending_activation": [], "preferences": {PREFERENCE_KEY: provenance}} def historical_identity_plan(db: sqlite3.Connection, tenant_id: int) -> dict: """Plan only the reviewed, missing owner referenced by four source rows.""" # Reuse organization/role validation without changing the original batch. enabled = identity_plan(db, tenant_id) if db.execute("SELECT id FROM appuser WHERE id = ?", (SOURCE_USER_ID,)).fetchone() is not None: raise ValueError("Historical owner source record is present; separate identity review required") for table, expected_ids in OWNER_REFERENCES.items(): rows = db.execute(f"SELECT id, organization_id FROM {table} WHERE owner_user_id = ? ORDER BY id", (SOURCE_USER_ID,)).fetchall() if rows != [(source_id, SOURCE_ORGANIZATION) for source_id in expected_ids]: raise ValueError(f"Historical owner references differ from the reviewed {table} batch") organization = enabled["source_organization"] plan = {"schema_version": 1, "source": SOURCE, "batch": BATCH, "target_database": TARGET_DATABASE, "target_tenant_id": tenant_id, "source_organization": organization, "source_role_definitions": enabled["source_role_definitions"], "users": [_historical_user(organization)], "skipped": [], "account_policy": POLICY, "password_policy": "no_source_password_read_or_copied; random_non_bcrypt_placeholder_in_sql", "repeat_policy": "durable_ledger; no_updates; no_recreation; reject_activation_or_tokens"} _validate_plan(plan) return plan def _validate_plan(plan: dict) -> None: if (plan.get("schema_version") != 1 or plan.get("source") != SOURCE or plan.get("batch") != BATCH or plan.get("target_database") != TARGET_DATABASE or type(plan.get("target_tenant_id")) is not int or plan["target_tenant_id"] != TARGET_TENANT or plan.get("account_policy") != POLICY): raise ValueError("Historical identity plan has an unsupported source or target scope") organization = plan.get("source_organization", {}) if organization.get("id") != SOURCE_ORGANIZATION or organization.get("enabled") != 1: raise ValueError("Historical identity plan requires the reviewed source organization") 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("Historical identity plan must retain the reviewed organization role definitions") for definition in definitions: permissions = definition.get("permissions") if (definition.get("organization_id") != SOURCE_ORGANIZATION or not isinstance(permissions, list) or not all(isinstance(p, str) for p in permissions)): raise ValueError("Historical identity plan has invalid organization role definitions") # Compare canonical JSON so bool/int coercion cannot validate tampered flags. if _canonical(plan.get("users")) != _canonical([_historical_user(organization)]): raise ValueError("Historical identity plan changes the pinned tombstone or suspended account policy") def _canonical(value) -> str: return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")) def _payload(plan: dict) -> tuple[str, str]: _validate_plan(plan) if not re.fullmatch(r"[a-f0-9]{64}", str(plan.get("source_snapshot_sha256", ""))): raise ValueError("Historical identity SQL requires the caller's source_snapshot_sha256") payload = _canonical(plan) return "'" + payload.replace("'", "''") + "'::jsonb", hashlib.sha256(payload.encode()).hexdigest()[:16] def historical_identity_sql(plan: dict) -> str: """Emit insert-only SQL; existing durable ledgers are mandatory.""" literal, digest = _payload(plan) return f"""SET LOCAL standard_conforming_strings = on; DO $historical_identity_{digest}$ DECLARE migration jsonb := {literal}; item jsonb := migration->'users'->0; mapped_user record; existing_user users%ROWTYPE; existing_member tenant_members%ROWTYPE; has_mapping boolean; has_user boolean; BEGIN IF current_database() <> '{TARGET_DATABASE}' THEN RAISE EXCEPTION 'Historical identity target database mismatch'; END IF; PERFORM pg_advisory_xact_lock({TARGET_TENANT}, 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 'Historical identity requires active target tenant'; END IF; LOCK TABLE users, tenant_members, auth_tokens IN SHARE ROW EXCLUSIVE MODE; LOCK TABLE {ORGANIZATION_LEDGER}, {IDENTITY_LEDGER} IN SHARE ROW EXCLUSIVE MODE; PERFORM 1 FROM {ORGANIZATION_LEDGER} WHERE source_system='{SOURCE}' AND source_organization_id={SOURCE_ORGANIZATION} AND target_tenant_id={TARGET_TENANT} AND schema_version=1 AND source_snapshot=migration->'source_organization' AND source_role_definitions=migration->'source_role_definitions'; IF NOT FOUND THEN RAISE EXCEPTION 'Historical identity organization mapping conflict'; END IF; SELECT * INTO mapped_user FROM {IDENTITY_LEDGER} WHERE source_system='{SOURCE}' AND source_organization_id={SOURCE_ORGANIZATION} AND source_user_id={SOURCE_USER_ID}; 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 'create_suspended' OR mapped_user.source_snapshot IS DISTINCT FROM item->'source_user' OR mapped_user.initial_requested_username IS DISTINCT FROM item->>'target_username' OR mapped_user.initial_requested_email IS DISTINCT FROM item->>'target_email' THEN RAISE EXCEPTION 'Historical durable identity mapping conflict'; 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 'Historical target identity is mapped to another source'; END IF; IF EXISTS (SELECT 1 FROM users WHERE id <> item->>'target_id' AND (lower(btrim(username))=lower(btrim(item->>'target_username')) OR lower(btrim(email))=lower(btrim(item->>'target_email')))) THEN RAISE EXCEPTION 'Historical identity username/email collision'; END IF; END IF; SELECT * INTO existing_user FROM users WHERE id=item->>'target_id' FOR UPDATE; has_user := FOUND; IF 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 'Historical identity ID/provenance collision'; END IF; IF existing_user.is_active IS DISTINCT FROM false OR existing_user.password_hash NOT LIKE '!migration-login-disabled:%' THEN RAISE EXCEPTION 'Historical identity must remain login-disabled'; END IF; 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 historical 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 IS DISTINCT FROM 'contributor' OR existing_member.status IS DISTINCT FROM 'suspended' THEN RAISE EXCEPTION 'Historical identity membership must remain suspended'; 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 'Historical identity has unexpected cross-tenant membership'; END IF; ELSE IF has_mapping THEN RAISE EXCEPTION 'Mapped historical 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 historical 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 EXISTS (SELECT 1 FROM auth_tokens WHERE user_id=item->>'target_id') THEN RAISE EXCEPTION 'Historical identity must not have authentication tokens'; 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},{SOURCE_USER_ID},{TARGET_TENANT},item->>'target_id', 'create_suspended',item->'source_user',migration->>'source_snapshot_sha256', item->>'target_username',item->>'target_email'); END IF; END $historical_identity_{digest}$; """ def historical_identity_verification_sql(plan: dict) -> str: """Check provenance and disabled state without returning credentials.""" literal, _ = _payload(plan) return f"""WITH migration AS (SELECT {literal} AS body), planned AS (SELECT body, body->'users'->0 AS item FROM migration) SELECT {SOURCE_USER_ID} 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.migration_action='create_suspended' AND l.source_snapshot=item->'source_user' AND l.initial_requested_username=item->>'target_username' AND l.initial_requested_email=item->>'target_email',false) AS identity_mapping_matches, coalesce(u.id IS NOT NULL AND u.deleted_at IS NULL AND u.tenant_id={TARGET_TENANT} AND NOT u.is_system_admin AND NOT u.can_access_all_tenants 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),false) AS account_scope_matches, coalesce(NOT u.is_active AND u.password_hash LIKE '!migration-login-disabled:%',false) AS login_disabled, coalesce(m.role='contributor' AND m.status='suspended',false) AS membership_matches, NOT EXISTS (SELECT 1 FROM auth_tokens a WHERE a.user_id=item->>'target_id') AS no_auth_tokens, u.username,u.is_active,m.status AS membership_status, l.source_snapshot_sha256 AS original_source_snapshot_sha256 FROM planned 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={SOURCE_USER_ID} 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; """