Files
xinmi/scripts/weknora_identity_etl.py
T
yawan f7fc992f57
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

391 lines
25 KiB
Python

"""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;
"""