d3ro-voice/server/supabase/migrations/20260821000014_device_revocation_security.sql
2026-08-29 18:33:45 +09:00

153 lines
4.8 KiB
PL/PgSQL

BEGIN;
DROP POLICY IF EXISTS "devices_delete_own" ON public.devices;
CREATE OR REPLACE FUNCTION public.protect_device_security_fields()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = ''
AS $$
BEGIN
IF current_user NOT IN ('postgres', 'service_role', 'supabase_admin') AND (
NEW.user_id IS DISTINCT FROM OLD.user_id
OR NEW.installation_id IS DISTINCT FROM OLD.installation_id
OR NEW.platform IS DISTINCT FROM OLD.platform
OR NEW.revoked_at IS DISTINCT FROM OLD.revoked_at
OR NEW.push_token IS DISTINCT FROM OLD.push_token
) THEN
RAISE EXCEPTION 'device_security_fields_are_server_managed' USING ERRCODE = '42501';
END IF;
IF OLD.revoked_at IS NOT NULL AND NEW.revoked_at IS NULL THEN
RAISE EXCEPTION 'device_revocation_is_permanent' USING ERRCODE = '42501';
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS reject_direct_device_push_token_update ON public.devices;
DROP FUNCTION IF EXISTS public.reject_direct_device_push_token();
CREATE TRIGGER protect_device_security_fields_update
BEFORE UPDATE ON public.devices
FOR EACH ROW EXECUTE FUNCTION public.protect_device_security_fields();
CREATE OR REPLACE FUNCTION public.revoke_device(target_device_id uuid)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
current_user_id uuid := auth.uid();
target_device public.devices;
BEGIN
IF current_user_id IS NULL THEN
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
END IF;
IF target_device_id IS NULL THEN
RAISE EXCEPTION 'invalid_device' USING ERRCODE = '22023';
END IF;
PERFORM pg_advisory_xact_lock(hashtextextended(target_device_id::text, 73052));
SELECT * INTO target_device
FROM public.devices
WHERE id = target_device_id AND user_id = current_user_id
FOR UPDATE;
IF target_device.id IS NULL THEN
RAISE EXCEPTION 'device_not_found' USING ERRCODE = 'P0002';
END IF;
IF target_device.revoked_at IS NULL THEN
UPDATE public.devices
SET revoked_at = now(), push_token = NULL
WHERE id = target_device.id
RETURNING * INTO target_device;
END IF;
DELETE FROM public.push_tokens
WHERE device_id = target_device.id AND user_id = current_user_id;
RETURN to_jsonb(target_device);
END;
$$;
CREATE OR REPLACE FUNCTION public.unregister_current_device(
current_installation_id uuid
)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
current_user_id uuid := auth.uid();
target_device_id uuid;
BEGIN
IF current_user_id IS NULL THEN
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
END IF;
IF current_installation_id IS NULL THEN
RAISE EXCEPTION 'invalid_device' USING ERRCODE = '22023';
END IF;
SELECT id INTO target_device_id
FROM public.devices
WHERE user_id = current_user_id
AND installation_id = current_installation_id
AND revoked_at IS NULL
FOR UPDATE;
IF target_device_id IS NULL THEN
RETURN jsonb_build_object('removed', false);
END IF;
DELETE FROM public.push_tokens
WHERE device_id = target_device_id AND user_id = current_user_id;
DELETE FROM public.devices
WHERE id = target_device_id AND user_id = current_user_id AND revoked_at IS NULL;
RETURN jsonb_build_object('removed', true, 'device_id', target_device_id);
END;
$$;
CREATE OR REPLACE FUNCTION public.purge_revoked_device(
target_device_id uuid
)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
current_user_id uuid := auth.uid();
jwt_issued_at timestamptz;
removed_count integer;
BEGIN
IF current_user_id IS NULL THEN
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
END IF;
BEGIN
jwt_issued_at := to_timestamp((auth.jwt()->>'iat')::double precision);
EXCEPTION WHEN OTHERS THEN
RAISE EXCEPTION 'recent_authentication_required' USING ERRCODE = '42501';
END;
IF jwt_issued_at IS NULL OR jwt_issued_at < now() - interval '10 minutes' THEN
RAISE EXCEPTION 'recent_authentication_required' USING ERRCODE = '42501';
END IF;
DELETE FROM public.devices
WHERE id = target_device_id
AND user_id = current_user_id
AND revoked_at IS NOT NULL;
GET DIAGNOSTICS removed_count = ROW_COUNT;
IF removed_count = 0 THEN
RAISE EXCEPTION 'revoked_device_not_found' USING ERRCODE = 'P0002';
END IF;
RETURN jsonb_build_object('removed', true, 'device_id', target_device_id);
END;
$$;
REVOKE ALL ON FUNCTION public.revoke_device(uuid) FROM PUBLIC, anon;
REVOKE ALL ON FUNCTION public.unregister_current_device(uuid) FROM PUBLIC, anon;
REVOKE ALL ON FUNCTION public.purge_revoked_device(uuid) FROM PUBLIC, anon;
GRANT EXECUTE ON FUNCTION public.revoke_device(uuid) TO authenticated;
GRANT EXECUTE ON FUNCTION public.unregister_current_device(uuid) TO authenticated;
GRANT EXECUTE ON FUNCTION public.purge_revoked_device(uuid) TO authenticated;
COMMIT;