fix(migrations): close data-integrity gaps and gate version collisions #120

Closed
David wants to merge 1 commit from david/fix/bunyip-79-migration-data-integrity into main AGit
Owner

Guard the destructive/unconstrained DDL in bunyip-api/migrations, unify the two refresh-token revocation surfaces, retire the abandoned mokosh-server OIDC client row and dead seeds, and add a CI gate plus docs so future parallel branches cannot reintroduce version collisions (BUNYIP-79).

Migration content fixes (edited in place; renaming applied files would diverge _sqlx_migrations on live DBs, so file versions are untouched and a README documents the checksum caveat):

  • 20260319000025_encrypt_stripe_secrets: RAISE EXCEPTION before the DROP/ADD if secret_key/webhook_secret are non-NULL, so the destructive column swap fails closed instead of discarding live plaintext.
  • 20260313000021_add_feedback_attachments: bound data to 5 MiB (matching MAX_ATTACHMENT_SIZE) via CHECK octet_length(data) = size_bytes plus a size_bytes range CHECK.
  • 20260605000010_create_application_entitlements: CHECK source IN ('admin','stripe','backfill').
  • 20241230000014_create_email_change_requests: UNIQUE on token_hash, matching sibling single-use token tables.
  • 20260417000040_create_oidc_clients: CHECK refresh_idle_ttl_seconds BETWEEN 3600 AND 7776000.
  • 20260429000045_add_price_ids_to_tier_config: add lifetime_price_id to pair with lifetime_product_id.
  • 20260417000042_create_oidc_tokens: document the intentional no-FK on lifecycle_event_outbox.user_id (inline comment + COMMENT ON COLUMN) so user.deleted events outlive the user.
  • 20260602000050_seed_distribution_catalog: make idx_applications_is_hosted a plain index so catalog (WHERE NOT is_hosted) queries are covered, not just hub queries.
  • 20260417000041_create_oidc_sessions_and_codes: correct the misleading user_application_access backfill comment (zero rows on a fresh chain because clients seed later).
  • 20260502000048_register_mokosh_oidc_client: insert the abandoned client already-disabled (disabled_at set) so it leaves the oauth_clients_active index; superseded by ...0002/...0003 in 20260603000010.
  • 20241230000017_add_application_subdomain: remove the dead 'rus'/'rustylinks' subdomain backfills (their seed migration at position 8 was deleted).

Refresh-token revocation consolidation:

  • TokenRepository::revoke_all_user_refresh_tokens now revokes legacy refresh_tokens AND the OIDC refresh_token_families / refresh_tokens_v2 surfaces in one transaction, so a security event invalidates every outstanding token.
  • AuthService::change_password now calls it (it previously revoked nothing), matching the password-reset path.

Collision gate and docs:

  • scripts/check-migration-versions.sh + a check-migrations justfile recipe + a CI step enforce unique, strictly increasing 14-digit migration versions without renumbering applied history.
  • bunyip-api/migrations/README.md documents the versioning policy, the intentional position-8 sequence gap, and the pre-deploy _sqlx_migrations checksum verification.

Verified: all 55 migrations apply cleanly against postgres:16-alpine; the stripe guard raises on non-NULL secrets; every new CHECK/UNIQUE/index/comment is present; fmt + clippy + workspace lib tests are green in the pinned rust-builder image.

#BUNYIP-79

Guard the destructive/unconstrained DDL in bunyip-api/migrations, unify the two refresh-token revocation surfaces, retire the abandoned mokosh-server OIDC client row and dead seeds, and add a CI gate plus docs so future parallel branches cannot reintroduce version collisions (BUNYIP-79). Migration content fixes (edited in place; renaming applied files would diverge _sqlx_migrations on live DBs, so file versions are untouched and a README documents the checksum caveat): - 20260319000025_encrypt_stripe_secrets: RAISE EXCEPTION before the DROP/ADD if secret_key/webhook_secret are non-NULL, so the destructive column swap fails closed instead of discarding live plaintext. - 20260313000021_add_feedback_attachments: bound data to 5 MiB (matching MAX_ATTACHMENT_SIZE) via CHECK octet_length(data) = size_bytes plus a size_bytes range CHECK. - 20260605000010_create_application_entitlements: CHECK source IN ('admin','stripe','backfill'). - 20241230000014_create_email_change_requests: UNIQUE on token_hash, matching sibling single-use token tables. - 20260417000040_create_oidc_clients: CHECK refresh_idle_ttl_seconds BETWEEN 3600 AND 7776000. - 20260429000045_add_price_ids_to_tier_config: add lifetime_price_id to pair with lifetime_product_id. - 20260417000042_create_oidc_tokens: document the intentional no-FK on lifecycle_event_outbox.user_id (inline comment + COMMENT ON COLUMN) so user.deleted events outlive the user. - 20260602000050_seed_distribution_catalog: make idx_applications_is_hosted a plain index so catalog (WHERE NOT is_hosted) queries are covered, not just hub queries. - 20260417000041_create_oidc_sessions_and_codes: correct the misleading user_application_access backfill comment (zero rows on a fresh chain because clients seed later). - 20260502000048_register_mokosh_oidc_client: insert the abandoned client already-disabled (disabled_at set) so it leaves the oauth_clients_active index; superseded by ...0002/...0003 in 20260603000010. - 20241230000017_add_application_subdomain: remove the dead 'rus'/'rustylinks' subdomain backfills (their seed migration at position 8 was deleted). Refresh-token revocation consolidation: - TokenRepository::revoke_all_user_refresh_tokens now revokes legacy refresh_tokens AND the OIDC refresh_token_families / refresh_tokens_v2 surfaces in one transaction, so a security event invalidates every outstanding token. - AuthService::change_password now calls it (it previously revoked nothing), matching the password-reset path. Collision gate and docs: - scripts/check-migration-versions.sh + a check-migrations justfile recipe + a CI step enforce unique, strictly increasing 14-digit migration versions without renumbering applied history. - bunyip-api/migrations/README.md documents the versioning policy, the intentional position-8 sequence gap, and the pre-deploy _sqlx_migrations checksum verification. Verified: all 55 migrations apply cleanly against postgres:16-alpine; the stripe guard raises on non-NULL secrets; every new CHECK/UNIQUE/index/comment is present; fmt + clippy + workspace lib tests are green in the pinned rust-builder image. #BUNYIP-79
fix(migrations): close data-integrity gaps and gate version collisions
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt / clippy / build / test (pull_request) Successful in 1m0s
e08b2e6b19
Guard the destructive/unconstrained DDL in bunyip-api/migrations, unify the two refresh-token revocation surfaces, retire the abandoned mokosh-server OIDC client row and dead seeds, and add a CI gate plus docs so future parallel branches cannot reintroduce version collisions (BUNYIP-79).

Migration content fixes (edited in place; renaming applied files would diverge _sqlx_migrations on live DBs, so file versions are untouched and a README documents the checksum caveat):
- 20260319000025_encrypt_stripe_secrets: RAISE EXCEPTION before the DROP/ADD if secret_key/webhook_secret are non-NULL, so the destructive column swap fails closed instead of discarding live plaintext.
- 20260313000021_add_feedback_attachments: bound data to 5 MiB (matching MAX_ATTACHMENT_SIZE) via CHECK octet_length(data) = size_bytes plus a size_bytes range CHECK.
- 20260605000010_create_application_entitlements: CHECK source IN ('admin','stripe','backfill').
- 20241230000014_create_email_change_requests: UNIQUE on token_hash, matching sibling single-use token tables.
- 20260417000040_create_oidc_clients: CHECK refresh_idle_ttl_seconds BETWEEN 3600 AND 7776000.
- 20260429000045_add_price_ids_to_tier_config: add lifetime_price_id to pair with lifetime_product_id.
- 20260417000042_create_oidc_tokens: document the intentional no-FK on lifecycle_event_outbox.user_id (inline comment + COMMENT ON COLUMN) so user.deleted events outlive the user.
- 20260602000050_seed_distribution_catalog: make idx_applications_is_hosted a plain index so catalog (WHERE NOT is_hosted) queries are covered, not just hub queries.
- 20260417000041_create_oidc_sessions_and_codes: correct the misleading user_application_access backfill comment (zero rows on a fresh chain because clients seed later).
- 20260502000048_register_mokosh_oidc_client: insert the abandoned client already-disabled (disabled_at set) so it leaves the oauth_clients_active index; superseded by ...0002/...0003 in 20260603000010.
- 20241230000017_add_application_subdomain: remove the dead 'rus'/'rustylinks' subdomain backfills (their seed migration at position 8 was deleted).

Refresh-token revocation consolidation:
- TokenRepository::revoke_all_user_refresh_tokens now revokes legacy refresh_tokens AND the OIDC refresh_token_families / refresh_tokens_v2 surfaces in one transaction, so a security event invalidates every outstanding token.
- AuthService::change_password now calls it (it previously revoked nothing), matching the password-reset path.

Collision gate and docs:
- scripts/check-migration-versions.sh + a check-migrations justfile recipe + a CI step enforce unique, strictly increasing 14-digit migration versions without renumbering applied history.
- bunyip-api/migrations/README.md documents the versioning policy, the intentional position-8 sequence gap, and the pre-deploy _sqlx_migrations checksum verification.

Verified: all 55 migrations apply cleanly against postgres:16-alpine; the stripe guard raises on non-NULL secrets; every new CHECK/UNIQUE/index/comment is present; fmt + clippy + workspace lib tests are green in the pinned rust-builder image.

#BUNYIP-79
David closed this pull request 2026-06-12 11:59:50 +02:00
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt / clippy / build / test (pull_request) Successful in 1m0s

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
psa-systems/bunyip!120
No description provided.