Skip to content

Commit 1e89b2b

Browse files
fix: infinite recursion bug
1 parent bf5ffbc commit 1e89b2b

2 files changed

Lines changed: 153 additions & 0 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
-- Fix org create RPC for Postgres plpgsql.variable_conflict=error environments.
2+
-- Earlier deployments used an unqualified `org_id` column reference inside
3+
-- `powergit_create_org`, which collides with the function argument and raises:
4+
-- column reference "org_id" is ambiguous
5+
6+
create or replace function public.powergit_create_org(org_id text, name text default null)
7+
returns public.orgs
8+
language plpgsql
9+
security definer
10+
set search_path = public
11+
as $$
12+
declare
13+
uid uuid;
14+
normalized text;
15+
candidate_name text;
16+
existing public.orgs%rowtype;
17+
member_count int;
18+
begin
19+
uid := auth.uid();
20+
if uid is null then
21+
raise exception 'Not authenticated.' using errcode = '28000';
22+
end if;
23+
24+
normalized := lower(trim(org_id));
25+
if normalized is null or normalized = '' then
26+
raise exception 'org_id is required.' using errcode = '22023';
27+
end if;
28+
29+
if normalized like 'gh-%' or normalized like 'github-%' then
30+
raise exception 'Org ids starting with "gh-" or "github-" are reserved.' using errcode = '22023';
31+
end if;
32+
33+
if normalized !~ '^[a-z0-9][a-z0-9._-]{0,79}$' then
34+
raise exception 'Invalid org id "%". Use 1-80 chars: [a-z0-9._-].', normalized using errcode = '22023';
35+
end if;
36+
37+
candidate_name := nullif(trim(coalesce(name, '')), '');
38+
39+
select * into existing from public.orgs where id = normalized limit 1;
40+
if found then
41+
-- Qualify org_id to avoid ambiguity with the `org_id` function argument.
42+
select count(*) into member_count from public.org_members m where m.org_id = normalized;
43+
if member_count = 0 then
44+
update public.orgs
45+
set
46+
name = coalesce(candidate_name, public.orgs.name),
47+
created_by = coalesce(public.orgs.created_by, uid),
48+
updated_at = now()
49+
where id = normalized;
50+
insert into public.org_members (org_id, user_id, role)
51+
values (normalized, uid, 'admin')
52+
on conflict on constraint org_members_pkey do update set role = 'admin', updated_at = now();
53+
select * into existing from public.orgs where id = normalized limit 1;
54+
return existing;
55+
end if;
56+
raise exception 'Org "%" already exists.', normalized using errcode = '23505';
57+
end if;
58+
59+
insert into public.orgs (id, name, created_by)
60+
values (normalized, candidate_name, uid)
61+
returning * into existing;
62+
63+
insert into public.org_members (org_id, user_id, role)
64+
values (normalized, uid, 'admin')
65+
on conflict on constraint org_members_pkey do update set role = 'admin', updated_at = now();
66+
67+
return existing;
68+
end;
69+
$$;
70+
71+
grant execute on function public.powergit_create_org(text, text) to authenticated;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
-- Fix org ACL helper functions for RLS recursion.
2+
-- Older deployments defined these helpers as INVOKER functions, but they are used inside RLS
3+
-- policies for `org_members`, causing infinite recursion and:
4+
-- stack depth limit exceeded
5+
--
6+
-- This migration replaces the helpers with SECURITY DEFINER variants that (a) avoid recursion by
7+
-- bypassing RLS as the table owner and (b) refuse checks for other users by validating auth.uid().
8+
9+
create or replace function public.powergit_user_is_org_member(org_id text, user_id uuid)
10+
returns boolean
11+
language sql
12+
stable
13+
security definer
14+
set search_path = public
15+
as $$
16+
select case
17+
when auth.uid() is distinct from $2 then false
18+
else exists (
19+
select 1
20+
from public.org_members m
21+
where m.org_id = $1
22+
and m.user_id = $2
23+
)
24+
end;
25+
$$;
26+
27+
create or replace function public.powergit_user_is_org_admin(org_id text, user_id uuid)
28+
returns boolean
29+
language sql
30+
stable
31+
security definer
32+
set search_path = public
33+
as $$
34+
select case
35+
when auth.uid() is distinct from $2 then false
36+
else exists (
37+
select 1
38+
from public.org_members m
39+
where m.org_id = $1
40+
and m.user_id = $2
41+
and m.role = 'admin'
42+
)
43+
end;
44+
$$;
45+
46+
create or replace function public.powergit_user_can_read_repo(org_id text, repo_id text, user_id uuid)
47+
returns boolean
48+
language sql
49+
stable
50+
security definer
51+
set search_path = public
52+
as $$
53+
select case
54+
when auth.uid() is distinct from $3 then false
55+
else public.powergit_repo_is_public($1, $2)
56+
or public.powergit_user_is_org_member($1, $3)
57+
end;
58+
$$;
59+
60+
create or replace function public.powergit_user_can_write_repo(org_id text, repo_id text, user_id uuid)
61+
returns boolean
62+
language sql
63+
stable
64+
security definer
65+
set search_path = public
66+
as $$
67+
select case
68+
when auth.uid() is distinct from $3 then false
69+
else exists (
70+
select 1
71+
from public.org_members m
72+
where m.org_id = $1
73+
and m.user_id = $3
74+
and m.role in ('admin', 'write')
75+
)
76+
end;
77+
$$;
78+
79+
grant execute on function public.powergit_user_is_org_member(text, uuid) to anon, authenticated, service_role;
80+
grant execute on function public.powergit_user_is_org_admin(text, uuid) to anon, authenticated, service_role;
81+
grant execute on function public.powergit_user_can_read_repo(text, text, uuid) to anon, authenticated, service_role;
82+
grant execute on function public.powergit_user_can_write_repo(text, text, uuid) to anon, authenticated, service_role;

0 commit comments

Comments
 (0)