Using Materialized Views for RLS in Supabase: Best Practices and UI Limitations #17790
-
BodyHello everyone, I'm working on a project where I need to calculate the final cost ( Context:
Why Materialized View?I opted for a materialized view for two main reasons:
Questions:
Any insights or recommendations would be greatly appreciated. Thank you! |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 2 replies
-
You can enforce RLS on regular views now with security_invoker. https://www.postgresql.org/docs/current/sql-createview.html |
Beta Was this translation helpful? Give feedback.
-
I'm fairly certain PostgreSQL as of v15 does NOT support RLS on materialized views, in contrast with regular views which do using security_invoker = true. |
Beta Was this translation helpful? Give feedback.
-
As @stamler said, the current Postgres version doesn't support RLS on materialized views and will not be available anytime soon (https://postgrespro.com/list/thread-id/2400463). I proposed a workaround design pattern for managing authorization when we use materialized views or any other object (e.g. FWD) in Postgres that doesn't support RLS by leveraging functions and views.
CREATE SCHEMA analytics;
CREATE MATERIALIZED VIEW analytics.analytics_mv AS
SELECT
user_id,
COUNT(*) AS total_records,
SUM(amount) AS total_amount
FROM
public.sensitive_table
GROUP BY
user_id;
REVOKE ALL ON analytics.analytics_mv FROM PUBLIC, ANON, AUTHENTICATED;
GRANT SELECT ON analytics.analytics_mv TO postgres;
CREATE OR REPLACE FUNCTION analytics.get_user_analytics_mv()
RETURNS SETOF analytics_mv AS $$
BEGIN
RETURN QUERY SELECT * FROM analytics.analytics_mv
-- RLS-like behavior
WHERE user_id = auth.uid() AND auth.role() = 'authenticated';
END;
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '';
GRANT EXECUTE ON FUNCTION analytics.get_user_analytics_mv() TO authenticated;
CREATE VIEW public.analytics WITH (security_invoker=on) AS
SELECT * FROM analytics.get_user_analytics_mv(); Complete POC SQL Script -- Create 'sensitive_table' in the 'public' schema
CREATE TABLE public.sensitive_table (
id SERIAL PRIMARY KEY,
user_id uuid NOT NULL,
amount NUMERIC NOT NULL,
sale_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
constraint sensitive_table_user_id_fkey foreign key (user_id) references auth.users (id)
);
-- Insert sample data into 'sensitive_table'
INSERT INTO public.sensitive_table (user_id, amount) VALUES
('1c314945-6f17-4037-8130-6a23d3f3983e', 100.00),
('1c314945-6f17-4037-8130-6a23d3f3983e', 150.50),
('1c314945-6f17-4037-8130-6a23d3f3983e', 200.00),
('c4b6d34f-a414-477f-b6db-40d232a3c8e0', 300.75),
('c4b6d34f-a414-477f-b6db-40d232a3c8e0', 50.25);
-- Create 'analytics' schema if it doesn't exist
CREATE SCHEMA IF NOT EXISTS analytics;
-- Drop existing materialized view if it exists for idempotency
DROP MATERIALIZED VIEW IF EXISTS analytics.analytics_mv CASCADE;
-- Create the materialized view 'analytics_mv'
CREATE MATERIALIZED VIEW analytics.analytics_mv AS
SELECT
user_id,
COUNT(*) AS total_records,
SUM(amount) AS total_amount
FROM
public.sensitive_table
GROUP BY
user_id;
REVOKE ALL ON analytics.analytics_mv FROM PUBLIC, anon, authenticated;
CREATE OR REPLACE FUNCTION analytics.get_user_analytics_mv()
RETURNS SETOF analytics.analytics_mv AS $$
BEGIN
RETURN QUERY
SELECT *
FROM analytics.analytics_mv
WHERE analytics.analytics_mv.user_id = auth.uid() AND auth.role() = 'authenticated';
END;
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '';
ALTER FUNCTION analytics.get_user_analytics_mv() OWNER TO postgres;
GRANT EXECUTE ON FUNCTION analytics.get_user_analytics_mv() TO authenticated;
CREATE VIEW public.analytics AS
SELECT * FROM analytics.get_user_analytics_mv();
-- Revoke all access to the view from PUBLIC
REVOKE ALL ON public.analytics FROM PUBLIC;
-- Grant SELECT access on the view to the 'authenticated' role
GRANT SELECT ON public.analytics TO authenticated;
DROP TABLE IF EXISTS public.sensitive_table CASCADE;
DROP FUNCTION IF EXISTS analytics.get_user_analytics_mv() CASCADE; -- Impersonate postgres
SELECT * FROM analytics.analytics_mv
-- Impersonate user c4b6d34f-a414-477f-b6db-40d232a3c8e0
SELECT * FROM public.analytics
-- Impersonate user 1c314945-6f17-4037-8130-6a23d3f3983e
SELECT * FROM public.analytics
-- Impersonate user 1c314945-6f17-4037-8130-6a23d3f3983e and try to query other user
SELECT * FROM analytics where user_id = 'c4b6d34f-a414-477f-b6db-40d232a3c8e0'; No rows. |
Beta Was this translation helpful? Give feedback.
You can enforce RLS on regular views now with security_invoker. https://www.postgresql.org/docs/current/sql-createview.html
The UI is limited to displaying views and you need to use SQL to edit them.