Files
gongxue-base/docs/refactor/clean-room-migration-audit-20260712.md
2026-07-12 19:26:57 +08:00

128 lines
6.8 KiB
Markdown

# Clean-room migration audit - 2026-07-12
## Audit scope
- Isolated project ID: `tiku-clean-final-20260712-v2`
- Database image: official Supabase PostgreSQL `15.8`
- Isolated database endpoint used during verification: `127.0.0.1:55522`
- Migration role: standard non-superuser `postgres`
- Privileged bootstrap/test role: `supabase_admin`, used for the runtime-role bootstrap and controlled role-boundary probes
- The existing databases on ports `5432`, `54322`, and `55432` were explicitly excluded and were not modified.
The clean-room startup log reported that no `supabase/seed.sql` matched. The seed was therefore not executed. At the end of migration verification, `public.tenants`, `app_private.environment_safety`, and `auth.users` all contained zero rows.
## Migration result
All 78 migrations were applied successfully in filename order to the empty database. `supabase migration list --local` subsequently showed every local version paired with the applied version through `202607120019`.
Migration history verification returned:
- Rows: `78`
- Distinct versions: `78`
- Duplicate versions: `0`
- Maximum version: `202607120019`
The privileged `scripts/deploy/sql/bootstrap-backend-runtime-roles.sql` bootstrap ran before the normal migrations. Migrations `202607120013_backend_runtime_roles.sql`, `202607120018_auth_user_reference_boundary.sql`, and `202607120019_production_migration_history_boundary.sql` were later replayed directly by the non-superuser `postgres` role. All three replays succeeded. A normalized fingerprint covering role attributes, memberships, schema/table/sequence/function ACLs, default ACLs, and security-definer attributes remained `1691|7aee08a0b05a30fc49fd278748d4c4e3` before and after replay.
## Extensions and function ACLs
The required extensions were installed in the `extensions` schema:
- `citext`
- `ltree`
- `pg_trgm`
- `pgcrypto`
No client-facing role can execute a function exposed through `public`. The final extension-function matrix was:
| Role | `citext` | `ltree` | `pg_trgm` | `pgcrypto` |
| --- | ---: | ---: | ---: | ---: |
| `anon` | 0/45 | 0/78 | 0/31 | 0/36 |
| `authenticated` | 0/45 | 0/78 | 0/31 | 0/36 |
| `tiku_api` | 45/45 | 78/78 | 31/31 | 0/36 |
| `tiku_worker` | 45/45 | 78/78 | 31/31 | 0/36 |
`citext` also owns two aggregates; when all `pg_proc` extension members are counted, API and worker have all 47 required `citext` members while client roles still have zero.
## Runtime roles and Auth boundary
Both backend roles were verified as:
- `LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION BYPASSRLS`
- No parent-role memberships
- `search_path=pg_catalog, public, extensions`
Runtime identity checks confirmed that both roles can execute `citext` comparisons, `ltree` operators, and `extensions.similarity()`.
The API role could execute exactly five reviewed `app` functions:
- `app.auth_user_exists(uuid)`
- `app.production_migration_history(text)`
- `app.uuid_array_from_jsonb(jsonb)`
- `app.public_question_bank_grant_allows(uuid[], uuid[], uuid, uuid[])`
- `app.public_question_bank_subscription_allows(jsonb, jsonb, uuid, uuid, uuid[])`
Direct `tiku_api` access to `supabase_migrations.schema_migrations` was denied. The API-only `app.production_migration_history('202607120019')` boundary returned `latest_version=202607120019`, `applied_count=78`, `distinct_version_count=78`, and `expected_version_applied=true`. Worker and anonymous execution were denied.
The worker has no `app` schema usage and can execute no `app` functions. Both API-only boundary functions are stable `SECURITY DEFINER` functions with an empty search path. `app.auth_user_exists(uuid)` returned `false` for an absent UUID and `true` for a temporary clean-room Auth user; that user was deleted immediately after the probe. Worker and anonymous execution were denied. Direct reads of `auth.users` were denied for both API and worker.
Supabase internal compatibility checks also passed:
- `supabase_auth_admin` could query `auth.users`.
- `supabase_storage_admin` could query `storage.objects`.
- `authenticator` could still `SET ROLE anon` and `SET ROLE authenticated`.
## RLS, constraints, and indexes
All 140 `public` tables had RLS enabled. Across `public` and `app_private`, all 134 tables containing a `tenant_id` column had RLS enabled.
The only checked internal table without RLS was `app_private.environment_safety`. It is not tenant data, is intentionally fail-closed, and explicitly revokes access from `public`, `anon`, and `authenticated`.
The project tenant-foreign-key audit matched all `189/189` expected relations and its expected SHA-256 fingerprint, found all three reviewed exceptions, found no unvalidated relations, and reported `0` data violations.
All 22 explicitly checked critical foreign keys, unique constraints, and check constraints introduced by migrations `202607120010` through `202607120017` existed with `convalidated=true`. All 16 explicitly checked critical indexes existed with `indisvalid=true` and `indisready=true`, including tenant-safe question/version relations, answer semantics, tenant student keyset/search indexes, SMS reservation limits, audit-log capacity indexes, and import-job lease indexes.
Supabase lint completed successfully:
```text
Linting schema: public
Linting schema: app
Linting schema: app_private
No schema errors found
```
The command used was:
```bash
npx --no-install supabase db lint \
--local \
--workdir /tmp/tiku-clean-final-20260712-v2 \
--schema public,app,app_private \
--level error \
--fail-on error
```
## Residual foreign-key index risk
The application schemas (`public` and `app_private`) contain 465 foreign keys. The structural audit found:
- 146 with an unconditional complete left-prefix index
- 6 covered only by a partial left-prefix index
- 313 without a complete left-prefix index
The newly hardened `202607120015` and `202607120017` hot paths are substantially covered. The remaining count is a capacity and operations backlog, not a migration correctness or tenant-isolation failure.
Adding 313 indexes blindly is not recommended. Every index increases storage, write amplification, vacuum work, cache pressure, migration time, and lock risk. Some foreign keys are low-volume, rarely joined, never cascaded in normal operations, or already served by a more useful query-specific index. Production indexing should therefore be prioritized from representative capacity tests, cascade/delete behavior, slow-query evidence, and `pg_stat_statements`, then introduced in controlled batches.
## Cleanup
After all checks passed, the isolated Supabase project was stopped with `--no-backup`. The following were verified absent:
- `/tmp/tiku-clean-final-20260712-v2`
- Clean-room containers
- Clean-room Docker volumes
- Clean-room Docker networks
The pre-existing databases on ports `5432`, `54322`, and `55432` remained running after cleanup.