echo "SUPABASE_URL=$SUPABASE_URL" echo "SUPABASE_PROJECT_REF=$SUPABASE_PROJECT_REF" echo "SUPABASE_ANON_KEY=$SUPABASE_ANON_KEY" echo "SUPABASE_SERVICE_ROLE_KEY=$SUPABASE_SERVICE_ROLE_KEY" echo "SUPABASE_DB_URL=$SUPABASE_DB_URL" echo "SUPABASE_DB_PASSWORD=$SUPABASE_DB_PASSWORD"
SUPABASE_URL=https://<ref>.supabase.co SUPABASE_PROJECT_REF=<ref> SUPABASE_ANON_KEY=<redacted:SUPABASE_ANON_KEY> SUPABASE_SERVICE_ROLE_KEY=<redacted:SUPABASE_SERVICE_ROLE_KEY> SUPABASE_DB_URL=<redacted:SUPABASE_DB_URL> SUPABASE_DB_PASSWORD=<redacted:SUPABASE_DB_PASSWORD>
PGPASSWORD=$SUPABASE_DB_PASSWORD psql "$SUPABASE_DB_URL" -c " -- Check current schema SELECT table_name FROM information_schema.tables WHERE table_schema='public' ORDER BY table_name; "
table_name ------------- org_members orgs tickets (3 rows)
PGPASSWORD=$SUPABASE_DB_PASSWORD psql "$SUPABASE_DB_URL" -c " -- Check existing data SELECT * FROM public.orgs; SELECT * FROM public.org_members; SELECT * FROM public.tickets; "
id | name
--------------------------------------+--------
289956b4-4a68-44fd-85a3-3184599d8827 | Acme
f3633d62-579b-4860-afd5-9a8528ecf153 | Globex
(2 rows)
org_id | user_id | role
--------------------------------------+--------------------------------------+--------
289956b4-4a68-44fd-85a3-3184599d8827 | c5f159dc-947b-4ef1-8dc0-477d954359ba | admin
289956b4-4a68-44fd-85a3-3184599d8827 | c90e81ea-41d9-4309-bd45-02352196b3ee | member
f3633d62-579b-4860-afd5-9a8528ecf153 | e24e3ff1-d8c9-4675-93bf-abf7ffd5cb24 | member
(3 rows)
id | org_id | title | status
----+--------------------------------------+----------------+--------
1 | 289956b4-4a68-44fd-85a3-3184599d8827 | Fix login | open
2 | 289956b4-4a68-44fd-85a3-3184599d8827 | Update billing | open
3 | 289956b4-4a68-44fd-85a3-3184599d8827 | Rotate keys | open
4 | f3633d62-579b-4860-afd5-9a8528ecf153 | Migrate DNS | open
5 | f3633d62-579b-4860-afd5-9a8528ecf153 | Renew cert | open
(5 rows)
PGPASSWORD=$SUPABASE_DB_PASSWORD psql "$SUPABASE_DB_URL" -c " -- Check RLS status and existing policies SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname='public'; SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check FROM pg_policies WHERE schemaname='public'; "
schemaname | tablename | rowsecurity ------------+-------------+------------- public | orgs | f public | org_members | f public | tickets | f (3 rows) schemaname | tablename | policyname | permissive | roles | cmd | qual | with_check ------------+-----------+------------+------------+-------+-----+------+------------ (0 rows)
PGPASSWORD=$SUPABASE_DB_PASSWORD psql "$SUPABASE_DB_URL" -c " -- Check auth users SELECT id, email FROM auth.users ORDER BY email; "
id | email --------------------------------------+----------------------- c5f159dc-947b-4ef1-8dc0-477d954359ba | uma-admin@cari.dev c90e81ea-41d9-4309-bd45-02352196b3ee | vic-member@cari.dev e24e3ff1-d8c9-4675-93bf-abf7ffd5cb24 | wen-outsider@cari.dev (3 rows)
PGPASSWORD=$SUPABASE_DB_PASSWORD psql "$SUPABASE_DB_URL" <<'SQL'
-- ============================================================
-- 1. ENABLE ROW-LEVEL SECURITY on every table
-- ============================================================
ALTER TABLE public.orgs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.org_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.tickets ENABLE ROW LEVEL SECURITY;
-- ============================================================
-- 2. ORGS – readable by everyone (members and anonymous)
-- ============================================================
CREATE POLICY "orgs_select_anyone"
ON public.orgs
FOR SELECT
USING ( true );
-- ============================================================
-- 3. ORG_MEMBERS – only members of the same org can see rows
-- ============================================================
CREATE POLICY "org_members_select_own_org"
ON public.org_members
FOR SELECT
USING (
-- the requesting user must belong to the same org
EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = public.org_members.org_id
AND om.user_id = auth.uid()
)
);
-- ============================================================
-- 4. TICKETS
-- 4a. SELECT – any member of the ticket's org
-- 4b. INSERT/UPDATE/DELETE – only admins of the ticket's org
-- ============================================================
-- 4a. READ: members of the org (any role)
CREATE POLICY "tickets_select_org_members"
ON public.tickets
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = public.tickets.org_id
AND om.user_id = auth.uid()
)
);
-- 4b. WRITE: only admins of the org
CREATE POLICY "tickets_insert_org_admins"
ON public.tickets
FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = public.tickets.org_id
AND om.user_id = auth.uid()
AND om.role = 'admin'
)
);
CREATE POLICY "tickets_update_org_admins"
ON public.tickets
FOR UPDATE
USING (
EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = public.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 = public.tickets.org_id
AND om.user_id = auth.uid()
AND om.role = 'admin'
)
);
CREATE POLICY "tickets_delete_org_admins"
ON public.tickets
FOR DELETE
USING (
EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = public.tickets.org_id
AND om.user_id = auth.uid()
AND om.role = 'admin'
)
);
SQLALTER TABLE ALTER TABLE ALTER TABLE CREATE POLICY CREATE POLICY CREATE POLICY CREATE POLICY CREATE POLICY CREATE POLICY
PGPASSWORD=$SUPABASE_DB_PASSWORD psql "$SUPABASE_DB_URL" -c " SELECT schemaname, tablename, policyname, cmd FROM pg_policies WHERE schemaname='public' ORDER BY tablename, policyname; "
schemaname | tablename | policyname | cmd ------------+-------------+----------------------------+-------- public | org_members | org_members_select_own_org | SELECT public | orgs | orgs_select_anyone | SELECT public | tickets | tickets_delete_org_admins | DELETE public | tickets | tickets_insert_org_admins | INSERT public | tickets | tickets_select_org_members | SELECT public | tickets | tickets_update_org_admins | UPDATE (6 rows)
# Sign in as uma-admin (Acme admin)
ADMIN_TOKEN=$(curl -s -X POST "$SUPABASE_URL/auth/v1/token?grant_type=password" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"uma-admin@cari.dev","password":"Passw0rd-uma-11"}' | jq -r '.access_token')
# Sign in as vic-member (Acme member)
MEMBER_TOKEN=$(curl -s -X POST "$SUPABASE_URL/auth/v1/token?grant_type=password" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"vic-member@cari.dev","password":"Passw0rd-vic-22"}' | jq -r '.access_token')
# Sign in as wen-outsider (Globex member)
OUTSIDER_TOKEN=$(curl -s -X POST "$SUPABASE_URL/auth/v1/token?grant_type=password" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"wen-outsider@cari.dev","password":"Passw0rd-wen-33"}' | jq -r '.access_token')
echo "Admin token length: ${#ADMIN_TOKEN}"
echo "Member token length: ${#MEMBER_TOKEN}"
echo "Outsider token length: ${#OUTSIDER_TOKEN}"
# Save for reuse
echo "$ADMIN_TOKEN" > /tmp/admin_token
echo "$MEMBER_TOKEN" > /tmp/member_token
echo "$OUTSIDER_TOKEN" > /tmp/outsider_tokenAdmin token length: 796 Member token length: 798 Outsider token length: 800
ADMIN_TOKEN=$(cat /tmp/admin_token)
MEMBER_TOKEN=$(cat /tmp/member_token)
OUTSIDER_TOKEN=$(cat /tmp/outsider_token)
API="https://<ref>.supabase.co/rest/v1"
PASS=0
FAIL=0
check() {
local label="$1" expect="$2" actual="$3"
if echo "$actual" | jq -e "select($expect)" > /dev/null 2>&1; then
echo " ✅ $label"
PASS=$((PASS+1))
else
echo " ❌ $label (got: $actual)"
FAIL=$((FAIL+1))
fi
}
echo "============================================"
echo "TEST GROUP 1 – TICKETS"
echo "============================================"
echo "--- Admin (Acme) reads Acme tickets ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $ADMIN_TOKEN" \
"$API/tickets?org_id=eq.289956b4-4a68-44fd-85a3-3184599d8827&select=id,title,org_id" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "admin sees 3 Acme tickets" '(. | length) == 3' "$BODY"
check "HTTP 200" "$CODE == 200" "$CODE"
echo "--- Admin (Acme) reads Globex tickets (should see 0) ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $ADMIN_TOKEN" \
"$API/tickets?org_id=eq.f3633d62-579b-4860-afd5-9a8528ecf153&select=id" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "admin sees 0 Globex tickets" '(. | length) == 0' "$BODY"
echo "--- Member (Acme) reads Acme tickets ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $MEMBER_TOKEN" \
"$API/tickets?org_id=eq.289956b4-4a68-44fd-85a3-3184599d8827&select=id,title" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "member sees 3 Acme tickets" '(. | length) == 3' "$BODY"
check "HTTP 200" "$CODE == 200" "$CODE"
echo "--- Outsider (Globex) reads Acme tickets (should see 0) ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $OUTSIDER_TOKEN" \
"$API/tickets?org_id=eq.289956b4-4a68-44fd-85a3-3184599d8827&select=id" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "outsider sees 0 Acme tickets" '(. | length) == 0' "$BODY"
echo "--- Outsider (Globex) reads Globex tickets ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $OUTSIDER_TOKEN" \
"$API/tickets?org_id=eq.f3633d62-579b-4860-afd5-9a8528ecf153&select=id,title" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "outsider sees 2 Globex tickets" '(. | length) == 2' "$BODY"
check "HTTP 200" "$CODE == 200" "$CODE"
echo "--- Anonymous reads tickets (should see 0) ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" \
"$API/tickets?select=id" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "anonymous sees 0 tickets" '(. | length) == 0' "$BODY"
echo ""
echo "============================================"
echo "TEST GROUP 2 – ORG_MEMBERS"
echo "============================================"
echo "--- Admin (Acme) reads Acme members ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $ADMIN_TOKEN" \
"$API/org_members?org_id=eq.289956b4-4a68-44fd-85a3-3184599d8827&select=user_id,role" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "admin sees 2 Acme members" '(. | length) == 2' "$BODY"
check "HTTP 200" "$CODE == 200" "$CODE"
echo "--- Member (Acme) reads Acme members ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $MEMBER_TOKEN" \
"$API/org_members?org_id=eq.289956b4-4a68-44fd-85a3-3184599d8827&select=user_id,role" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "member sees 2 Acme members" '(. | length) == 2' "$BODY"
echo "--- Outsider reads Acme members (should see 0) ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $OUTSIDER_TOKEN" \
"$API/org_members?org_id=eq.289956b4-4a68-44fd-85a3-3184599d8827&select=user_id" \
-w "\n%{http_code}")
BODY=$(echo "$R============================================
TEST GROUP 1 – TICKETS
============================================
--- Admin (Acme) reads Acme tickets ---
❌ admin sees 3 Acme tickets (got: {"code":"42P17","details":null,"hint":null,"message":"infinite recursion detected in policy for relation \"org_members\""})
❌ HTTP 200 (got: 500)
--- Admin (Acme) reads Globex tickets (should see 0) ---
❌ admin sees 0 Globex tickets (got: {"code":"42P17","details":null,"hint":null,"message":"infinite recursion detected in policy for relation \"org_members\""})
--- Member (Acme) reads Acme tickets ---
❌ member sees 3 Acme tickets (got: {"code":"42P17","details":null,"hint":null,"message":"infinite recursion detected in policy for relation \"org_members\""})
❌ HTTP 200 (got: 500)
--- Outsider (Globex) reads Acme tickets (should see 0) ---
❌ outsider sees 0 Acme tickets (got: {"code":"42P17","details":null,"hint":null,"message":"infinite recursion detected in policy for relation \"org_members\""})
--- Outsider (Globex) reads Globex tickets ---
❌ outsider sees 2 Globex tickets (got: {"code":"42P17","details":null,"hint":null,"message":"infinite recursion detected in policy for relation \"org_members\""})
❌ HTTP 200 (got: 500)
--- Anonymous reads tickets (should see 0) ---
❌ anonymous sees 0 tickets (got: {"code":"42P17","details":null,"hint":null,"message":"infinite recursion detected in policy for relation \"org_members\""})
============================================
TEST GROUP 2 – ORG_MEMBERS
============================================
--- Admin (Acme) reads Acme members ---
❌ admin sees 2 Acme members (got: {"code":"42P17","details":null,"hint":null,"message":"infinite recursion detected in policy for relation \"org_members\""})
❌ HTTP 200 (got: 500)
--- Member (Acme) reads Acme members ---
❌ member sees 2 Acme members (got: {"code":"42P17","details":null,"hint":null,"message":"infinite recursion detected in policy for relation \"org_members\""})
--- Outsider reads Acme members (should see 0) ---
❌ outsider sees 0 Acme members (got: {"code":"42P17","details":null,"hint":null,"message":"infinite recursion detected in policy for relation \"org_members\""})
--- Outsider reads Globex members (should see 1, self) ---
❌ outsider sees 1 Globex member (got: {"code":"42P17","details":null,"hint":null,"message":"infinite recursion detected in policy for relation \"org_members\""})
❌ HTTP 200 (got: 500)
--- Anonymous reads org_members (should see 0) ---
❌ anonymous sees 0 members (got: {"code":"42P17","details":null,"hint":null,"message":"infinite recursion detected in policy for relation \"org_members\""})
============================================
TEST GROUP 3 – ORGS (public read)
============================================
--- Admin reads orgs ---
✅ admin sees 2 orgs
--- Anonymous reads orgs ---
✅ anonymous sees 2 orgs
============================================
TEST GROUP 4 – WRITE OPERATIONS
============================================
--- Admin inserts ticket into Acme (should succeed) ---
❌ admin insert succeeds (got: {"code":"42P17","details":null,"hint":null,"message":"infinite recursion detected in policy for relation \"org_members\""})
❌ HTTP 201 (got: 500)
Created ticket id=
--- Member inserts ticket into Acme (should fail 403) ---
❌ member insert blocked (403) (got: 500)
--- Outsider inserts ticket into Acme (should fail) ---
❌ outsider insert blocked (got: 500)
--- Admin updates ticket in Acme (should succeed) ---
❌ admin update succeeds (got: {"code":"42P17","details":null,"hint":null,"message":"infinite recursion detected in policy for relation \"org_members\""})
--- Admin deletes ticket in Acme (should succeed) ---
❌ admin delete succeeds (200) (got: 500)
--- Admin updates ticket in Globex (should fail) ---
❌ admin cannot update Globex ticket (got: 500)
============================================
VERIFICATION – DATA UNTOUCHED
==========================jq: error (at <stdin>:1): Cannot index object with number
PGPASSWORD=$SUPABASE_DB_PASSWORD psql "$SUPABASE_DB_URL" <<'SQL'
-- Drop existing policies that cause recursion
DROP POLICY IF EXISTS "org_members_select_own_org" ON public.org_members;
DROP POLICY IF EXISTS "tickets_select_org_members" ON public.tickets;
DROP POLICY IF EXISTS "tickets_insert_org_admins" ON public.tickets;
DROP POLICY IF EXISTS "tickets_update_org_admins" ON public.tickets;
DROP POLICY IF EXISTS "tickets_delete_org_admins" ON public.tickets;
-- SECURITY DEFINER helper functions (bypass RLS to avoid recursion)
CREATE OR REPLACE FUNCTION public.check_org_membership(check_org_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT EXISTS (
SELECT 1 FROM public.org_members
WHERE org_id = check_org_id
AND user_id = auth.uid()
);
$$;
CREATE OR REPLACE FUNCTION public.check_org_admin(check_org_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT EXISTS (
SELECT 1 FROM public.org_members
WHERE org_id = check_org_id
AND user_id = auth.uid()
AND role = 'admin'
);
$$;
-- Re-create ORG_MEMBERS policy using the helper
CREATE POLICY "org_members_select_own_org"
ON public.org_members
FOR SELECT
USING (public.check_org_membership(org_members.org_id));
-- Re-create TICKETS policies using the helpers
CREATE POLICY "tickets_select_org_members"
ON public.tickets
FOR SELECT
USING (public.check_org_membership(tickets.org_id));
CREATE POLICY "tickets_insert_org_admins"
ON public.tickets
FOR INSERT
WITH CHECK (public.check_org_admin(tickets.org_id));
CREATE POLICY "tickets_update_org_admins"
ON public.tickets
FOR UPDATE
USING (public.check_org_admin(tickets.org_id))
WITH CHECK (public.check_org_admin(tickets.org_id));
CREATE POLICY "tickets_delete_org_admins"
ON public.tickets
FOR DELETE
USING (public.check_org_admin(tickets.org_id));
SQLDROP POLICY DROP POLICY DROP POLICY DROP POLICY DROP POLICY CREATE FUNCTION CREATE FUNCTION CREATE POLICY CREATE POLICY CREATE POLICY CREATE POLICY CREATE POLICY
ADMIN_TOKEN=$(cat /tmp/admin_token)
MEMBER_TOKEN=$(cat /tmp/member_token)
OUTSIDER_TOKEN=$(cat /tmp/outsider_token)
API="https://<ref>.supabase.co/rest/v1"
PASS=0
FAIL=0
check() {
local label="$1" expect="$2" actual="$3"
if echo "$actual" | jq -e "select($expect)" > /dev/null 2>&1; then
echo " ✅ $label"
PASS=$((PASS+1))
else
echo " ❌ $label (got: $(echo "$actual" | head -c 200))"
FAIL=$((FAIL+1))
fi
}
echo "============================================"
echo "TEST GROUP 1 – TICKETS (READ)"
echo "============================================"
echo "--- Admin (Acme) reads Acme tickets ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $ADMIN_TOKEN" \
"$API/tickets?org_id=eq.289956b4-4a68-44fd-85a3-3184599d8827&select=id,title,org_id" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "admin sees 3 Acme tickets" '(. | length) == 3' "$BODY"
check "HTTP 200" "$CODE == 200" "$CODE"
echo "--- Admin (Acme) reads Globex tickets (should see 0) ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $ADMIN_TOKEN" \
"$API/tickets?org_id=eq.f3633d62-579b-4860-afd5-9a8528ecf153&select=id" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "admin sees 0 Globex tickets" '(. | length) == 0' "$BODY"
echo "--- Member (Acme) reads Acme tickets ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $MEMBER_TOKEN" \
"$API/tickets?org_id=eq.289956b4-4a68-44fd-85a3-3184599d8827&select=id,title" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "member sees 3 Acme tickets" '(. | length) == 3' "$BODY"
check "HTTP 200" "$CODE == 200" "$CODE"
echo "--- Outsider (Globex) reads Acme tickets (should see 0) ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $OUTSIDER_TOKEN" \
"$API/tickets?org_id=eq.289956b4-4a68-44fd-85a3-3184599d8827&select=id" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "outsider sees 0 Acme tickets" '(. | length) == 0' "$BODY"
echo "--- Outsider (Globex) reads Globex tickets ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $OUTSIDER_TOKEN" \
"$API/tickets?org_id=eq.f3633d62-579b-4860-afd5-9a8528ecf153&select=id,title" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "outsider sees 2 Globex tickets" '(. | length) == 2' "$BODY"
check "HTTP 200" "$CODE == 200" "$CODE"
echo "--- Anonymous reads tickets (should see 0) ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" \
"$API/tickets?select=id" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "anonymous sees 0 tickets" '(. | length) == 0' "$BODY"
echo ""
echo "============================================"
echo "TEST GROUP 2 – ORG_MEMBERS (READ)"
echo "============================================"
echo "--- Admin (Acme) reads Acme members ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $ADMIN_TOKEN" \
"$API/org_members?org_id=eq.289956b4-4a68-44fd-85a3-3184599d8827&select=user_id,role" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "admin sees 2 Acme members" '(. | length) == 2' "$BODY"
check "HTTP 200" "$CODE == 200" "$CODE"
echo "--- Member (Acme) reads Acme members ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $MEMBER_TOKEN" \
"$API/org_members?org_id=eq.289956b4-4a68-44fd-85a3-3184599d8827&select=user_id,role" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "member sees 2 Acme members" '(. | length) == 2' "$BODY"
check "HTTP 200" "$CODE == 200" "$CODE"
echo "--- Outsider reads Acme members (should see 0) ---"
R=$(curl -s -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $OUTSIDER_TOKEN" \
"$API/org_members?org_id=eq.289956b4-4a68-4============================================ TEST GROUP 1 – TICKETS (READ) ============================================ --- Admin (Acme) reads Acme tickets --- ✅ admin sees 3 Acme tickets ✅ HTTP 200 --- Admin (Acme) reads Globex tickets (should see 0) --- ✅ admin sees 0 Globex tickets --- Member (Acme) reads Acme tickets --- ✅ member sees 3 Acme tickets ✅ HTTP 200 --- Outsider (Globex) reads Acme tickets (should see 0) --- ✅ outsider sees 0 Acme tickets --- Outsider (Globex) reads Globex tickets --- ✅ outsider sees 2 Globex tickets ✅ HTTP 200 --- Anonymous reads tickets (should see 0) --- ✅ anonymous sees 0 tickets ============================================ TEST GROUP 2 – ORG_MEMBERS (READ) ============================================ --- Admin (Acme) reads Acme members --- ✅ admin sees 2 Acme members ✅ HTTP 200 --- Member (Acme) reads Acme members --- ✅ member sees 2 Acme members ✅ HTTP 200 --- Outsider reads Acme members (should see 0) --- ✅ outsider sees 0 Acme members --- Outsider reads Globex members (should see 1, self) --- ✅ outsider sees 1 Globex member ✅ HTTP 200 --- Anonymous reads org_members (should see 0) --- ✅ anonymous sees 0 members ============================================ TEST GROUP 3 – ORGS (public read) ============================================ --- Admin reads orgs --- ✅ admin sees 2 orgs --- Anonymous reads orgs --- ✅ anonymous sees 2 orgs ============================================ TEST GROUP 4 – WRITE OPERATIONS ============================================ --- Admin inserts ticket into Acme (should succeed) --- ✅ admin insert succeeds ✅ HTTP 201 Created ticket id=6 --- Admin inserts ticket into Globex (should fail) --- ✅ admin cannot insert into Globex (403) --- Member inserts ticket into Acme (should fail 403) --- ✅ member insert blocked (403) --- Outsider inserts ticket into Acme (should fail) --- ✅ outsider insert blocked --- Admin updates ticket in Acme (should succeed) --- ✅ admin update succeeds ✅ HTTP 200 --- Admin deletes ticket in Acme (should succeed) --- ❌ admin delete succeeds (200) (got: 204) --- Member updates ticket in Acme (should fail) --- ❌ member update blocked (got: 204) --- Admin updates ticket in Globex (should fail) --- ❌ admin cannot update Globex ticket (got: 204) ============================================ VERIFICATION – DATA UNTOUCHED ============================================ ✅ still 5 tickets in DB ============================================ RESULTS: 27 passed, 3 failed ============================================
ADMIN_TOKEN=$(cat /tmp/admin_token)
MEMBER_TOKEN=$(cat /tmp/member_token)
OUTSIDER_TOKEN=$(cat /tmp/outsider_token)
API="https://<ref>.supabase.co/rest/v1"
PASS=0
FAIL=0
check() {
local label="$1" expect="$2" actual="$3"
if echo "$actual" | jq -e "select($expect)" > /dev/null 2>&1; then
echo " ✅ $label"
PASS=$((PASS+1))
else
echo " ❌ $label (got: $(echo "$actual" | head -c 200))"
FAIL=$((FAIL+1))
fi
}
echo "============================================"
echo "CORRECTED TEST GROUP 4 – WRITE OPERATIONS"
echo "============================================"
echo "--- Admin inserts ticket into Acme (should succeed) ---"
R=$(curl -s -X POST -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
"$API/tickets" \
-d '{"org_id":"289956b4-4a68-44fd-85a3-3184599d8827","title":"Test from admin","status":"open"}' \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "admin insert succeeds (201)" "$CODE == 201" "$CODE"
TICKET_ID=$(echo "$BODY" | jq -r '.[0].id')
echo " Created ticket id=$TICKET_ID"
echo "--- Admin inserts ticket into Globex (should be blocked by RLS) ---"
R=$(curl -s -X POST -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
"$API/tickets" \
-d '{"org_id":"f3633d62-579b-4860-afd5-9a8528ecf153","title":"Should fail","status":"open"}' \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "admin cannot insert into Globex (0 rows returned)" '(. | length) == 0' "$BODY"
echo "--- Member inserts ticket into Acme (should be blocked) ---"
R=$(curl -s -X POST -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $MEMBER_TOKEN" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
"$API/tickets" \
-d '{"org_id":"289956b4-4a68-44fd-85a3-3184599d8827","title":"Should fail","status":"open"}' \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "member insert blocked (0 rows returned)" '(. | length) == 0' "$BODY"
echo "--- Outsider inserts ticket into Acme (should be blocked) ---"
R=$(curl -s -X POST -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $OUTSIDER_TOKEN" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
"$API/tickets" \
-d '{"org_id":"289956b4-4a68-44fd-85a3-3184599d8827","title":"Should fail too","status":"open"}' \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "outsider insert blocked (0 rows returned)" '(. | length) == 0' "$BODY"
echo "--- Admin updates own Acme ticket (should succeed) ---"
R=$(curl -s -X PATCH -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
"$API/tickets?id=eq.$TICKET_ID" \
-d '{"title":"Test from admin - updated"}' \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "admin update succeeds" '.[0].title == "Test from admin - updated"' "$BODY"
echo "--- Admin deletes own Acme ticket (should succeed) ---"
R=$(curl -s -X DELETE -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Prefer: return=representation" \
"$API/tickets?id=eq.$TICKET_ID" \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
check "admin delete succeeds (200 or 204)" "$CODE == 200 or $CODE == 204" "$CODE"
echo "--- Member updates ticket in Acme (should be blocked) ---"
R=$(curl -s -X PATCH -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $MEMBER_TOKEN" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
"$API/tickets?id=eq.1" \
-d '{"title":"Hacked by member"}' \
-w "\n%{http_code}")
BODY=$(echo "$R" | head -n -1); CODE=$(echo "$R" | tail -1)
chec============================================
CORRECTED TEST GROUP 4 – WRITE OPERATIONS
============================================
--- Admin inserts ticket into Acme (should succeed) ---
✅ admin insert succeeds (201)
Created ticket id=10
--- Admin inserts ticket into Globex (should be blocked by RLS) ---
❌ admin cannot insert into Globex (0 rows returned) (got: {"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"tickets\""})
--- Member inserts ticket into Acme (should be blocked) ---
❌ member insert blocked (0 rows returned) (got: {"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"tickets\""})
--- Outsider inserts ticket into Acme (should be blocked) ---
❌ outsider insert blocked (0 rows returned) (got: {"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"tickets\""})
--- Admin updates own Acme ticket (should succeed) ---
✅ admin update succeeds
--- Admin deletes own Acme ticket (should succeed) ---
✅ admin delete succeeds (200 or 204)
--- Member updates ticket in Acme (should be blocked) ---
✅ member update blocked (0 rows returned)
✅ HTTP 200 (empty)
--- Verify ticket 1 unchanged ---
✅ ticket 1 still titled 'Fix login'
--- Admin updates Globex ticket (should be blocked) ---
✅ admin cannot update Globex ticket (0 rows)
--- Verify ticket 4 unchanged ---
❌ ticket 4 still titled 'Migrate DNS' (got: [])
============================================
VERIFICATION – DATA UNTOUCHED (DB direct)
============================================
id | title | org_id
----+----------------+--------------------------------------
1 | Fix login | 289956b4-4a68-44fd-85a3-3184599d8827
2 | Update billing | 289956b4-4a68-44fd-85a3-3184599d8827
3 | Rotate keys | 289956b4-4a68-44fd-85a3-3184599d8827
4 | Migrate DNS | f3633d62-579b-4860-afd5-9a8528ecf153
5 | Renew cert | f3633d62-579b-4860-afd5-9a8528ecf153
(5 rows)
RESULTS: 7 passed, 4 failed
============================================
# Clean up the test ticket PGPASSWORD=$SUPABASE_DB_PASSWORD psql "$SUPABASE_DB_URL" -c "DELETE FROM public.tickets WHERE id = 10;" PGPASSWORD=$SUPABASE_DB_PASSWORD psql "$SUPABASE_DB_URL" -c "SELECT count(*) FROM public.tickets;"
DELETE 0
count
-------
5
(1 row)
8 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