199 lines
6.6 KiB
PL/PgSQL
199 lines
6.6 KiB
PL/PgSQL
-- Durable, transactionally-enqueued API-key auth cache invalidation.
|
|||
|
|
-- cache_key is always SHA-256 hex; plaintext credentials never leave api_keys.
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS auth_cache_invalidation_outbox (
|
||
|
|
id BIGSERIAL PRIMARY KEY,
|
||
|
|
cache_key CHAR(64) NOT NULL CHECK (cache_key ~ '^[0-9a-f]{64}$'),
|
||
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
|
available_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
|
delivery_stage SMALLINT NOT NULL DEFAULT 0 CHECK (delivery_stage IN (0, 1)),
|
||
|
|
attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
||
|
|
last_error TEXT,
|
||
|
|
claimed_at TIMESTAMPTZ,
|
||
|
|
claimed_by TEXT
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_auth_cache_invalidation_outbox_available
|
||
|
|
ON auth_cache_invalidation_outbox (available_at, id)
|
||
|
|
WHERE claimed_at IS NULL;
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_auth_cache_invalidation_outbox_lease
|
||
|
|
ON auth_cache_invalidation_outbox (claimed_at)
|
||
|
|
WHERE claimed_at IS NOT NULL;
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_auth_cache_invalidation_outbox_cache_key
|
||
|
|
ON auth_cache_invalidation_outbox (cache_key);
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_auth_cache_invalidation_outbox_created_at
|
||
|
|
ON auth_cache_invalidation_outbox (created_at);
|
||
|
|
|
||
|
|
CREATE OR REPLACE FUNCTION enqueue_auth_cache_invalidation(raw_key TEXT)
|
||
|
|
RETURNS VOID
|
||
|
|
LANGUAGE plpgsql
|
||
|
|
AS $$
|
||
|
|
BEGIN
|
||
|
|
IF raw_key IS NULL OR raw_key = '' THEN
|
||
|
|
RETURN;
|
||
|
|
END IF;
|
||
|
|
INSERT INTO auth_cache_invalidation_outbox (cache_key)
|
||
|
|
VALUES (encode(sha256(convert_to(raw_key, 'UTF8')), 'hex'));
|
||
|
|
END;
|
||
|
|
$$;
|
||
|
|
|
||
|
|
CREATE OR REPLACE FUNCTION enqueue_api_key_auth_cache_invalidation()
|
||
|
|
RETURNS TRIGGER
|
||
|
|
LANGUAGE plpgsql
|
||
|
|
AS $$
|
||
|
|
BEGIN
|
||
|
|
IF TG_OP = 'DELETE' THEN
|
||
|
|
PERFORM enqueue_auth_cache_invalidation(OLD.key);
|
||
|
|
RETURN OLD;
|
||
|
|
END IF;
|
||
|
|
|
||
|
|
IF OLD.key IS DISTINCT FROM NEW.key
|
||
|
|
OR OLD.status IS DISTINCT FROM NEW.status
|
||
|
|
OR OLD.deleted_at IS DISTINCT FROM NEW.deleted_at
|
||
|
|
OR OLD.user_id IS DISTINCT FROM NEW.user_id
|
||
|
|
OR OLD.group_id IS DISTINCT FROM NEW.group_id
|
||
|
|
OR OLD.ip_whitelist IS DISTINCT FROM NEW.ip_whitelist
|
||
|
|
OR OLD.ip_blacklist IS DISTINCT FROM NEW.ip_blacklist
|
||
|
|
OR OLD.expires_at IS DISTINCT FROM NEW.expires_at THEN
|
||
|
|
PERFORM enqueue_auth_cache_invalidation(OLD.key);
|
||
|
|
IF NEW.deleted_at IS NULL AND NEW.key IS DISTINCT FROM OLD.key THEN
|
||
|
|
PERFORM enqueue_auth_cache_invalidation(NEW.key);
|
||
|
|
END IF;
|
||
|
|
END IF;
|
||
|
|
RETURN NEW;
|
||
|
|
END;
|
||
|
|
$$;
|
||
|
|
|
||
|
|
DROP TRIGGER IF EXISTS trg_api_keys_auth_cache_invalidation ON api_keys;
|
||
|
|
CREATE TRIGGER trg_api_keys_auth_cache_invalidation
|
||
|
|
AFTER UPDATE OR DELETE ON api_keys
|
||
|
|
FOR EACH ROW EXECUTE FUNCTION enqueue_api_key_auth_cache_invalidation();
|
||
|
|
|
||
|
|
CREATE OR REPLACE FUNCTION enqueue_user_auth_cache_invalidation()
|
||
|
|
RETURNS TRIGGER
|
||
|
|
LANGUAGE plpgsql
|
||
|
|
AS $$
|
||
|
|
DECLARE
|
||
|
|
target_user_id BIGINT;
|
||
|
|
BEGIN
|
||
|
|
target_user_id := OLD.id;
|
||
|
|
IF TG_OP = 'UPDATE'
|
||
|
|
AND OLD.status IS NOT DISTINCT FROM NEW.status
|
||
|
|
AND OLD.role IS NOT DISTINCT FROM NEW.role
|
||
|
|
AND OLD.deleted_at IS NOT DISTINCT FROM NEW.deleted_at THEN
|
||
|
|
RETURN NEW;
|
||
|
|
END IF;
|
||
|
|
|
||
|
|
INSERT INTO auth_cache_invalidation_outbox (cache_key)
|
||
|
|
SELECT encode(sha256(convert_to(k.key, 'UTF8')), 'hex')
|
||
|
|
FROM api_keys AS k
|
||
|
|
WHERE k.user_id = target_user_id
|
||
|
|
AND k.deleted_at IS NULL
|
||
|
|
AND k.key <> '';
|
||
|
|
IF TG_OP = 'DELETE' THEN
|
||
|
|
RETURN OLD;
|
||
|
|
END IF;
|
||
|
|
RETURN NEW;
|
||
|
|
END;
|
||
|
|
$$;
|
||
|
|
|
||
|
|
DROP TRIGGER IF EXISTS trg_users_auth_cache_invalidation ON users;
|
||
|
|
CREATE TRIGGER trg_users_auth_cache_invalidation
|
||
|
|
AFTER UPDATE OR DELETE ON users
|
||
|
|
FOR EACH ROW EXECUTE FUNCTION enqueue_user_auth_cache_invalidation();
|
||
|
|
|
||
|
|
CREATE OR REPLACE FUNCTION enqueue_group_auth_cache_invalidation()
|
||
|
|
RETURNS TRIGGER
|
||
|
|
LANGUAGE plpgsql
|
||
|
|
AS $$
|
||
|
|
DECLARE
|
||
|
|
target_group_id BIGINT;
|
||
|
|
BEGIN
|
||
|
|
target_group_id := OLD.id;
|
||
|
|
IF TG_OP = 'UPDATE'
|
||
|
|
AND OLD.status IS NOT DISTINCT FROM NEW.status
|
||
|
|
AND OLD.is_exclusive IS NOT DISTINCT FROM NEW.is_exclusive
|
||
|
|
AND OLD.deleted_at IS NOT DISTINCT FROM NEW.deleted_at THEN
|
||
|
|
RETURN NEW;
|
||
|
|
END IF;
|
||
|
|
|
||
|
|
INSERT INTO auth_cache_invalidation_outbox (cache_key)
|
||
|
|
SELECT encode(sha256(convert_to(k.key, 'UTF8')), 'hex')
|
||
|
|
FROM api_keys AS k
|
||
|
|
WHERE k.group_id = target_group_id
|
||
|
|
AND k.deleted_at IS NULL
|
||
|
|
AND k.key <> '';
|
||
|
|
IF TG_OP = 'DELETE' THEN
|
||
|
|
RETURN OLD;
|
||
|
|
END IF;
|
||
|
|
RETURN NEW;
|
||
|
|
END;
|
||
|
|
$$;
|
||
|
|
|
||
|
|
DROP TRIGGER IF EXISTS trg_groups_auth_cache_invalidation ON groups;
|
||
|
|
CREATE TRIGGER trg_groups_auth_cache_invalidation
|
||
|
|
AFTER UPDATE OR DELETE ON groups
|
||
|
|
FOR EACH ROW EXECUTE FUNCTION enqueue_group_auth_cache_invalidation();
|
||
|
|
|
||
|
|
CREATE OR REPLACE FUNCTION enqueue_allowed_group_auth_cache_invalidation()
|
||
|
|
RETURNS TRIGGER
|
||
|
|
LANGUAGE plpgsql
|
||
|
|
AS $$
|
||
|
|
DECLARE
|
||
|
|
target_user_id BIGINT;
|
||
|
|
target_group_id BIGINT;
|
||
|
|
BEGIN
|
||
|
|
IF TG_OP = 'UPDATE'
|
||
|
|
AND (OLD.user_id IS DISTINCT FROM NEW.user_id
|
||
|
|
OR OLD.group_id IS DISTINCT FROM NEW.group_id) THEN
|
||
|
|
IF EXISTS (
|
||
|
|
SELECT 1 FROM groups g
|
||
|
|
WHERE g.id = OLD.group_id AND g.is_exclusive = TRUE
|
||
|
|
) THEN
|
||
|
|
INSERT INTO auth_cache_invalidation_outbox (cache_key)
|
||
|
|
SELECT encode(sha256(convert_to(k.key, 'UTF8')), 'hex')
|
||
|
|
FROM api_keys AS k
|
||
|
|
WHERE k.user_id = OLD.user_id
|
||
|
|
AND k.group_id = OLD.group_id
|
||
|
|
AND k.deleted_at IS NULL
|
||
|
|
AND k.key <> '';
|
||
|
|
END IF;
|
||
|
|
target_user_id := NEW.user_id;
|
||
|
|
target_group_id := NEW.group_id;
|
||
|
|
ELSIF TG_OP = 'UPDATE' THEN
|
||
|
|
RETURN NEW;
|
||
|
|
ELSIF TG_OP = 'INSERT' THEN
|
||
|
|
target_user_id := NEW.user_id;
|
||
|
|
target_group_id := NEW.group_id;
|
||
|
|
ELSE
|
||
|
|
target_user_id := OLD.user_id;
|
||
|
|
target_group_id := OLD.group_id;
|
||
|
|
END IF;
|
||
|
|
|
||
|
|
IF EXISTS (
|
||
|
|
SELECT 1 FROM groups g
|
||
|
|
WHERE g.id = target_group_id AND g.is_exclusive = TRUE
|
||
|
|
) THEN
|
||
|
|
INSERT INTO auth_cache_invalidation_outbox (cache_key)
|
||
|
|
SELECT encode(sha256(convert_to(k.key, 'UTF8')), 'hex')
|
||
|
|
FROM api_keys AS k
|
||
|
|
WHERE k.user_id = target_user_id
|
||
|
|
AND k.group_id = target_group_id
|
||
|
|
AND k.deleted_at IS NULL
|
||
|
|
AND k.key <> '';
|
||
|
|
END IF;
|
||
|
|
IF TG_OP = 'DELETE' THEN
|
||
|
|
RETURN OLD;
|
||
|
|
END IF;
|
||
|
|
RETURN NEW;
|
||
|
|
END;
|
||
|
|
$$;
|
||
|
|
|
||
|
|
DROP TRIGGER IF EXISTS trg_user_allowed_groups_auth_cache_invalidation ON user_allowed_groups;
|
||
|
|
CREATE TRIGGER trg_user_allowed_groups_auth_cache_invalidation
|
||
|
|
AFTER INSERT OR UPDATE OR DELETE ON user_allowed_groups
|
||
|
|
FOR EACH ROW EXECUTE FUNCTION enqueue_allowed_group_auth_cache_invalidation();
|
||
|
|
|
||
|
|
COMMENT ON TABLE auth_cache_invalidation_outbox IS
|
||
|
|
'Durable cross-instance auth cache invalidations; cache_key is SHA-256 hex, never plaintext API key';
|