65bd675f43
* fix(auth): unlink social identities bound to the old address when the login email changes GoTrue keys OAuth identities on the provider subject, so after a secure email change from A to B the Google identity auto-linked for A stayed on the account and "Logga in med Google" from the A mailbox still opened the company (prod 2026-09-03, willemduplessis999 -> levandefisken kept both Google logins). A change is a change: only identities bound to the address the user switched from go; the email identity, password, BankID and social identities on other addresses stay, and Google with the new address re-links itself on the first sign-in. Migration 20260903110000 adds a BEFORE UPDATE OF email trigger on auth.users (next to sync_profile_email) that deletes those identities and recomputes app_metadata.providers. A trigger covers every completion path: hook link, stock link, phone click without a session, admin-side change. pg-real test covers removal, keep-others, case-insensitive match, email identity untouched, no-op on unchanged email, and other users on the same address. Applied to staging under the same version. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LMFybWJqw8vScQiEDwKXGi * fix(auth): make the old-identity unlink trigger safe without GoTrue and keep Google-only accounts reachable Skeptic findings on 5889aec7e: - The pg-real container has no auth.identities (GoTrue creates it and does not run in CI), so the trigger failed every auth.users email update there, including the existing profile-email-sync suite. Guard the function with to_regclass and bootstrap a GoTrue-shaped auth.identities in tests/pg/bootstrap.sql so the trigger's own tests actually run. - A Google-only account (no password, no email identity) ended with zero identities after the change, and whether Google with the new address re-links then depends on GoTrue internals. When the trigger removes the last social identity and no email identity exists, it now creates the email identity for the new address, the row GoTrue links Google through and password recovery resolves. pg-real tests cover both cases. - Self-hosting note: keep secure email change enabled, since a change now also removes the old address's social logins. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LMFybWJqw8vScQiEDwKXGi * fix(auth): verified flag, audit trail and redaction for the old-identity unlink trigger Second review round on PR #2208: - Superagent P1: the synthesized email identity claimed email_verified for every email update, admin-side included. It is now verified only when the pending address became the address in the same write (the signature of GoTrue's ConfirmEmailChange); anything else gets an unverified identity, as GoTrue itself would create it. - Compliance swarm A.8.15: removing a login method left no trail. The trigger now writes an identity_unlink entry to auth.audit_log_entries with the removed providers, old and new address and whether the change was confirmed, next to GoTrue's own user_modified entry. - Compliance swarm A.5.34: the migration comment named real test accounts; redacted. - pg-real: the cross-user test still expected the old-address user to end with zero identities; it now expects the email identity the previous round introduced. New tests cover the unverified admin path and the audit entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LMFybWJqw8vScQiEDwKXGi * test(pg): give the CI auth audit table the ip_address column GoTrue adds pg-real runs against the bare Postgres image, whose auth.audit_log_entries predates GoTrue's ip_address column (NOT NULL DEFAULT '' on every hosted project). unlink_old_address_identities writes that column, so all seven trigger tests failed with 42703 in CI while the same migration ran clean on staging. Mirror the real shape in the bootstrap instead of changing a migration that is already applied under this version. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
75 lines
3.0 KiB
PL/PgSQL
75 lines
3.0 KiB
PL/PgSQL
-- pg-real CI bootstrap.
|
|
--
|
|
-- The Supabase Postgres image ships a partial `storage` schema; the remaining
|
|
-- columns and functions are provisioned at runtime by the storage-api
|
|
-- service, which we do not run in CI. This bootstrap aligns the schema with
|
|
-- what our migrations expect so the replay loop succeeds. It is idempotent
|
|
-- and safe to run against a freshly-initialised container.
|
|
|
|
CREATE SCHEMA IF NOT EXISTS storage;
|
|
|
|
CREATE TABLE IF NOT EXISTS storage.buckets (
|
|
id text PRIMARY KEY,
|
|
name text NOT NULL,
|
|
owner uuid,
|
|
created_at timestamptz DEFAULT now(),
|
|
updated_at timestamptz DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE storage.buckets
|
|
ADD COLUMN IF NOT EXISTS public boolean DEFAULT false,
|
|
ADD COLUMN IF NOT EXISTS file_size_limit bigint,
|
|
ADD COLUMN IF NOT EXISTS allowed_mime_types text[];
|
|
|
|
CREATE TABLE IF NOT EXISTS storage.objects (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
bucket_id text REFERENCES storage.buckets(id) ON DELETE CASCADE,
|
|
name text,
|
|
owner uuid,
|
|
created_at timestamptz DEFAULT now(),
|
|
updated_at timestamptz DEFAULT now(),
|
|
last_accessed_at timestamptz DEFAULT now(),
|
|
metadata jsonb,
|
|
version text,
|
|
owner_id text
|
|
);
|
|
|
|
ALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- storage.foldername(): splits a slash-delimited object name into segments.
|
|
-- Migrations use `(storage.foldername(name))[n]` to derive tenant scoping
|
|
-- from the object path.
|
|
CREATE OR REPLACE FUNCTION storage.foldername(name text)
|
|
RETURNS text[]
|
|
LANGUAGE sql
|
|
IMMUTABLE
|
|
AS $$
|
|
SELECT string_to_array(name, '/');
|
|
$$;
|
|
|
|
-- auth.identities is created by GoTrue at startup, not by the Postgres
|
|
-- image, and GoTrue does not run in CI. Triggers on auth.users that touch
|
|
-- identities (20260903110000 unlink_old_address_identities) need the table
|
|
-- to exist so an email change in a pg-real test does not fail with 42P01.
|
|
-- Shape mirrors GoTrue's migration (same PK, unique key, generated email).
|
|
CREATE TABLE IF NOT EXISTS auth.identities (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
provider_id text NOT NULL,
|
|
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
identity_data jsonb NOT NULL,
|
|
provider text NOT NULL,
|
|
last_sign_in_at timestamptz,
|
|
created_at timestamptz,
|
|
updated_at timestamptz,
|
|
email text GENERATED ALWAYS AS (lower(identity_data ->> 'email')) STORED,
|
|
CONSTRAINT identities_provider_id_provider_unique UNIQUE (provider_id, provider)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS identities_user_id_idx ON auth.identities (user_id);
|
|
|
|
-- auth.audit_log_entries ships in the Postgres image without the ip_address
|
|
-- column GoTrue adds on first boot (NOT NULL DEFAULT '' on every hosted
|
|
-- project). unlink_old_address_identities writes GoTrue's audit table with
|
|
-- that column, so the CI double must carry it too.
|
|
ALTER TABLE IF EXISTS auth.audit_log_entries
|
|
ADD COLUMN IF NOT EXISTS ip_address varchar(64) NOT NULL DEFAULT '';
|