env | grep -i supabase | sed 's/\(KEY\|PASSWORD\)=.*/\1=***/' && echo "---" && which psql && psql --version
SUPABASE_DB_PASSWORD=*** SUPABASE_DB_URL=<redacted:SUPABASE_DB_URL> SUPABASE_URL=https://<ref>.supabase.co SUPABASE_SERVICE_ROLE_KEY=*** SUPABASE_PROJECT_REF=<ref> SUPABASE_ANON_KEY=*** --- /usr/bin/psql psql (PostgreSQL) 18.4 (Debian 18.4-1.pgdg12+1)
export PGPASSWORD='<redacted:SUPABASE_DB_PASSWORD>' psql "$SUPABASE_DB_URL" -c "\dt public.*" echo "---orgs---" psql "$SUPABASE_DB_URL" -c "SELECT id, name FROM public.orgs ORDER BY name;" echo "---org_members---" psql "$SUPABASE_DB_URL" -c "SELECT org_id, user_id, role FROM public.org_members ORDER BY org_id, role;" echo "---tickets---" psql "$SUPABASE_DB_URL" -c "SELECT id, org_id, title, status FROM public.tickets ORDER BY org_id, id;" echo "---auth.users---" psql "$SUPABASE_DB_URL" -c "SELECT id, email FROM auth.users ORDER BY email;"
List of tables
Schema | Name | Type | Owner
--------+-------------+-------+----------
public | org_members | table | postgres
public | orgs | table | postgres
public | tickets | table | postgres
(3 rows)
---orgs---
id | name
--------------------------------------+--------
35d9ad58-5bf0-448f-be83-554f0c27636e | Acme
60b9571d-16dc-4358-855a-9b6e0b2c02b2 | Globex
(2 rows)
---org_members---
org_id | user_id | role
--------------------------------------+--------------------------------------+--------
35d9ad58-5bf0-448f-be83-554f0c27636e | 93c019af-2eb0-4972-afbe-5eed77c5d2a6 | admin
35d9ad58-5bf0-448f-be83-554f0c27636e | b492fa17-4d34-4168-935b-cc0086ec7d7e | member
60b9571d-16dc-4358-855a-9b6e0b2c02b2 | 2d4bf1ce-ba27-4339-9f0a-65fcea10078a | member
(3 rows)
---tickets---
id | org_id | title | status
----+--------------------------------------+----------------+--------
1 | 35d9ad58-5bf0-448f-be83-554f0c27636e | Fix login | open
2 | 35d9ad58-5bf0-448f-be83-554f0c27636e | Update billing | open
3 | 35d9ad58-5bf0-448f-be83-554f0c27636e | Rotate keys | open
4 | 60b9571d-16dc-4358-855a-9b6e0b2c02b2 | Migrate DNS | open
5 | 60b9571d-16dc-4358-855a-9b6e0b2c02b2 | Renew cert | open
(5 rows)
---auth.users---
id | email
--------------------------------------+-----------------------
93c019af-2eb0-4972-afbe-5eed77c5d2a6 | uma-admin@cari.dev
b492fa17-4d34-4168-935b-cc0086ec7d7e | vic-member@cari.dev
2d4bf1ce-ba27-4339-9f0a-65fcea10078a | wen-outsider@cari.dev
(3 rows)
export PGPASSWORD='<redacted:SUPABASE_DB_PASSWORD>' psql "$SUPABASE_DB_URL" <<'SQL' SELECT schemaname, tablename, rowsecurity, forcerowsecurity FROM pg_tables WHERE schemaname='public' ORDER BY tablename; \echo --- existing policies --- SELECT schemaname, tablename, policyname, cmd, roles, qual, with_check FROM pg_policies WHERE schemaname='public' ORDER BY tablename, policyname; SQL
--- existing policies --- schemaname | tablename | policyname | cmd | roles | qual | with_check ------------+-----------+------------+-----+-------+------+------------ (0 rows)
ERROR: column "forcerowsecurity" does not exist
LINE 1: SELECT schemaname, tablename, rowsecurity, forcerowsecurity
^
export PGPASSWORD='<redacted:SUPABASE_DB_PASSWORD>'
psql "$SUPABASE_DB_URL" <<'SQL'
-- 1. ORGS: remain readable for all (authenticated + anon).
ALTER TABLE public.orgs ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS orgs_read_all ON public.orgs;
CREATE POLICY orgs_read_all ON public.orgs
FOR SELECT
TO authenticated, anon
USING (true);
-- 2. ORG_MEMBERS: only members of that same org may SELECT rows.
-- No write policies -> writes blocked for non-owners.
ALTER TABLE public.org_members ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS org_members_select_same_org ON public.org_members;
CREATE POLICY org_members_select_same_org ON public.org_members
FOR SELECT
TO authenticated
USING (
org_id IN (
SELECT om.org_id FROM public.org_members om
WHERE om.user_id = auth.uid()
)
);
-- 3. TICKETS: members read; admins write.
ALTER TABLE public.tickets ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tickets_select_members ON public.tickets;
CREATE POLICY tickets_select_members ON public.tickets
FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = tickets.org_id
AND om.user_id = auth.uid()
)
);
DROP POLICY IF EXISTS tickets_insert_admin ON public.tickets;
CREATE POLICY tickets_insert_admin ON public.tickets
FOR INSERT
TO authenticated
WITH CHECK (
EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = tickets.org_id
AND om.user_id = auth.uid()
AND om.role = 'admin'
)
);
DROP POLICY IF EXISTS tickets_update_admin ON public.tickets;
CREATE POLICY tickets_update_admin ON public.tickets
FOR UPDATE
TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = tickets.org_id
AND om.user_id = auth.uid()
AND om.role = 'admin'
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = tickets.org_id
AND om.user_id = auth.uid()
AND om.role = 'admin'
)
);
DROP POLICY IF EXISTS tickets_delete_admin ON public.tickets;
CREATE POLICY tickets_delete_admin ON public.tickets
FOR DELETE
TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = tickets.org_id
AND om.user_id = auth.uid()
AND om.role = 'admin'
)
);
\echo --- rls status ---
SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname='public' ORDER BY tablename;
\echo --- policies ---
SELECT tablename, policyname, cmd, roles
FROM pg_policies WHERE schemaname='public' ORDER BY tablename, policyname;
SQLALTER TABLE
DROP POLICY
CREATE POLICY
ALTER TABLE
DROP POLICY
CREATE POLICY
ALTER TABLE
DROP POLICY
CREATE POLICY
DROP POLICY
CREATE POLICY
DROP POLICY
CREATE POLICY
DROP POLICY
CREATE POLICY
--- rls status ---
tablename | rowsecurity
-------------+-------------
org_members | t
orgs | t
tickets | t
(3 rows)
--- policies ---
tablename | policyname | cmd | roles
-------------+-----------------------------+--------+----------------------
org_members | org_members_select_same_org | SELECT | {authenticated}
orgs | orgs_read_all | SELECT | {anon,authenticated}
tickets | tickets_delete_admin | DELETE | {authenticated}
tickets | tickets_insert_admin | INSERT | {authenticated}
tickets | tickets_select_members | SELECT | {authenticated}
tickets | tickets_update_admin | UPDATE | {authenticated}
(6 rows)
NOTICE: policy "orgs_read_all" for relation "public.orgs" does not exist, skipping NOTICE: policy "org_members_select_same_org" for relation "public.org_members" does not exist, skipping NOTICE: policy "tickets_select_members" for relation "public.tickets" does not exist, skipping NOTICE: policy "tickets_insert_admin" for relation "public.tickets" does not exist, skipping NOTICE: policy "tickets_update_admin" for relation "public.tickets" does not exist, skipping NOTICE: policy "tickets_delete_admin" for relation "public.tickets" does not exist, skipping
cat > /tmp/login.sh <<'SH'
#!/usr/bin/env bash
email="$1"; pw="$2"
curl -s -X POST "$SUPABASE_URL/auth/v1/token?grant_type=password" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Content-Type: application/json" \
-d "{\"email\":\"$email\",\"password\":\"$pw\"}"
SH
chmod +x /tmp/login.sh
login() { /tmp/login.sh "$1" "$2" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('access_token') or json.dumps(d))"; }
UMA=$(login uma-admin@cari.dev Passw0rd-uma-11)
VIC=$(login vic-member@cari.dev Passw0rd-vic-22)
WEN=$(login wen-outsider@cari.dev Passw0rd-wen-33)
echo "UMA len: ${#UMA} VIC len: ${#VIC} WEN len: ${#WEN}"
[ "${#UMA}" -gt 50 ] && echo "UMA OK" || { echo "UMA login:"; echo "$UMA"; }
[ "${#VIC}" -gt 50 ] && echo "VIC OK" || { echo "VIC login:"; echo "$VIC"; }
[ "${#WEN}" -gt 50 ] && echo "WEN OK" || { echo "WEN login:"; echo "$WEN"; }
echo "$UMA" > /tmp/uma.tok
echo "$VIC" > /tmp/vic.tok
echo "$WEN" > /tmp/wen.tokUMA len: 796 VIC len: 798 WEN len: 800 UMA OK VIC OK WEN OK
UMA=$(cat /tmp/uma.tok); VIC=$(cat /tmp/vic.tok); WEN=$(cat /tmp/wen.tok)
probe() {
local label="$1" tok="$2" table="$3"
echo "=== $label -> $table ==="
curl -s -o /tmp/body -w "HTTP %{http_code}\n" \
"$SUPABASE_URL/rest/v1/$table?select=*" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Authorization: Bearer $tok"
python3 -c "import json,sys; d=json.load(open('/tmp/body')); print(json.dumps(d, indent=2) if isinstance(d,list) else d)" 2>/dev/null || cat /tmp/body
echo
}
probe "UMA (Acme admin)" "$UMA" "tickets"
probe "VIC (Acme member)" "$VIC" "tickets"
probe "WEN (Globex member, outsider to Acme)" "$WEN" "tickets"
probe "anon" "$SUPABASE_ANON_KEY" "tickets"=== UMA (Acme admin) -> tickets ===
HTTP 500
{'code': '42P17', 'details': None, 'hint': None, 'message': 'infinite recursion detected in policy for relation "org_members"'}
=== VIC (Acme member) -> tickets ===
HTTP 500
{'code': '42P17', 'details': None, 'hint': None, 'message': 'infinite recursion detected in policy for relation "org_members"'}
=== WEN (Globex member, outsider to Acme) -> tickets ===
HTTP 500
{'code': '42P17', 'details': None, 'hint': None, 'message': 'infinite recursion detected in policy for relation "org_members"'}
=== anon -> tickets ===
HTTP 200
[]
export PGPASSWORD='<redacted:SUPABASE_DB_PASSWORD>' psql "$SUPABASE_DB_URL" <<'SQL' -- Helper: list of org_ids the current user belongs to. SECURITY DEFINER -- bypasses RLS, so it doesn't recurse into org_members' own SELECT policy. CREATE OR REPLACE FUNCTION public.current_user_org_ids() RETURNS SETOF uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT org_id FROM public.org_members WHERE user_id = auth.uid(); $$; CREATE OR REPLACE FUNCTION public.is_org_member(p_org_id uuid) RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT EXISTS (SELECT 1 FROM public.org_members WHERE user_id = auth.uid() AND org_id = p_org_id); $$; CREATE OR REPLACE FUNCTION public.is_org_admin(p_org_id uuid) RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT EXISTS (SELECT 1 FROM public.org_members WHERE user_id = auth.uid() AND org_id = p_org_id AND role = 'admin'); $$; REVOKE ALL ON FUNCTION public.current_user_org_ids() FROM PUBLIC; REVOKE ALL ON FUNCTION public.is_org_member(uuid) FROM PUBLIC; REVOKE ALL ON FUNCTION public.is_org_admin(uuid) FROM PUBLIC; GRANT EXECUTE ON FUNCTION public.current_user_org_ids() TO authenticated; GRANT EXECUTE ON FUNCTION public.is_org_member(uuid) TO authenticated; GRANT EXECUTE ON FUNCTION public.is_org_admin(uuid) TO authenticated; -- Replace the org_members policy to use the helper. DROP POLICY org_members_select_same_org ON public.org_members; CREATE POLICY org_members_select_same_org ON public.org_members FOR SELECT TO authenticated USING (org_id IN (SELECT public.current_user_org_ids())); -- Rewrite ticket policies to use the helpers (cleaner, no self-reference risk). DROP POLICY tickets_select_members ON public.tickets; CREATE POLICY tickets_select_members ON public.tickets FOR SELECT TO authenticated USING (public.is_org_member(org_id)); DROP POLICY tickets_insert_admin ON public.tickets; CREATE POLICY tickets_insert_admin ON public.tickets FOR INSERT TO authenticated WITH CHECK (public.is_org_admin(org_id)); DROP POLICY tickets_update_admin ON public.tickets; CREATE POLICY tickets_update_admin ON public.tickets FOR UPDATE TO authenticated USING (public.is_org_admin(org_id)) WITH CHECK (public.is_org_admin(org_id)); DROP POLICY tickets_delete_admin ON public.tickets; CREATE POLICY tickets_delete_admin ON public.tickets FOR DELETE TO authenticated USING (public.is_org_admin(org_id)); \echo --- final policies --- SELECT tablename, policyname, cmd, roles FROM pg_policies WHERE schemaname='public' ORDER BY tablename, policyname; SQL
CREATE FUNCTION
CREATE FUNCTION
CREATE FUNCTION
REVOKE
REVOKE
REVOKE
GRANT
GRANT
GRANT
DROP POLICY
CREATE POLICY
DROP POLICY
CREATE POLICY
DROP POLICY
CREATE POLICY
DROP POLICY
CREATE POLICY
DROP POLICY
CREATE POLICY
--- final policies ---
tablename | policyname | cmd | roles
-------------+-----------------------------+--------+----------------------
org_members | org_members_select_same_org | SELECT | {authenticated}
orgs | orgs_read_all | SELECT | {anon,authenticated}
tickets | tickets_delete_admin | DELETE | {authenticated}
tickets | tickets_insert_admin | INSERT | {authenticated}
tickets | tickets_select_members | SELECT | {authenticated}
tickets | tickets_update_admin | UPDATE | {authenticated}
(6 rows)
UMA=$(cat /tmp/uma.tok); VIC=$(cat /tmp/vic.tok); WEN=$(cat /tmp/wen.tok)
probe() {
local label="$1" tok="$2" table="$3" qs="$4"
echo "=== $label -> $table $qs ==="
curl -s -o /tmp/body -w "HTTP %{http_code}\n" \
"$SUPABASE_URL/rest/v1/$table$qs" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Authorization: Bearer $tok"
python3 -c "import json,sys; d=json.load(open('/tmp/body')); print(json.dumps(d, indent=2) if isinstance(d,list) else d)" 2>/dev/null || cat /tmp/body
echo
}
# ---- TICKETS read ----
probe "UMA" "$UMA" "tickets" "?select=id,org_id,title&order=id"
probe "VIC" "$VIC" "tickets" "?select=id,org_id,title&order=id"
probe "WEN" "$WEN" "tickets" "?select=id,org_id,title&order=id"
probe "anon" "$SUPABASE_ANON_KEY" "tickets" "?select=id,title"
# ---- ORG_MEMBERS read ----
probe "UMA" "$UMA" "org_members" "?select=org_id,user_id,role&order=role"
probe "VIC" "$VIC" "org_members" "?select=org_id,user_id,role&order=role"
probe "WEN" "$WEN" "org_members" "?select=org_id,user_id,role&order=role"
probe "anon" "$SUPABASE_ANON_KEY" "org_members" "?select=org_id,user_id"
# ---- ORGS read (should remain readable to everyone) ----
probe "UMA" "$UMA" "orgs" "?select=id,name&order=name"
probe "anon" "$SUPABASE_ANON_KEY" "orgs" "?select=id,name"=== UMA -> tickets ?select=id,org_id,title&order=id ===
HTTP 200
[
{
"id": 1,
"org_id": "35d9ad58-5bf0-448f-be83-554f0c27636e",
"title": "Fix login"
},
{
"id": 2,
"org_id": "35d9ad58-5bf0-448f-be83-554f0c27636e",
"title": "Update billing"
},
{
"id": 3,
"org_id": "35d9ad58-5bf0-448f-be83-554f0c27636e",
"title": "Rotate keys"
}
]
=== VIC -> tickets ?select=id,org_id,title&order=id ===
HTTP 200
[
{
"id": 1,
"org_id": "35d9ad58-5bf0-448f-be83-554f0c27636e",
"title": "Fix login"
},
{
"id": 2,
"org_id": "35d9ad58-5bf0-448f-be83-554f0c27636e",
"title": "Update billing"
},
{
"id": 3,
"org_id": "35d9ad58-5bf0-448f-be83-554f0c27636e",
"title": "Rotate keys"
}
]
=== WEN -> tickets ?select=id,org_id,title&order=id ===
HTTP 200
[
{
"id": 4,
"org_id": "60b9571d-16dc-4358-855a-9b6e0b2c02b2",
"title": "Migrate DNS"
},
{
"id": 5,
"org_id": "60b9571d-16dc-4358-855a-9b6e0b2c02b2",
"title": "Renew cert"
}
]
=== anon -> tickets ?select=id,title ===
HTTP 200
[]
=== UMA -> org_members ?select=org_id,user_id,role&order=role ===
HTTP 200
[
{
"org_id": "35d9ad58-5bf0-448f-be83-554f0c27636e",
"user_id": "93c019af-2eb0-4972-afbe-5eed77c5d2a6",
"role": "admin"
},
{
"org_id": "35d9ad58-5bf0-448f-be83-554f0c27636e",
"user_id": "b492fa17-4d34-4168-935b-cc0086ec7d7e",
"role": "member"
}
]
=== VIC -> org_members ?select=org_id,user_id,role&order=role ===
HTTP 200
[
{
"org_id": "35d9ad58-5bf0-448f-be83-554f0c27636e",
"user_id": "93c019af-2eb0-4972-afbe-5eed77c5d2a6",
"role": "admin"
},
{
"org_id": "35d9ad58-5bf0-448f-be83-554f0c27636e",
"user_id": "b492fa17-4d34-4168-935b-cc0086ec7d7e",
"role": "member"
}
]
=== WEN -> org_members ?select=org_id,user_id,role&order=role ===
HTTP 200
[
{
"org_id": "60b9571d-16dc-4358-855a-9b6e0b2c02b2",
"user_id": "2d4bf1ce-ba27-4339-9f0a-65fcea10078a",
"role": "member"
}
]
=== anon -> org_members ?select=org_id,user_id ===
HTTP 200
[]
=== UMA -> orgs ?select=id,name&order=name ===
HTTP 200
[
{
"id": "35d9ad58-5bf0-448f-be83-554f0c27636e",
"name": "Acme"
},
{
"id": "60b9571d-16dc-4358-855a-9b6e0b2c02b2",
"name": "Globex"
}
]
=== anon -> orgs ?select=id,name ===
HTTP 200
[
{
"id": "35d9ad58-5bf0-448f-be83-554f0c27636e",
"name": "Acme"
},
{
"id": "60b9571d-16dc-4358-855a-9b6e0b2c02b2",
"name": "Globex"
}
]
UMA=$(cat /tmp/uma.tok); VIC=$(cat /tmp/vic.tok); WEN=$(cat /tmp/wen.tok)
ACME='35d9ad58-5bf0-448f-be83-554f0c27636e'
GLOBEX='60b9571d-16dc-4358-855a-9b6e0b2c02b2'
# We'll only use rollback-friendly ops: INSERT then immediately DELETE; UPDATE that we then revert.
# Actually safest: just attempt writes and inspect responses (no successful writes should change anything net).
# --- INSERT ---
echo "--- INSERT (admin UMA into Acme) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X POST "$SUPABASE_URL/rest/v1/tickets" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $UMA" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d "{\"org_id\":\"$ACME\",\"title\":\"__rls_test_insert_admin__\",\"status\":\"open\"}"
cat /tmp/b; echo
echo "--- INSERT (member VIC into Acme, should be denied) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X POST "$SUPABASE_URL/rest/v1/tickets" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $VIC" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d "{\"org_id\":\"$ACME\",\"title\":\"__rls_test_insert_member__\",\"status\":\"open\"}"
cat /tmp/b; echo
echo "--- INSERT (outsider WEN into Acme, should be denied) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X POST "$SUPABASE_URL/rest/v1/tickets" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $WEN" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d "{\"org_id\":\"$ACME\",\"title\":\"__rls_test_insert_outsider__\",\"status\":\"open\"}"
cat /tmp/b; echo
echo "--- INSERT (anon, should be denied) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X POST "$SUPABASE_URL/rest/v1/tickets" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $SUPABASE_ANON_KEY" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d "{\"org_id\":\"$ACME\",\"title\":\"__rls_test_insert_anon__\",\"status\":\"open\"}"
cat /tmp/b; echo
echo "--- INSERT (admin UMA into Globex, should be denied - not a member) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X POST "$SUPABASE_URL/rest/v1/tickets" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $UMA" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d "{\"org_id\":\"$GLOBEX\",\"title\":\"__rls_test_insert_cross__\",\"status\":\"open\"}"
cat /tmp/b; echo--- INSERT (admin UMA into Acme) ---
HTTP 201
[{"id":6,"org_id":"35d9ad58-5bf0-448f-be83-554f0c27636e","title":"__rls_test_insert_admin__","status":"open"}]
--- INSERT (member VIC into Acme, should be denied) ---
HTTP 403
{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"tickets\""}
--- INSERT (outsider WEN into Acme, should be denied) ---
HTTP 403
{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"tickets\""}
--- INSERT (anon, should be denied) ---
HTTP 401
{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"tickets\""}
--- INSERT (admin UMA into Globex, should be denied - not a member) ---
HTTP 403
{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"tickets\""}
UMA=$(cat /tmp/uma.tok); VIC=$(cat /tmp/vic.tok); WEN=$(cat /tmp/wen.tok)
ACME='35d9ad58-5bf0-448f-be83-554f0c27636e'
# Clean up our test row with admin token.
echo "--- CLEANUP: admin deletes row id=6 ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X DELETE "$SUPABASE_URL/rest/v1/tickets?id=eq.6" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $UMA" \
-H "Prefer: return=representation"
cat /tmp/b; echo
# --- UPDATE: admin UMA should succeed; member VIC, outsider WEN, anon denied ---
echo "--- UPDATE (admin UMA) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X PATCH "$SUPABASE_URL/rest/v1/tickets?id=eq.1" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $UMA" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d '{"status":"in_progress"}'
cat /tmp/b; echo
echo "--- UPDATE (member VIC, should be denied) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X PATCH "$SUPABASE_URL/rest/v1/tickets?id=eq.1" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $VIC" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d '{"status":"blocked"}'
cat /tmp/b; echo
echo "--- UPDATE (outsider WEN, should be denied) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X PATCH "$SUPABASE_URL/rest/v1/tickets?id=eq.1" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $WEN" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d '{"status":"hacked"}'
cat /tmp/b; echo
echo "--- UPDATE (anon, should be denied) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X PATCH "$SUPABASE_URL/rest/v1/tickets?id=eq.1" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $SUPABASE_ANON_KEY" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d '{"status":"hacked"}'
cat /tmp/b; echo
# Revert admin update so data is unchanged.
echo "--- REVERT (admin UMA restores status) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X PATCH "$SUPABASE_URL/rest/v1/tickets?id=eq.1" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $UMA" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d '{"status":"open"}'
cat /tmp/b; echo
# --- DELETE ---
echo "--- DELETE (member VIC, should be denied) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X DELETE "$SUPABASE_URL/rest/v1/tickets?id=eq.1" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $VIC" \
-H "Prefer: return=representation"
cat /tmp/b; echo
echo "--- DELETE (outsider WEN, should be denied) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X DELETE "$SUPABASE_URL/rest/v1/tickets?id=eq.1" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $WEN" \
-H "Prefer: return=representation"
cat /tmp/b; echo
echo "--- DELETE (anon, should be denied) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X DELETE "$SUPABASE_URL/rest/v1/tickets?id=eq.1" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $SUPABASE_ANON_KEY" \
-H "Prefer: return=representation"
cat /tmp/b; echo
echo "--- DELETE (admin UMA across orgs should be denied: pick a Globex ticket) ---"
curl -s -o /tmp/b -w "HTTP %{http_code}\n" -X DELETE "$SUPABASE_URL/rest/v1/tickets?id=eq.4" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $UMA" \
-H "Prefer: return=representation"
cat /tmp/b; echo--- CLEANUP: admin deletes row id=6 ---
HTTP 200
[{"id":6,"org_id":"35d9ad58-5bf0-448f-be83-554f0c27636e","title":"__rls_test_insert_admin__","status":"open"}]
--- UPDATE (admin UMA) ---
HTTP 200
[{"id":1,"org_id":"35d9ad58-5bf0-448f-be83-554f0c27636e","title":"Fix login","status":"in_progress"}]
--- UPDATE (member VIC, should be denied) ---
HTTP 200
[]
--- UPDATE (outsider WEN, should be denied) ---
HTTP 200
[]
--- UPDATE (anon, should be denied) ---
HTTP 200
[]
--- REVERT (admin UMA restores status) ---
HTTP 200
[{"id":1,"org_id":"35d9ad58-5bf0-448f-be83-554f0c27636e","title":"Fix login","status":"open"}]
--- DELETE (member VIC, should be denied) ---
HTTP 200
[]
--- DELETE (outsider WEN, should be denied) ---
HTTP 200
[]
--- DELETE (anon, should be denied) ---
HTTP 200
[]
--- DELETE (admin UMA across orgs should be denied: pick a Globex ticket) ---
HTTP 200
[]
export PGPASSWORD='<redacted:SUPABASE_DB_PASSWORD>' psql "$SUPABASE_DB_URL" -c "SELECT id, org_id, title, status FROM public.tickets ORDER BY id;" psql "$SUPABASE_DB_URL" -c "SELECT count(*) AS ticket_count FROM public.tickets;"
id | org_id | title | status
----+--------------------------------------+----------------+--------
1 | 35d9ad58-5bf0-448f-be83-554f0c27636e | Fix login | open
2 | 35d9ad58-5bf0-448f-be83-554f0c27636e | Update billing | open
3 | 35d9ad58-5bf0-448f-be83-554f0c27636e | Rotate keys | open
4 | 60b9571d-16dc-4358-855a-9b6e0b2c02b2 | Migrate DNS | open
5 | 60b9571d-16dc-4358-855a-9b6e0b2c02b2 | Renew cert | open
(5 rows)
ticket_count
--------------
5
(1 row)
6 earlier narration turn(s) are in the raw transcript.
A shell script executed against the live project and asserted on the state the agent left behind. No model judged another model.
Raw transcript · Task definition and verifier · Every attempt