echo "URL: $SUPABASE_URL" && echo "Ref: $SUPABASE_PROJECT_REF" && echo "DB URL: $SUPABASE_DB_URL" | sed 's/:[^:@]*@/:****@/'
URL: https://<ref>.supabase.co Ref: <ref> DB URL: postgresql://postgres.<ref>:<redacted>@aws-0-us-east-1.pooler.supabase.com:5432/postgres
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" -c " SELECT table_name, table_schema FROM information_schema.tables WHERE table_schema='public' ORDER BY table_name; "
table_name | table_schema -------------+-------------- org_members | public orgs | public tickets | public (3 rows)
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" -c " SELECT * FROM public.orgs ORDER BY id; "
id | name --------------------------------------+-------- 3e836868-a27c-40b2-849d-d261f7b00e67 | Globex 997be617-20e6-41a3-b8c0-c4b5139e45aa | Acme (2 rows)
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" -c " SELECT om.id, om.org_id, o.name as org_name, om.user_id, om.role, u.email FROM public.org_members om JOIN public.orgs o ON om.org_id = o.id JOIN auth.users u ON om.user_id = u.id ORDER BY o.id, om.role; "
ERROR: column om.id does not exist
LINE 2: SELECT om.id, om.org_id, o.name as org_name, om.user_id, om....
^
HINT: Perhaps you meant to reference the column "o.id".
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" -c " SELECT t.id, t.org_id, o.name as org_name, t.title, t.status FROM public.tickets t JOIN public.orgs o ON t.org_id = o.id ORDER BY o.id, t.id; "
id | org_id | org_name | title | status ----+--------------------------------------+----------+----------------+-------- 4 | 3e836868-a27c-40b2-849d-d261f7b00e67 | Globex | Migrate DNS | open 5 | 3e836868-a27c-40b2-849d-d261f7b00e67 | Globex | Renew cert | open 1 | 997be617-20e6-41a3-b8c0-c4b5139e45aa | Acme | Fix login | open 2 | 997be617-20e6-41a3-b8c0-c4b5139e45aa | Acme | Update billing | open 3 | 997be617-20e6-41a3-b8c0-c4b5139e45aa | Acme | Rotate keys | open (5 rows)
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" -c " SELECT column_name, data_type FROM information_schema.columns WHERE table_name='org_members' ORDER BY ordinal_position; "
column_name | data_type -------------+----------- org_id | uuid user_id | uuid role | text (3 rows)
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" -c " SELECT om.org_id, o.name as org_name, om.user_id, om.role, u.email FROM public.org_members om JOIN public.orgs o ON om.org_id = o.id JOIN auth.users u ON om.user_id = u.id ORDER BY o.id, om.role; "
org_id | org_name | user_id | role | email --------------------------------------+----------+--------------------------------------+--------+----------------------- 3e836868-a27c-40b2-849d-d261f7b00e67 | Globex | b2eb31f9-c331-45e9-918b-015a37a5f325 | member | wen-outsider@cari.dev 997be617-20e6-41a3-b8c0-c4b5139e45aa | Acme | 82c877bd-747d-4949-9136-67f62153f9bb | admin | uma-admin@cari.dev 997be617-20e6-41a3-b8c0-c4b5139e45aa | Acme | 8dcc3dcd-8a09-4e60-95ee-6553cafe297e | member | vic-member@cari.dev (3 rows)
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" -c " SELECT column_name, data_type FROM information_schema.columns WHERE table_name='tickets' ORDER BY ordinal_position; "
column_name | data_type -------------+----------- id | bigint org_id | uuid title | text status | text (4 rows)
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" -c " SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check FROM pg_policies WHERE schemaname='public' ORDER BY tablename, cmd; "
schemaname | tablename | policyname | permissive | roles | cmd | qual | with_check ------------+-----------+------------+------------+-------+-----+------+------------ (0 rows)
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" <<'SQL'
-- Enable RLS on all three tables
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;
-- orgs: remain readable by anyone (authenticated or anonymous)
CREATE POLICY orgs_select_policy ON public.orgs FOR SELECT USING (true);
-- org_members: visible ONLY to members of the same org
CREATE POLICY org_members_select_policy ON public.org_members FOR SELECT
USING (EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = org_members.org_id AND om.user_id = auth.uid()
));
-- tickets: SELECT — any member of the org can read
CREATE POLICY tickets_select_policy ON public.tickets FOR SELECT
USING (EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = tickets.org_id AND om.user_id = auth.uid()
));
-- tickets: INSERT — only admins of the org
CREATE POLICY tickets_insert_policy ON public.tickets FOR INSERT
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'
));
-- tickets: UPDATE — only admins of the org
CREATE POLICY tickets_update_policy ON public.tickets FOR UPDATE
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'
));
-- tickets: DELETE — only admins of the org
CREATE POLICY tickets_delete_policy ON public.tickets FOR DELETE
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'
));
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 tablename, policyname, permissive, roles, cmd, qual IS NOT NULL as has_qual, with_check IS NOT NULL as has_withcheck FROM pg_policies WHERE schemaname='public' ORDER BY tablename, cmd; "
tablename | policyname | permissive | roles | cmd | has_qual | has_withcheck
-------------+---------------------------+------------+----------+--------+----------+---------------
org_members | org_members_select_policy | PERMISSIVE | {public} | SELECT | t | f
orgs | orgs_select_policy | PERMISSIVE | {public} | SELECT | t | f
tickets | tickets_delete_policy | PERMISSIVE | {public} | DELETE | t | f
tickets | tickets_insert_policy | PERMISSIVE | {public} | INSERT | f | t
tickets | tickets_select_policy | PERMISSIVE | {public} | SELECT | t | f
tickets | tickets_update_policy | PERMISSIVE | {public} | UPDATE | t | t
(6 rows)
# Sign in as uma-admin@cari.dev (admin of Acme)
RESP=$(curl -s "https://<ref>.supabase.co/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"}')
echo "UMA ADMIN JWT:"
echo "$RESP" | jq -r '.access_token' | head -c 80
echo "..."
UMA_JWT=$(echo "$RESP" | jq -r '.access_token')UMA ADMIN JWT: eyJhbGciOiJFUzI1NiIsImtpZCI6IjEwNTcwMDZhLTY0OGQtNGNjNi04ZTY4LWY1MjY2OTc1NmI1ZiIs...
# Sign in as vic-member@cari.dev (member of Acme)
RESP=$(curl -s "https://<ref>.supabase.co/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"}')
echo "VIC MEMBER JWT:"
echo "$RESP" | jq -r '.access_token' | head -c 80
echo "..."
VIC_JWT=$(echo "$RESP" | jq -r '.access_token')VIC MEMBER JWT: eyJhbGciOiJFUzI1NiIsImtpZCI6IjEwNTcwMDZhLTY0OGQtNGNjNi04ZTY4LWY1MjY2OTc1NmI1ZiIs...
# Sign in as wen-outsider@cari.dev (member of Globex)
RESP=$(curl -s "https://<ref>.supabase.co/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"}')
echo "WEN OUTSIDER JWT:"
echo "$RESP" | jq -r '.access_token' | head -c 80
echo "..."
WEN_JWT=$(echo "$RESP" | jq -r '.access_token')WEN OUTSIDER JWT: eyJhbGciOiJFUzI1NiIsImtpZCI6IjEwNTcwMDZhLTY0OGQtNGNjNi04ZTY4LWY1MjY2OTc1NmI1ZiIs...
echo "=== ANONYMOUS: tickets ===" curl -s "https://<ref>.supabase.co/rest/v1/tickets?select=*" \ -H "apikey: $SUPABASE_ANON_KEY" | jq 'length' echo "=== ANONYMOUS: org_members ===" curl -s "https://<ref>.supabase.co/rest/v1/org_members?select=*" \ -H "apikey: $SUPABASE_ANON_KEY" | jq 'length' echo "=== ANONYMOUS: orgs ===" curl -s "https://<ref>.supabase.co/rest/v1/orgs?select=*" \ -H "apikey: $SUPABASE_ANON_KEY" | jq 'length'
=== ANONYMOUS: tickets === 4 === ANONYMOUS: org_members === 4 === ANONYMOUS: orgs === 2
echo "=== ANONYMOUS: tickets (full) ===" curl -s "https://<ref>.supabase.co/rest/v1/tickets?select=*" \ -H "apikey: $SUPABASE_ANON_KEY" | jq . echo "" echo "=== ANONYMOUS: org_members (full) ===" curl -s "https://<ref>.supabase.co/rest/v1/org_members?select=*" \ -H "apikey: $SUPABASE_ANON_KEY" | jq .
=== ANONYMOUS: tickets (full) ===
{
"code": "42P17",
"details": null,
"hint": null,
"message": "infinite recursion detected in policy for relation \"org_members\""
}
=== ANONYMOUS: org_members (full) ===
{
"code": "42P17",
"details": null,
"hint": null,
"message": "infinite recursion detected in policy for relation \"org_members\""
}
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" <<'SQL'
-- Drop the problematic policy first
DROP POLICY IF EXISTS org_members_select_policy ON public.org_members;
-- Create a security-definer function that bypasses RLS to check membership
CREATE OR REPLACE FUNCTION public.is_org_member(check_org_id uuid)
RETURNS boolean
LANGUAGE sql
SECURITY DEFINER
AS $$
SELECT EXISTS (
SELECT 1 FROM public.org_members
WHERE org_id = check_org_id AND user_id = auth.uid()
);
$$;
-- Recreate org_members policy using the security definer function
CREATE POLICY org_members_select_policy ON public.org_members FOR SELECT
USING (public.is_org_member(org_id));
-- Update tickets policies to use the function too (avoids same recursion risk)
DROP POLICY IF EXISTS tickets_select_policy ON public.tickets;
DROP POLICY IF EXISTS tickets_insert_policy ON public.tickets;
DROP POLICY IF EXISTS tickets_update_policy ON public.tickets;
DROP POLICY IF EXISTS tickets_delete_policy ON public.tickets;
CREATE POLICY tickets_select_policy ON public.tickets FOR SELECT
USING (public.is_org_member(org_id));
CREATE POLICY tickets_insert_policy ON public.tickets FOR INSERT
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'
));
CREATE POLICY tickets_update_policy ON public.tickets FOR UPDATE
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'
));
CREATE POLICY tickets_delete_policy ON public.tickets FOR DELETE
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'
));
SQLDROP POLICY CREATE FUNCTION CREATE POLICY DROP POLICY DROP POLICY DROP POLICY DROP POLICY CREATE POLICY CREATE POLICY CREATE POLICY CREATE POLICY
echo "=== ANONYMOUS: tickets ===" curl -s "https://<ref>.supabase.co/rest/v1/tickets?select=*" \ -H "apikey: $SUPABASE_ANON_KEY" | jq . echo "" echo "=== ANONYMOUS: org_members ===" curl -s "https://<ref>.supabase.co/rest/v1/org_members?select=*" \ -H "apikey: $SUPABASE_ANON_KEY" | jq . echo "" echo "=== ANONYMOUS: orgs ===" curl -s "https://<ref>.supabase.co/rest/v1/orgs?select=*" \ -H "apikey: $SUPABASE_ANON_KEY" | jq .
=== ANONYMOUS: tickets ===
[]
=== ANONYMOUS: org_members ===
[]
=== ANONYMOUS: orgs ===
[
{
"id": "997be617-20e6-41a3-b8c0-c4b5139e45aa",
"name": "Acme"
},
{
"id": "3e836868-a27c-40b2-849d-d261f7b00e67",
"name": "Globex"
}
]
# Get fresh JWTs
UMA_JWT=$(curl -s "https://<ref>.supabase.co/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')
VIC_JWT=$(curl -s "https://<ref>.supabase.co/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')
WEN_JWT=$(curl -s "https://<ref>.supabase.co/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 "=== UMA (admin of Acme): tickets ==="
curl -s "https://<ref>.supabase.co/rest/v1/tickets?select=*" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $UMA_JWT" | jq .
echo ""
echo "=== UMA (admin of Acme): org_members ==="
curl -s "https://<ref>.supabase.co/rest/v1/org_members?select=*" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $UMA_JWT" | jq .=== UMA (admin of Acme): tickets ===
[
{
"id": 1,
"org_id": "997be617-20e6-41a3-b8c0-c4b5139e45aa",
"title": "Fix login",
"status": "open"
},
{
"id": 2,
"org_id": "997be617-20e6-41a3-b8c0-c4b5139e45aa",
"title": "Update billing",
"status": "open"
},
{
"id": 3,
"org_id": "997be617-20e6-41a3-b8c0-c4b5139e45aa",
"title": "Rotate keys",
"status": "open"
}
]
=== UMA (admin of Acme): org_members ===
[
{
"org_id": "997be617-20e6-41a3-b8c0-c4b5139e45aa",
"user_id": "82c877bd-747d-4949-9136-67f62153f9bb",
"role": "admin"
},
{
"org_id": "997be617-20e6-41a3-b8c0-c4b5139e45aa",
"user_id": "8dcc3dcd-8a09-4e60-95ee-6553cafe297e",
"role": "member"
}
]
echo "=== VIC (member of Acme): tickets ===" curl -s "https://<ref>.supabase.co/rest/v1/tickets?select=*" \ -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $VIC_JWT" | jq . echo "" echo "=== VIC (member of Acme): org_members ===" curl -s "https://<ref>.supabase.co/rest/v1/org_members?select=*" \ -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $VIC_JWT" | jq . echo "" echo "=== WEN (member of Globex): tickets ===" curl -s "https://<ref>.supabase.co/rest/v1/tickets?select=*" \ -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $WEN_JWT" | jq . echo "" echo "=== WEN (member of Globex): org_members ===" curl -s "https://<ref>.supabase.co/rest/v1/org_members?select=*" \ -H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $WEN_JWT" | jq .
=== VIC (member of Acme): tickets ===
{
"code": "PGRST301",
"details": null,
"hint": null,
"message": "Empty JWT is sent in Authorization header"
}
=== VIC (member of Acme): org_members ===
{
"code": "PGRST301",
"details": null,
"hint": null,
"message": "Empty JWT is sent in Authorization header"
}
=== WEN (member of Globex): tickets ===
{
"code": "PGRST301",
"details": null,
"hint": null,
"message": "Empty JWT is sent in Authorization header"
}
=== WEN (member of Globex): org_members ===
{
"code": "PGRST301",
"details": null,
"hint": null,
"message": "Empty JWT is sent in Authorization header"
}
# Get all JWTs fresh
UMA_JWT=$(curl -s "https://<ref>.supabase.co/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')
VIC_JWT=$(curl -s "https://<ref>.supabase.co/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')
WEN_JWT=$(curl -s "https://<ref>.supabase.co/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 "=== VIC (member of Acme): tickets ==="
curl -s "https://<ref>.supabase.co/rest/v1/tickets?select=*" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $VIC_JWT" | jq .
echo ""
echo "=== VIC (member of Acme): org_members ==="
curl -s "https://<ref>.supabase.co/rest/v1/org_members?select=*" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $VIC_JWT" | jq .
echo ""
echo "=== WEN (member of Globex): tickets ==="
curl -s "https://<ref>.supabase.co/rest/v1/tickets?select=*" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $WEN_JWT" | jq .
echo ""
echo "=== WEN (member of Globex): org_members ==="
curl -s "https://<ref>.supabase.co/rest/v1/org_members?select=*" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $WEN_JWT" | jq .=== VIC (member of Acme): tickets ===
[
{
"id": 1,
"org_id": "997be617-20e6-41a3-b8c0-c4b5139e45aa",
"title": "Fix login",
"status": "open"
},
{
"id": 2,
"org_id": "997be617-20e6-41a3-b8c0-c4b5139e45aa",
"title": "Update billing",
"status": "open"
},
{
"id": 3,
"org_id": "997be617-20e6-41a3-b8c0-c4b5139e45aa",
"title": "Rotate keys",
"status": "open"
}
]
=== VIC (member of Acme): org_members ===
[
{
"org_id": "997be617-20e6-41a3-b8c0-c4b5139e45aa",
"user_id": "82c877bd-747d-4949-9136-67f62153f9bb",
"role": "admin"
},
{
"org_id": "997be617-20e6-41a3-b8c0-c4b5139e45aa",
"user_id": "8dcc3dcd-8a09-4e60-95ee-6553cafe297e",
"role": "member"
}
]
=== WEN (member of Globex): tickets ===
[
{
"id": 4,
"org_id": "3e836868-a27c-40b2-849d-d261f7b00e67",
"title": "Migrate DNS",
"status": "open"
},
{
"id": 5,
"org_id": "3e836868-a27c-40b2-849d-d261f7b00e67",
"title": "Renew cert",
"status": "open"
}
]
=== WEN (member of Globex): org_members ===
[
{
"org_id": "3e836868-a27c-40b2-849d-d261f7b00e67",
"user_id": "b2eb31f9-c331-45e9-918b-015a37a5f325",
"role": "member"
}
]
UMA_JWT=$(curl -s "https://<ref>.supabase.co/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')
VIC_JWT=$(curl -s "https://<ref>.supabase.co/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')
WEN_JWT=$(curl -s "https://<ref>.supabase.co/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')
ACME_ID="997be617-20e6-41a3-b8c0-c4b5139e45aa"
GLOBEX_ID="3e836868-a27c-40b2-849d-d261f7b00e67"
echo "=== UMA (admin): INSERT ticket into Acme ==="
curl -s -X POST "https://<ref>.supabase.co/rest/v1/tickets" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $UMA_JWT" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d "{\"org_id\":\"$ACME_ID\",\"title\":\"Test admin insert\",\"status\":\"open\"}" | jq .
echo ""
echo "=== VIC (member): INSERT ticket into Acme (should FAIL) ==="
curl -s -X POST "https://<ref>.supabase.co/rest/v1/tickets" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $VIC_JWT" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d "{\"org_id\":\"$ACME_ID\",\"title\":\"Test member insert\",\"status\":\"open\"}" | jq .=== UMA (admin): INSERT ticket into Acme ===
[
{
"id": 6,
"org_id": "997be617-20e6-41a3-b8c0-c4b5139e45aa",
"title": "Test admin insert",
"status": "open"
}
]
=== VIC (member): INSERT ticket into Acme (should FAIL) ===
{
"code": "42501",
"details": null,
"hint": null,
"message": "new row violates row-level security policy for table \"tickets\""
}
UMA_JWT=$(curl -s "https://<ref>.supabase.co/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')
VIC_JWT=$(curl -s "https://<ref>.supabase.co/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')
WEN_JWT=$(curl -s "https://<ref>.supabase.co/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 "=== UMA (admin): UPDATE ticket 1 (Acme) ==="
curl -s -X PATCH "https://<ref>.supabase.co/rest/v1/tickets?id=eq.1" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $UMA_JWT" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d '{"status":"in_progress","title":"Fix login (updated)"}' | jq .
echo ""
echo "=== VIC (member): UPDATE ticket 1 (Acme) — should FAIL ==="
curl -s -X PATCH "https://<ref>.supabase.co/rest/v1/tickets?id=eq.1" \
-H "apikey: $SUPABASE_ANON_KEY" -H "Authorization: Bearer $VIC_JWT" \
-H "Content-Type: application/json" -H "Prefer: return=representation" \
-d '{"status":"hacked"}' | jq .=== UMA (admin): UPDATE ticket 1 (Acme) ===
[
{
"id": 1,
"org_id": "997be617-20e6-41a3-b8c0-c4b5139e45aa",
"title": "Fix login (updated)",
"status": "in_progress"
}
]
=== VIC (member): UPDATE ticket 1 (Acme) — should FAIL ===
[]
12 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