Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions supabase/tests/admin_tables_rls_test.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
BEGIN;
SELECT plan(19);

-- Returns how many rows a statement changed; a privilege or RLS rejection changes none.
CREATE FUNCTION pg_temp.rows_changed(statement text)
RETURNS bigint LANGUAGE plpgsql AS $$
DECLARE changed bigint;
BEGIN
EXECUTE statement;
GET DIAGNOSTICS changed = ROW_COUNT;
RETURN changed;
EXCEPTION WHEN insufficient_privilege THEN
RETURN 0;
END;
$$;

INSERT INTO auth.users (id) VALUES
('b1111111-1111-4111-8111-111111111111'),
('b2222222-2222-4222-8222-222222222222'),
('b3333333-3333-4333-8333-333333333333');
INSERT INTO public.user_roles (user_id, role) VALUES
('b1111111-1111-4111-8111-111111111111', 'user'),
('b2222222-2222-4222-8222-222222222222', 'admin');
INSERT INTO public.admin_grants (admin_user_id, target_user_id, grant_type, starts_at, ends_at, notes) VALUES
('b2222222-2222-4222-8222-222222222222', 'b1111111-1111-4111-8111-111111111111', 'pro', now(), now() + interval '30 days', 'pgtap grant');
INSERT INTO public.rate_limits (user_id, limit_type, limit_value) VALUES
(NULL, 'pgtap_daily', 100),
('b1111111-1111-4111-8111-111111111111', 'pgtap_daily', 50),
('b3333333-3333-4333-8333-333333333333', 'pgtap_daily', 500);
INSERT INTO public.global_prompts (feature, prompt_text)
VALUES ('ask', 'pgtap prompt')
ON CONFLICT (feature) DO UPDATE SET prompt_text = EXCLUDED.prompt_text;

-- A regular user who is the target of an admin grant.
SET LOCAL ROLE authenticated;
SELECT set_config('request.jwt.claim.sub', 'b1111111-1111-4111-8111-111111111111', true);
SELECT set_config('request.jwt.claim.role', 'authenticated', true);
SELECT is_empty(
$$SELECT 1 FROM public.admin_grants$$,
'a regular user cannot read the admin grant log, even grants made to them'
);
SELECT throws_ok(
$$INSERT INTO public.admin_grants (target_user_id, grant_type, starts_at, ends_at)
VALUES ('b1111111-1111-4111-8111-111111111111', 'pro', now(), now() + interval '1 year')$$,
'42501',
NULL,
'a regular user cannot record a grant for themselves'
);
SELECT results_eq(
$$SELECT user_id, limit_value FROM public.rate_limits
WHERE limit_type = 'pgtap_daily' ORDER BY user_id NULLS FIRST$$,
$$VALUES (NULL::uuid, 100), ('b1111111-1111-4111-8111-111111111111'::uuid, 50)$$,
'a user reads the global limit and their own, not other users'' limits'
);
SELECT is(
pg_temp.rows_changed($$UPDATE public.rate_limits SET limit_value = 1000000
WHERE limit_type = 'pgtap_daily'$$),
0::bigint,
'a user cannot raise their own or the global rate limit'
);
SELECT is(
pg_temp.rows_changed($$DELETE FROM public.rate_limits WHERE limit_type = 'pgtap_daily'$$),
0::bigint,
'a user cannot delete rate limits'
);
SELECT throws_ok(
$$INSERT INTO public.rate_limits (user_id, limit_type, limit_value)
VALUES ('b1111111-1111-4111-8111-111111111111', 'pgtap_other', 1000000)$$,
'42501',
NULL,
'a user cannot create a rate limit override'
);
SELECT throws_ok(
$$SELECT * FROM public.get_active_rate_limits()$$,
'P0001',
'administrator access required',
'a regular user cannot list every user''s rate limits'
);
SELECT throws_ok(
$$INSERT INTO public.global_prompts (feature, prompt_text) VALUES ('attack', 'injected')$$,
'42501',
NULL,
'a regular user cannot add a global prompt'
);
SELECT is(
pg_temp.rows_changed($$UPDATE public.global_prompts SET prompt_text = 'injected' WHERE feature = 'ask'$$),
0::bigint,
'a regular user cannot rewrite a global prompt'
);

-- Administrators read the grant log, but every admin write goes through the
-- admin Edge Function with the service role.
SELECT set_config('request.jwt.claim.sub', 'b2222222-2222-4222-8222-222222222222', true);
SELECT results_eq(
$$SELECT target_user_id, notes FROM public.admin_grants$$,
$$VALUES ('b1111111-1111-4111-8111-111111111111'::uuid, 'pgtap grant'::text)$$,
'an administrator reads the grant log'
);
SELECT throws_ok(
$$INSERT INTO public.admin_grants (target_user_id, grant_type, starts_at, ends_at)
VALUES ('b2222222-2222-4222-8222-222222222222', 'pro', now(), now() + interval '1 year')$$,
'42501',
NULL,
'an administrator cannot write the grant log directly'
);
SELECT is(
pg_temp.rows_changed($$DELETE FROM public.admin_grants$$),
0::bigint,
'an administrator cannot erase the grant log directly'
);
SELECT is(
pg_temp.rows_changed($$UPDATE public.global_prompts SET prompt_text = 'direct' WHERE feature = 'ask'$$),
0::bigint,
'an administrator cannot rewrite a global prompt directly'
);
RESET ROLE;

SET LOCAL ROLE anon;
SELECT set_config('request.jwt.claim.sub', '', true);
SELECT set_config('request.jwt.claim.role', 'anon', true);
SELECT is_empty(
$$SELECT 1 FROM public.admin_grants$$,
'anonymous callers cannot read the grant log'
);
SELECT is_empty(
$$SELECT 1 FROM public.rate_limits WHERE user_id IS NOT NULL$$,
'anonymous callers cannot read per-user rate limits'
);
SELECT throws_ok(
$$SELECT * FROM public.get_active_rate_limits()$$,
'P0001',
'administrator access required',
'anonymous callers cannot list rate limits through the admin RPC'
);
SELECT throws_ok(
$$INSERT INTO public.rate_limits (user_id, limit_type, limit_value)
VALUES (NULL, 'pgtap_other', 1000000)$$,
'42501',
NULL,
'anonymous callers cannot create rate limits'
);
SELECT is(
pg_temp.rows_changed($$DELETE FROM public.global_prompts$$),
0::bigint,
'anonymous callers cannot delete global prompts'
);
RESET ROLE;

SELECT results_eq(
$$SELECT
(SELECT count(*) FROM public.admin_grants WHERE notes = 'pgtap grant'),
(SELECT string_agg(limit_value::text, ',' ORDER BY limit_value)
FROM public.rate_limits WHERE limit_type = 'pgtap_daily'),
(SELECT prompt_text FROM public.global_prompts WHERE feature = 'ask')$$,
$$VALUES (1::bigint, '50,100,500'::text, 'pgtap prompt'::text)$$,
'no client attempt changed grants, limits, or prompts'
);

SELECT * FROM finish();
ROLLBACK;
160 changes: 160 additions & 0 deletions supabase/tests/owner_scoped_tables_rls_test.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
BEGIN;
SELECT plan(19);

-- Returns how many rows a statement changed; a privilege or RLS rejection changes none.
CREATE FUNCTION pg_temp.rows_changed(statement text)
RETURNS bigint LANGUAGE plpgsql AS $$
DECLARE changed bigint;
BEGIN
EXECUTE statement;
GET DIAGNOSTICS changed = ROW_COUNT;
RETURN changed;
EXCEPTION WHEN insufficient_privilege THEN
RETURN 0;
END;
$$;

INSERT INTO auth.users (id) VALUES
('e1111111-1111-4111-8111-111111111111'),
('e2222222-2222-4222-8222-222222222222');
INSERT INTO public.api_keys (id, user_id, provider, key_ciphertext, key_last4) VALUES
('e1a00000-0000-4000-8000-000000000001', 'e1111111-1111-4111-8111-111111111111', 'openai', 'ciphertext-one', '1111'),
('e2a00000-0000-4000-8000-000000000002', 'e2222222-2222-4222-8222-222222222222', 'openai', 'ciphertext-two', '2222');
INSERT INTO public.debates (id, user_id, motion) VALUES
('e1d00000-0000-4000-8000-000000000001', 'e1111111-1111-4111-8111-111111111111', 'This House would ban homework'),
('e2d00000-0000-4000-8000-000000000002', 'e2222222-2222-4222-8222-222222222222', 'This House would abolish zoos');
INSERT INTO public.sources (id, user_id, debate_id, title, content, type) VALUES
('e1500000-0000-4000-8000-000000000001', 'e1111111-1111-4111-8111-111111111111', 'e1d00000-0000-4000-8000-000000000001', 'One', 'Body', 'text'),
('e2500000-0000-4000-8000-000000000002', 'e2222222-2222-4222-8222-222222222222', 'e2d00000-0000-4000-8000-000000000002', 'Two', 'Body', 'text');
INSERT INTO public.source_files (id, user_id, source_id, filename, storage_key) VALUES
('e1f00000-0000-4000-8000-000000000001', 'e1111111-1111-4111-8111-111111111111', 'e1500000-0000-4000-8000-000000000001', 'one.pdf', 'one/key'),
('e2f00000-0000-4000-8000-000000000002', 'e2222222-2222-4222-8222-222222222222', 'e2500000-0000-4000-8000-000000000002', 'two.pdf', 'two/key');

SET LOCAL ROLE authenticated;
SELECT set_config('request.jwt.claim.sub', 'e1111111-1111-4111-8111-111111111111', true);
SELECT set_config('request.jwt.claim.role', 'authenticated', true);

-- api_keys: each user manages only their own encrypted provider keys.
SELECT results_eq(
$$SELECT id FROM public.api_keys$$,
$$VALUES ('e1a00000-0000-4000-8000-000000000001'::uuid)$$,
'a user reads only their own API keys'
);
SELECT lives_ok(
$$INSERT INTO public.api_keys (user_id, provider, key_ciphertext)
VALUES ('e1111111-1111-4111-8111-111111111111', 'anthropic', 'ciphertext-three')$$,
'a user can store their own API key'
);
SELECT throws_ok(
$$INSERT INTO public.api_keys (user_id, provider, key_ciphertext)
VALUES ('e2222222-2222-4222-8222-222222222222', 'anthropic', 'planted')$$,
'42501',
NULL,
'a user cannot store an API key on another account'
);
SELECT is(
pg_temp.rows_changed($$UPDATE public.api_keys SET key_ciphertext = 'replaced'
WHERE id = 'e2a00000-0000-4000-8000-000000000002'$$),
0::bigint,
'a user cannot replace another user''s API key'
);
SELECT throws_ok(
$$UPDATE public.api_keys SET user_id = 'e2222222-2222-4222-8222-222222222222'
WHERE id = 'e1a00000-0000-4000-8000-000000000001'$$,
'42501',
NULL,
'a user cannot move their API key onto another account'
);
SELECT is(
pg_temp.rows_changed($$DELETE FROM public.api_keys WHERE id = 'e2a00000-0000-4000-8000-000000000002'$$),
0::bigint,
'a user cannot delete another user''s API key'
);
SELECT is(
pg_temp.rows_changed($$DELETE FROM public.api_keys WHERE id = 'e1a00000-0000-4000-8000-000000000001'$$),
1::bigint,
'a user can delete their own API key'
);

-- source_files: each user manages only their own uploaded file records.
SELECT results_eq(
$$SELECT id FROM public.source_files$$,
$$VALUES ('e1f00000-0000-4000-8000-000000000001'::uuid)$$,
'a user reads only their own source files'
);
SELECT lives_ok(
$$INSERT INTO public.source_files (user_id, source_id, filename, storage_key)
VALUES ('e1111111-1111-4111-8111-111111111111', 'e1500000-0000-4000-8000-000000000001', 'three.pdf', 'three/key')$$,
'a user can record their own source file'
);
SELECT throws_ok(
$$INSERT INTO public.source_files (user_id, source_id, filename, storage_key)
VALUES ('e2222222-2222-4222-8222-222222222222', 'e2500000-0000-4000-8000-000000000002', 'planted.pdf', 'planted/key')$$,
'42501',
NULL,
'a user cannot record a source file on another account'
);
SELECT is(
pg_temp.rows_changed($$UPDATE public.source_files SET storage_key = 'redirected/key'
WHERE id = 'e2f00000-0000-4000-8000-000000000002'$$),
0::bigint,
'a user cannot redirect another user''s stored file'
);
SELECT throws_ok(
$$UPDATE public.source_files SET user_id = 'e2222222-2222-4222-8222-222222222222'
WHERE id = 'e1f00000-0000-4000-8000-000000000001'$$,
'42501',
NULL,
'a user cannot move their source file onto another account'
);
SELECT is(
pg_temp.rows_changed($$DELETE FROM public.source_files WHERE id = 'e2f00000-0000-4000-8000-000000000002'$$),
0::bigint,
'a user cannot delete another user''s source file'
);
SELECT is(
pg_temp.rows_changed($$UPDATE public.source_files SET filename = 'renamed.pdf'
WHERE id = 'e1f00000-0000-4000-8000-000000000001'$$),
1::bigint,
'a user can update their own source file'
);
RESET ROLE;

SET LOCAL ROLE anon;
SELECT set_config('request.jwt.claim.sub', '', true);
SELECT set_config('request.jwt.claim.role', 'anon', true);
SELECT is_empty(
$$SELECT 1 FROM public.api_keys$$,
'anonymous callers see no API keys'
);
SELECT is_empty(
$$SELECT 1 FROM public.source_files$$,
'anonymous callers see no source files'
);
SELECT throws_ok(
$$INSERT INTO public.api_keys (user_id, provider, key_ciphertext)
VALUES ('e2222222-2222-4222-8222-222222222222', 'anthropic', 'planted')$$,
'42501',
NULL,
'anonymous callers cannot store API keys'
);
SELECT throws_ok(
$$INSERT INTO public.source_files (user_id, source_id, filename, storage_key)
VALUES ('e2222222-2222-4222-8222-222222222222', 'e2500000-0000-4000-8000-000000000002', 'planted.pdf', 'planted/key')$$,
'42501',
NULL,
'anonymous callers cannot record source files'
);
RESET ROLE;

SELECT results_eq(
$$SELECT a.key_ciphertext, f.storage_key
FROM public.api_keys a, public.source_files f
WHERE a.user_id = 'e2222222-2222-4222-8222-222222222222'
AND f.user_id = 'e2222222-2222-4222-8222-222222222222'$$,
$$VALUES ('ciphertext-two'::text, 'two/key'::text)$$,
'the other user''s key and file are untouched'
);

SELECT * FROM finish();
ROLLBACK;
Loading
Loading