# Cutover Runbook — bermooda_usermanager → UserManager (IdP) Operational steps for cutting Hamsoo over to UserManager as the identity provider, and for rolling back if verification fails. Every gate below has a command; do not advance while a gate is red. ## 1. Preconditions (all must be green) | Gate | Command | Required result | |---|---|---| | Hamsoo suite | `manage.py test --noinput` (in `hamsoo_backend`) | `OK` (1 documented skip) | | UserManager suite | `manage.py test --noinput` (in `apps/api`) | `OK` | | Migration drift | `manage.py makemigrations --check` (in `apps/api`) | `No changes detected` | | Export | `python apps/identity/migration/export_identity_dump.py --db-name --output dump.json` | counts match the legacy tables | | Import dry-run | `manage.py import_legacy_identity --report import_report.json` | `has_problems: false`, exit 0, **0 collisions** | | Phone OTP | covered by `manage.py test apps.sms` (43 tests) | `OK` | | Shadow divergence | Hamsoo: `manage.py auth_divergence_report` | `blocking` = 0 | Export notes: * The exporter reads MySQL directly (`authmanager_useraccount`, `workspacemanager_workspace`, `workspacemanager_workspacemember`) and needs no Django. It runs with any Python that has `pymysql` or `mysqlclient`; in a venv without a driver it falls back to the machine's global CPython 3.12 site-packages (`mysqlclient`). * Connection settings: `--db-host/--db-port/--db-name/--db-user/--db-password` or `LEGACY_DB_*` environment variables. No password default. * Rehearsal source on this machine: `auth_staging_new_2` (144 users / 198 workspaces / 0 members — verified against the 10-11 dump). Point `--db-name` at the production legacy database for the real cutover. ## 2. Production environment ### UserManager (`apps/api`) | Variable | Value | Notes | |---|---|---| | `DJANGO_ENV` | `production` | boot refuses dev defaults | | `DJANGO_SECRET_KEY` | *secret* | | | `JWT_SIGNING_KEY` | *secret* (≥32 bytes) | boot fails on dev default | | `OIDC_ISSUER` | `https://` | embedded in `iss`; must match what clients validate | | `SSO_COOKIE_DOMAIN` | `.hamsoo.me` | parent-domain cookie for SSO | | `SSO_TRUSTED_REDIRECT_HOSTS` | `hamsoo.me,www.hamsoo.me` | post-login allow-list | | `SMS_PROVIDER` | `kavenegar` | boot refuses `console` in production | | `KAVENEGAR_API_KEY` / `KAVENEGAR_SENDER` | *secret* / sender id | | | `EMAIL_FROM_ADDRESS` | no-reply domain address | | | `MAIL_DOMAIN`, `MAIL_INBOUND_SECRET` | mailbox domain + *secret* | | | `MEDIA_ROOT` | persistent volume | mailbox uploads | | `DJANGO_ALLOWED_HOSTS`, `CORS_ALLOWED_ORIGINS`, `CSRF_TRUSTED_ORIGINS` | IdP host + `https://hamsoo.me` | | ### Hamsoo backend (`hamsoo_backend`) | Variable | Value | Notes | |---|---|---| | `IS_PRODUCTION` | `1` | | | `IDENTITY_PLATFORM_BASE_URL` | `https://` | | | `IDENTITY_PLATFORM_ISSUER` | same as `OIDC_ISSUER` | `iss` validation | | `IDENTITY_PLATFORM_CLIENT_ID` / `CLIENT_SECRET` | *secret* | BFF-only; never expose to the browser | | `IDENTITY_PLATFORM_REDIRECT_URI` | `https://hamsoo.me/sso/callback` | registered in the `hamsoo` OAuth client | | `IDENTITY_PLATFORM_JWKS_URL` / `AUDIENCE` | JWKS endpoint / audience | | | `SSO_FRONTEND_BASE_URL` | `https://hamsoo.me` | | ## 3. Cutover sequence 1. **Freeze** writes to the legacy usermanager (announce maintenance window). 2. **Export** the legacy database → fresh dump (schema_version 1). 3. **Dry-run** `import_legacy_identity --report import_report.json`; confirm `has_problems: false` and zero phone/username/email collisions. 4. **Backup** the UserManager database (`mysqldump`/snapshot). 5. **Apply** `import_legacy_identity --apply --report import_report.json`. The import is idempotent — re-running only updates, never duplicates. 6. **Verify counts** from the report (users / organizations / memberships / profiles match the dry-run numbers exactly). 7. **Phone OTP** spot-check: sign in with a migrated number, receive + verify a code, receive access + refresh tokens. 8. **Switch traffic**: point Hamsoo at the IdP env (section 2), deploy `hamsoo_backend` + frontend, enable the SSO login path. 9. **Watch** `manage.py auth_divergence_report` during the window — every `blocking` row is a session that would break; target is 0 before full traffic. ## 4. Rollback Roll back **before** the legacy service is decommissioned — the legacy database is the restore point for the whole window. 1. Stop the SSO login path in Hamsoo (feature flag / previous frontend build). 2. Re-point Hamsoo auth config to the previous identity service (previous env values + previous deployment). 3. Restore UserManager DB from the step-4 backup if it was mutated (the import only added rows; restoring removes them cleanly). 4. Confirm: legacy login works, `auth_divergence_report` shows no new blocking rows for the rolled-back path. 5. Keep the dump + `import_report.json` artifacts for the next attempt. Rolling back does not lose data as long as the legacy database was frozen only for the window and un-frozen at step 1 of rollback. ## 5. After cutover (deferred, needs explicit approval) - Remove `bermooda_usermanager` code/tables and the shadow tables. - Rewrite git history for the RSA key committed in `54d5891` (rotate first). - Normalize legacy phone numbers (`normalize_phones` backfill). - Decommission the legacy service.