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
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
-- Restrict org billing/usage status RPCs
-- so anonymous callers cannot infer org plan state.
CREATE OR REPLACE FUNCTION public.is_paying_and_good_plan_org_action(
"orgid" uuid,
"actions" public.action_type []
) RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = '' AS $$
DECLARE
caller_role text;
org_customer_id text;
result boolean;
has_credits boolean;
BEGIN
SELECT current_setting('role', true) INTO caller_role;

IF COALESCE(caller_role, '') NOT IN ('service_role', 'postgres', 'supabase_admin') THEN
IF NOT (public.check_min_rights(
'read'::public.user_min_right,
(SELECT public.get_identity_org_allowed('{read,upload,write,all}'::public.key_mode[], is_paying_and_good_plan_org_action.orgid)),
is_paying_and_good_plan_org_action.orgid,
NULL::character varying,
NULL::bigint
)) THEN
RETURN false;
END IF;
END IF;

SELECT EXISTS (
SELECT 1
FROM public.usage_credit_balances ucb
WHERE ucb.org_id = orgid
AND COALESCE(ucb.available_credits, 0) > 0
) INTO has_credits;

IF has_credits THEN
RETURN true;
END IF;

SELECT o.customer_id INTO org_customer_id
FROM public.orgs o
WHERE o.id = orgid;

SELECT (si.trial_at > now()) OR (si.status = 'succeeded' AND NOT (
(si.mau_exceeded AND 'mau' = ANY(actions))
OR (si.storage_exceeded AND 'storage' = ANY(actions))
OR (si.bandwidth_exceeded AND 'bandwidth' = ANY(actions))
OR (si.build_time_exceeded AND 'build_time' = ANY(actions))
))
INTO result
FROM public.stripe_info si
WHERE si.customer_id = org_customer_id
LIMIT 1;

RETURN COALESCE(result, false);
END;
$$;

ALTER FUNCTION public.is_paying_and_good_plan_org_action(
"orgid" uuid,
"actions" public.action_type []
) OWNER TO "postgres";

REVOKE ALL ON FUNCTION public.is_paying_and_good_plan_org_action(
"orgid" uuid,
"actions" public.action_type []
) FROM public;
REVOKE ALL ON FUNCTION public.is_paying_and_good_plan_org_action(
"orgid" uuid,
"actions" public.action_type []
) FROM anon;
REVOKE ALL ON FUNCTION public.is_paying_and_good_plan_org_action(
"orgid" uuid,
"actions" public.action_type []
) FROM authenticated;
GRANT EXECUTE ON FUNCTION public.is_paying_and_good_plan_org_action(
"orgid" uuid,
"actions" public.action_type []
) TO authenticated;
GRANT EXECUTE ON FUNCTION public.is_paying_and_good_plan_org_action(
"orgid" uuid,
"actions" public.action_type []
) TO service_role;
2 changes: 2 additions & 0 deletions supabase/tests/11_test_plan.sql
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ BEGIN
END;
$$ LANGUAGE plpgsql;

SELECT tests.authenticate_as_service_role();

SELECT my_tests();

SELECT *
Expand Down
2 changes: 2 additions & 0 deletions supabase/tests/25_test_secret_functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ BEGIN;

SELECT plan(3);

SELECT tests.authenticate_as_service_role();

-- Test is_org_yearly
SELECT
is(
Expand Down
43 changes: 41 additions & 2 deletions supabase/tests/46_test_org_status_rpcs.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
BEGIN;

SELECT plan(8);
SELECT plan(12);

-- Member of admin org can read billing/trial RPCs
SELECT tests.authenticate_as('test_admin');
Expand All @@ -19,6 +19,16 @@ SELECT
'is_trial_org - org admin can read trial days'
);

SELECT
is(
is_paying_and_good_plan_org_action(
'22dbad8a-b885-4309-9b3b-a09f8460fb6d',
ARRAY['mau']::public.action_type []
),
true,
'is_paying_and_good_plan_org_action - org admin can read plan status'
);

-- Non-member should be denied by org authorization checks
SELECT tests.authenticate_as('test_user');

Expand All @@ -36,6 +46,16 @@ SELECT
'is_trial_org - non-member org user gets 0'
);

SELECT
is(
is_paying_and_good_plan_org_action(
'22dbad8a-b885-4309-9b3b-a09f8460fb6d',
ARRAY['mau']::public.action_type []
),
false,
'is_paying_and_good_plan_org_action - non-member org user gets false'
);

-- Anonymous user should not have execute permission
SELECT tests.clear_authentication();

Expand All @@ -55,6 +75,16 @@ SELECT
'is_trial_org - anonymous call is blocked'
);

SELECT
throws_ok(
'SELECT is_paying_and_good_plan_org_action('
|| '''22dbad8a-b885-4309-9b3b-a09f8460fb6d'', '
|| 'ARRAY[''mau'']::public.action_type[])',
'42501',
'permission denied for function is_paying_and_good_plan_org_action',
'is_paying_and_good_plan_org_action - anonymous call is blocked'
);

-- service role keeps backend-style access
SELECT tests.authenticate_as_service_role();

Expand All @@ -73,7 +103,16 @@ SELECT
);

SELECT
*
is(
is_paying_and_good_plan_org_action(
'22dbad8a-b885-4309-9b3b-a09f8460fb6d',
ARRAY['mau']::public.action_type []
),
true,
'is_paying_and_good_plan_org_action - service role can read plan status'
);

SELECT * -- noqa: AM04
FROM
finish();

Expand Down
Loading