76 lines
2.7 KiB
PL/PgSQL
76 lines
2.7 KiB
PL/PgSQL
-- Migration: Secure User Roles from Self-Escalation
|
|
-- Purpose: Prevent users from updating their own roles to 'admin' using RLS / Triggers, while granting full access to service_role.
|
|
|
|
-- Create helper function to check admin role bypassing RLS (SECURITY DEFINER)
|
|
CREATE OR REPLACE FUNCTION public.is_admin(user_id UUID)
|
|
RETURNS BOOLEAN AS $$
|
|
BEGIN
|
|
RETURN EXISTS (
|
|
SELECT 1 FROM public.users
|
|
WHERE id = user_id AND role = 'admin'
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Grant privileges to PostgREST roles
|
|
GRANT ALL ON TABLE public.users TO service_role;
|
|
GRANT ALL ON TABLE public.users TO authenticated;
|
|
GRANT SELECT ON TABLE public.users TO anon;
|
|
GRANT ALL ON TABLE public.users TO postgres;
|
|
|
|
-- Service role policy
|
|
DROP POLICY IF EXISTS "service_role_all_users" ON public.users;
|
|
CREATE POLICY "service_role_all_users" ON public.users
|
|
FOR ALL
|
|
TO service_role
|
|
USING (true)
|
|
WITH CHECK (true);
|
|
|
|
-- Policy to allow users to view their own records
|
|
DROP POLICY IF EXISTS select_own_user ON public.users;
|
|
CREATE POLICY select_own_user ON public.users
|
|
FOR SELECT
|
|
TO authenticated
|
|
USING (auth.uid() = id);
|
|
|
|
-- Policy to allow admins to view all users
|
|
DROP POLICY IF EXISTS select_all_users_for_admin ON public.users;
|
|
CREATE POLICY select_all_users_for_admin ON public.users
|
|
FOR SELECT
|
|
TO authenticated
|
|
USING (public.is_admin(auth.uid()));
|
|
|
|
-- Policy to allow admins to update users
|
|
DROP POLICY IF EXISTS update_users_for_admin ON public.users;
|
|
CREATE POLICY update_users_for_admin ON public.users
|
|
FOR UPDATE
|
|
TO authenticated
|
|
USING (public.is_admin(auth.uid()))
|
|
WITH CHECK (public.is_admin(auth.uid()));
|
|
|
|
-- Trigger to prevent any role updates to 'admin' from unauthorized users
|
|
CREATE OR REPLACE FUNCTION check_user_role_escalation()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- Allow changes to the role column if executed by administrative DB roles or service_role JWT
|
|
IF (TG_OP = 'UPDATE' AND OLD.role IS DISTINCT FROM NEW.role) OR (TG_OP = 'INSERT') THEN
|
|
IF current_setting('request.jwt.claim.role', true) <> 'service_role'
|
|
AND current_setting('role', true) NOT IN ('service_role', 'supabase_admin', 'postgres') THEN
|
|
-- Partners cannot upgrade themselves or others to admin
|
|
IF NEW.role = 'admin' THEN
|
|
RAISE EXCEPTION 'Unberechtigtes Rollen-Upgrade verweigert.';
|
|
END IF;
|
|
END IF;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
DROP TRIGGER IF EXISTS enforce_role_protection ON public.users;
|
|
CREATE TRIGGER enforce_role_protection
|
|
BEFORE INSERT OR UPDATE ON public.users
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION check_user_role_escalation();
|