forked from wangziqi/gongxue-base
128 lines
6.8 KiB
Markdown
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.
|