"""Track domain row provenance + relax employee RBAC for /cmdb/domains. Two non-obvious facts surfaced by a 2026-05-29 employee report (``孟凡生 Jason Meng`` saw only 2 of his 11 owned-domains and every visible row was already-expired with auto-renew = false): 1. **62 / 62 prod ``cmdb_domains`` rows are NOT cloud-API-synced.** They land via ``server/scripts/migrate_cmdb.py`` (the legacy GCP CloudSQL → Helix import). That script writes ``registrar`` from the legacy WHOIS ``possessor`` column (``"PingCAP Ltd."`` / ``"平凯星辰…"``) instead of the cloud-provider slug (``"aliyun"`` / ``"tencent"`` …) the live driver path emits (see [aliyun.py:681](server/app/domains/cmdb/providers/aliyun.py#L681), [tencent.py:338](server/app/domains/cmdb/providers/tencent.py#L338)). ``expiry_date`` is frozen at migration time; ``auto_renew`` defaults ``False`` because the migration INSERT omits the column. The UI presents these stale values with the same visual weight as a fresh sync — admins/employees cannot tell. 2. **UCloud has no ``list_domains`` driver** ([providers/ucloud.py]( server/app/domains/cmdb/providers/ucloud.py)) — yet 50+ domains are tagged under the UCloud integration_id from the import. They will never be re-synced; ``last_synced_at`` is frozen at import time and never advances. This migration adds ``cmdb_domains.data_source ENUM`` so the API, the admin annotate path, and the UI can finally distinguish three provenance states: * ``synced`` — last write came from a cloud driver upsert. * ``migrated`` — landed via the legacy ``migrate_cmdb.py`` script and never re-synced; expiry / auto_renew / registrar are stale. * ``manual`` — admin edited expiry / auto_renew / registrar directly via ``PATCH /cmdb/domains/{id}``. Locked against being clobbered by a subsequent sync unless the sync brings a real driver value (in which case ``synced`` wins — cloud is the source of truth when it speaks). The backfill rule is conservative: any row whose ``registrar`` is NULL or not in the known cloud-driver slug whitelist is marked ``migrated``. The whitelist is the union of every driver's ``registrar=...`` literal — keeping it inline (not a DB-enum) so a future driver addition does not require a DB migration; the only cost is a tiny one-time backfill scan over a 62-row table. Sister change in the same PR: * ``app/domains/cmdb/router.py::list_domains`` / ``get_domain`` drop the ``owner_person_id=self`` employee auto-demote. Domains are company-public identifiers (every PingCAP employee already knows ``pingcap.com``); the demote was over-correction copied from ``list_servers``. DNS / CDN / servers / LB / IAM / object storage all KEEP their owner demote because they leak internal hostnames / IPs / DKIM / origins / ACLs (reconnaissance bait). See decision_domain_public_visibility.md. """ import sqlalchemy as sa from alembic import op revision = "218" down_revision = "217" branch_labels = None depends_on = None # Slugs every domain-capable cloud driver writes into ``registrar``. # Keeping this list inline (not an ENUM) so future driver additions # don't force a DB migration — the only cost is a one-line update to # this set + a single conservative backfill scan. _DRIVER_REGISTRAR_SLUGS = ( "aliyun", "tencent", "aws", "gcp", "azure", "alicloud_intl", "ksyun", "ucloud", "huawei", "dnspod", ) def upgrade() -> None: # 1 — column add. Default 'synced' so future driver upserts land # the correct provenance even before the upsert SQL is updated. # NOT NULL with server_default works on TiDB; we use a plain # VARCHAR rather than ENUM because TiDB's online schema change # on ENUM values is materially slower than on VARCHAR + CHECK # (we enforce the membership invariant at the application layer # in models.py / schemas.py — see app/domains/cmdb/models.py). op.add_column( "cmdb_domains", sa.Column( "data_source", sa.String(length=16), nullable=False, server_default="synced", ), ) # 2 — backfill. Anything whose ``registrar`` is NULL or NOT a # driver slug is by definition NOT cloud-synced; mark migrated. # We don't touch ``last_synced_at`` because it carries useful # information (when the legacy import ran) that the audit log # would otherwise lose. placeholders = ",".join(f":s{i}" for i in range(len(_DRIVER_REGISTRAR_SLUGS))) params = {f"s{i}": v for i, v in enumerate(_DRIVER_REGISTRAR_SLUGS)} op.execute( sa.text( "UPDATE cmdb_domains " "SET data_source = 'migrated' " "WHERE deleted_at IS NULL " " AND (registrar IS NULL " f" OR registrar NOT IN ({placeholders}))" ).bindparams(**params) ) def downgrade() -> None: op.drop_column("cmdb_domains", "data_source")