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:0:20}..."
echo "SUPABASE_DB_URL: $SUPABASE_DB_URL"
echo "SUPABASE_DB_PASSWORD: ${SUPABASE_DB_PASSWORD:0:4}..."SUPABASE_URL: https://<ref>.supabase.co SUPABASE_PROJECT_REF: <ref> SUPABASE_ANON_KEY: <redacted:SUPABASE_ANON_KEY> SUPABASE_SERVICE_ROLE_KEY: eyJhbGciOiJIUzI1NiIs... SUPABASE_DB_URL: <redacted:SUPABASE_DB_URL> SUPABASE_DB_PASSWORD: 1a9f...
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" -c "\dt public.*"
List of tables Schema | Name | Type | Owner --------+-------------+-------+---------- public | org_members | table | postgres public | orgs | table | postgres public | tickets | table | postgres (3 rows)
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" -c " SELECT * FROM public.orgs; SELECT * FROM public.org_members; SELECT * FROM public.tickets; "
id | name
--------------------------------------+--------
b5b897ee-1ab9-4266-8adf-a3474c96d24e | Acme
4ff84c79-77f4-489c-9dee-945cc4641c38 | Globex
(2 rows)
org_id | user_id | role
--------------------------------------+--------------------------------------+--------
b5b897ee-1ab9-4266-8adf-a3474c96d24e | 5232152e-4f4e-4b5f-8b04-26512faa2896 | admin
b5b897ee-1ab9-4266-8adf-a3474c96d24e | 0f37551f-77a4-483a-a3b2-14cab6c72321 | member
4ff84c79-77f4-489c-9dee-945cc4641c38 | 634c0673-67d9-437d-b04e-3cd8aac06781 | member
(3 rows)
id | org_id | title | status
----+--------------------------------------+----------------+--------
1 | b5b897ee-1ab9-4266-8adf-a3474c96d24e | Fix login | open
2 | b5b897ee-1ab9-4266-8adf-a3474c96d24e | Update billing | open
3 | b5b897ee-1ab9-4266-8adf-a3474c96d24e | Rotate keys | open
4 | 4ff84c79-77f4-489c-9dee-945cc4641c38 | Migrate DNS | open
5 | 4ff84c79-77f4-489c-9dee-945cc4641c38 | Renew cert | open
(5 rows)
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" -c " SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public'; "
schemaname | tablename | rowsecurity ------------+-------------+------------- public | orgs | f public | org_members | f public | tickets | f (3 rows)
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" -c " SELECT * FROM pg_policies WHERE schemaname = 'public'; "
schemaname | tablename | policyname | permissive | roles | cmd | qual | with_check ------------+-----------+------------+------------+-------+-----+------+------------ (0 rows)
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" -c " SELECT id, email FROM auth.users ORDER BY email; "
id | email --------------------------------------+----------------------- 5232152e-4f4e-4b5f-8b04-26512faa2896 | uma-admin@cari.dev 0f37551f-77a4-483a-a3b2-14cab6c72321 | vic-member@cari.dev 634c0673-67d9-437d-b04e-3cd8aac06781 | wen-outsider@cari.dev (3 rows)
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" <<'SQL'
-- =============================================
-- 1. 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;
-- =============================================
-- 2. TICKETS policies
-- =============================================
-- SELECT: any authenticated member of the ticket's org can read
CREATE POLICY "tickets_select_by_member"
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()
)
);
-- INSERT: only admin members of the target org can insert
CREATE POLICY "tickets_insert_by_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'
)
);
-- UPDATE: only admin members of the ticket's org can update
CREATE POLICY "tickets_update_by_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'
)
);
-- DELETE: only admin members of the ticket's org can delete
CREATE POLICY "tickets_delete_by_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'
)
);
-- =============================================
-- 3. ORG_MEMBERS policies
-- =============================================
-- SELECT: members can see the member list of their own org
CREATE POLICY "org_members_select_own_org"
ON public.org_members
FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = org_members.org_id
AND om.user_id = auth.uid()
)
);
-- =============================================
-- 4. ORGS policies (keep readable)
-- =============================================
-- SELECT: everyone (authenticated + anon) can read the orgs table
CREATE POLICY "orgs_select_all"
ON public.orgs
FOR SELECT
TO authenticated, anon
USING (true);
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, permissive, roles, cmd FROM pg_policies WHERE schemaname = 'public' ORDER BY tablename, policyname; "
schemaname | tablename | policyname | permissive | roles | cmd
------------+-------------+----------------------------+------------+----------------------+--------
public | org_members | org_members_select_own_org | PERMISSIVE | {authenticated} | SELECT
public | orgs | orgs_select_all | PERMISSIVE | {anon,authenticated} | SELECT
public | tickets | tickets_delete_by_admin | PERMISSIVE | {authenticated} | DELETE
public | tickets | tickets_insert_by_admin | PERMISSIVE | {authenticated} | INSERT
public | tickets | tickets_select_by_member | PERMISSIVE | {authenticated} | SELECT
public | tickets | tickets_update_by_admin | PERMISSIVE | {authenticated} | UPDATE
(6 rows)
# Get JWT for uma-admin@cari.dev
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"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
# Get JWT for vic-member@cari.dev
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"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
# Get JWT for wen-outsider@cari.dev
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"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
echo "ADMIN_TOKEN: ${ADMIN_TOKEN:0:30}..."
echo "MEMBER_TOKEN: ${MEMBER_TOKEN:0:30}..."
echo "OUTSIDER_TOKEN: ${OUTSIDER_TOKEN:0:30}..."
# Save to files for reuse
echo "$ADMIN_TOKEN" > /tmp/admin_token
echo "$MEMBER_TOKEN" > /tmp/member_token
echo "$OUTSIDER_TOKEN" > /tmp/outsider_tokenADMIN_TOKEN: eyJhbGciOiJFUzI1NiIsImtpZCI6Ij... MEMBER_TOKEN: eyJhbGciOiJFUzI1NiIsImtpZCI6Ij... OUTSIDER_TOKEN: eyJhbGciOiJFUzI1NiIsImtpZCI6Ij...
ADMIN_TOKEN=$(cat /tmp/admin_token) MEMBER_TOKEN=$(cat /tmp/member_token) OUTSIDER_TOKEN=$(cat /tmp/outsider_token) echo "===== TEST 1: Anonymous access to tickets (should see NONE) =====" curl -s "$SUPABASE_URL/rest/v1/tickets?select=*" \ -H "apikey: $SUPABASE_ANON_KEY" | python3 -m json.tool echo "" echo "===== TEST 2: Admin (uma) reads tickets (should see Acme's 3) =====" curl -s "$SUPABASE_URL/rest/v1/tickets?select=*&order=id" \ -H "apikey: $SUPABASE_ANON_KEY" \ -H "Authorization: Bearer $ADMIN_TOKEN" | python3 -m json.tool echo "" echo "===== TEST 3: Member (vic) reads tickets (should see Acme's 3) =====" curl -s "$SUPABASE_URL/rest/v1/tickets?select=*&order=id" \ -H "apikey: $SUPABASE_ANON_KEY" \ -H "Authorization: Bearer $MEMBER_TOKEN" | python3 -m json.tool echo "" echo "===== TEST 4: Outsider (wen) reads tickets (should see NONE - Globex only, but wait...) =====" curl -s "$SUPABASE_URL/rest/v1/tickets?select=*&order=id" \ -H "apikey: $SUPABASE_ANON_KEY" \ -H "Authorization: Bearer $OUTSIDER_TOKEN" | python3 -m json.tool
===== TEST 1: Anonymous access to tickets (should see NONE) =====
[]
===== TEST 2: Admin (uma) reads tickets (should see Acme's 3) =====
{
"code": "42P17",
"details": null,
"hint": null,
"message": "infinite recursion detected in policy for relation \"org_members\""
}
===== TEST 3: Member (vic) reads tickets (should see Acme's 3) =====
{
"code": "42P17",
"details": null,
"hint": null,
"message": "infinite recursion detected in policy for relation \"org_members\""
}
===== TEST 4: Outsider (wen) reads tickets (should see NONE - Globex only, but wait...) =====
{
"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'
-- Helper function: check if a user belongs to an org (bypasses RLS via SECURITY DEFINER)
CREATE OR REPLACE FUNCTION public.user_belongs_to_org(_org_id uuid, _user_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = _org_id
AND om.user_id = _user_id
);
$$;
-- Helper function: check if a user is an admin of an org (bypasses RLS via SECURITY DEFINER)
CREATE OR REPLACE FUNCTION public.user_is_org_admin(_org_id uuid, _user_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = _org_id
AND om.user_id = _user_id
AND om.role = 'admin'
);
$$;
-- Revoke direct execute from public (only the superuser/definer path should use these)
REVOKE EXECUTE ON FUNCTION public.user_belongs_to_org(uuid, uuid) FROM public;
REVOKE EXECUTE ON FUNCTION public.user_is_org_admin(uuid, uuid) FROM public;
SQLCREATE FUNCTION CREATE FUNCTION REVOKE REVOKE
PGPASSWORD="$SUPABASE_DB_PASSWORD" psql "$SUPABASE_DB_URL" <<'SQL'
-- Grant execute to authenticated and anon so RLS policies can call these
GRANT EXECUTE ON FUNCTION public.user_belongs_to_org(uuid, uuid) TO authenticated, anon;
GRANT EXECUTE ON FUNCTION public.user_is_org_admin(uuid, uuid) TO authenticated, anon;
-- Drop old policies
DROP POLICY IF EXISTS tickets_select_by_member ON public.tickets;
DROP POLICY IF EXISTS tickets_insert_by_admin ON public.tickets;
DROP POLICY IF EXISTS tickets_update_by_admin ON public.tickets;
DROP POLICY IF EXISTS tickets_delete_by_admin ON public.tickets;
DROP POLICY IF EXISTS org_members_select_own_org ON public.org_members;
-- Recreate tickets SELECT policy using security definer function
CREATE POLICY "tickets_select_by_member"
ON public.tickets
FOR SELECT
TO authenticated
USING (
public.user_belongs_to_org(tickets.org_id, auth.uid())
);
-- Recreate tickets INSERT policy
CREATE POLICY "tickets_insert_by_admin"
ON public.tickets
FOR INSERT
TO authenticated
WITH CHECK (
public.user_is_org_admin(tickets.org_id, auth.uid())
);
-- Recreate tickets UPDATE policy
CREATE POLICY "tickets_update_by_admin"
ON public.tickets
FOR UPDATE
TO authenticated
USING (
public.user_is_org_admin(tickets.org_id, auth.uid())
)
WITH CHECK (
public.user_is_org_admin(tickets.org_id, auth.uid())
);
-- Recreate tickets DELETE policy
CREATE POLICY "tickets_delete_by_admin"
ON public.tickets
FOR DELETE
TO authenticated
USING (
public.user_is_org_admin(tickets.org_id, auth.uid())
);
-- Recreate org_members SELECT policy using security definer function
CREATE POLICY "org_members_select_own_org"
ON public.org_members
FOR SELECT
TO authenticated
USING (
public.user_belongs_to_org(org_members.org_id, auth.uid())
);
SQLGRANT GRANT DROP POLICY DROP POLICY DROP POLICY DROP POLICY DROP POLICY 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) echo "===== TEST 1: Anonymous reads tickets (expect empty []) =====" curl -s "$SUPABASE_URL/rest/v1/tickets?select=*&order=id" \ -H "apikey: $SUPABASE_ANON_KEY" | python3 -m json.tool echo "" echo "===== TEST 2: Admin (uma) reads tickets (expect Acme 3) =====" curl -s "$SUPABASE_URL/rest/v1/tickets?select=*&order=id" \ -H "apikey: $SUPABASE_ANON_KEY" \ -H "Authorization: Bearer $ADMIN_TOKEN" | python3 -m json.tool echo "" echo "===== TEST 3: Member (vic) reads tickets (expect Acme 3) =====" curl -s "$SUPABASE_URL/rest/v1/tickets?select=*&order=id" \ -H "apikey: $SUPABASE_ANON_KEY" \ -H "Authorization: Bearer $MEMBER_TOKEN" | python3 -m json.tool echo "" echo "===== TEST 4: Outsider (wen) reads tickets (expect Globex 2) =====" curl -s "$SUPABASE_URL/rest/v1/tickets?select=*&order=id" \ -H "apikey: $SUPABASE_ANON_KEY" \ -H "Authorization: Bearer $OUTSIDER_TOKEN" | python3 -m json.tool
===== TEST 1: Anonymous reads tickets (expect empty []) =====
[]
===== TEST 2: Admin (uma) reads tickets (expect Acme 3) =====
[
{
"id": 1,
"org_id": "b5b897ee-1ab9-4266-8adf-a3474c96d24e",
"title": "Fix login",
"status": "open"
},
{
"id": 2,
"org_id": "b5b897ee-1ab9-4266-8adf-a3474c96d24e",
"title": "Update billing",
"status": "open"
},
{
"id": 3,
"org_id": "b5b897ee-1ab9-4266-8adf-a3474c96d24e",
"title": "Rotate keys",
"status": "open"
}
]
===== TEST 3: Member (vic) reads tickets (expect Acme 3) =====
[
{
"id": 1,
"org_id": "b5b897ee-1ab9-4266-8adf-a3474c96d24e",
"title": "Fix login",
"status": "open"
},
{
"id": 2,
"org_id": "b5b897ee-1ab9-4266-8adf-a3474c96d24e",
"title": "Update billing",
"status": "open"
},
{
"id": 3,
"org_id": "b5b897ee-1ab9-4266-8adf-a3474c96d24e",
"title": "Rotate keys",
"status": "open"
}
]
===== TEST 4: Outsider (wen) reads tickets (expect Globex 2) =====
[
{
"id": 4,
"org_id": "4ff84c79-77f4-489c-9dee-945cc4641c38",
"title": "Migrate DNS",
"status": "open"
},
{
"id": 5,
"org_id": "4ff84c79-77f4-489c-9dee-945cc4641c38",
"title": "Renew cert",
"status": "open"
}
]
ADMIN_TOKEN=$(cat /tmp/admin_token) MEMBER_TOKEN=$(cat /tmp/member_token) OUTSIDER_TOKEN=$(cat /tmp/outsider_token) echo "===== TEST 5: Anonymous reads org_members (expect []) =====" curl -s "$SUPABASE_URL/rest/v1/org_members?select=*&order=user_id" \ -H "apikey: $SUPABASE_ANON_KEY" | python3 -m json.tool echo "" echo "===== TEST 6: Admin (uma) reads org_members (expect Acme's 2 members) =====" curl -s "$SUPABASE_URL/rest/v1/org_members?select=*,orgs(name)&order=user_id" \ -H "apikey: $SUPABASE_ANON_KEY" \ -H "Authorization: Bearer $ADMIN_TOKEN" | python3 -m json.tool echo "" echo "===== TEST 7: Member (vic) reads org_members (expect Acme's 2 members) =====" curl -s "$SUPABASE_URL/rest/v1/org_members?select=*&order=user_id" \ -H "apikey: $SUPABASE_ANON_KEY" \ -H "Authorization: Bearer $MEMBER_TOKEN" | python3 -m json.tool echo "" echo "===== TEST 8: Outsider (wen) reads org_members (expect only Globex's 1 member) =====" curl -s "$SUPABASE_URL/rest/v1/org_members?select=*&order=user_id" \ -H "apikey: $SUPABASE_ANON_KEY" \ -H "Authorization: Bearer $OUTSIDER_TOKEN" | python3 -m json.tool
===== TEST 5: Anonymous reads org_members (expect []) =====
[]
===== TEST 6: Admin (uma) reads org_members (expect Acme's 2 members) =====
[
{
"org_id": "b5b897ee-1ab9-4266-8adf-a3474c96d24e",
"user_id": "0f37551f-77a4-483a-a3b2-14cab6c72321",
"role": "member",
"orgs": {
"name": "Acme"
}
},
{
"org_id": "b5b897ee-1ab9-4266-8adf-a3474c96d24e",
"user_id": "5232152e-4f4e-4b5f-8b04-26512faa2896",
"role": "admin",
"orgs": {
"name": "Acme"
}
}
]
===== TEST 7: Member (vic) reads org_members (expect Acme's 2 members) =====
[
{
"org_id": "b5b897ee-1ab9-4266-8adf-a3474c96d24e",
"user_id": "0f37551f-77a4-483a-a3b2-14cab6c72321",
"role": "member"
},
{
"org_id": "b5b897ee-1ab9-4266-8adf-a3474c96d24e",
"user_id": "5232152e-4f4e-4b5f-8b04-26512faa2896",
"role": "admin"
}
]
===== TEST 8: Outsider (wen) reads org_members (expect only Globex's 1 member) =====
[
{
"org_id": "4ff84c79-77f4-489c-9dee-945cc4641c38",
"user_id": "634c0673-67d9-437d-b04e-3cd8aac06781",
"role": "member"
}
]
ADMIN_TOKEN=$(cat /tmp/admin_token)
MEMBER_TOKEN=$(cat /tmp/member_token)
OUTSIDER_TOKEN=$(cat /tmp/outsider_token)
echo "===== TEST 9: Admin (uma) INSERT ticket into Acme (should SUCCEED) ====="
curl -s -w "\nHTTP %{http_code}" -X POST "$SUPABASE_URL/rest/v1/tickets" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d '{"org_id":"b5b897ee-1ab9-4266-8adf-a3474c96d24e","title":"Admin test ticket","status":"open"}' | python3 -m json.tool
echo ""
echo "===== TEST 10: Member (vic) INSERT ticket into Acme (should FAIL 403) ====="
curl -s -w "\nHTTP %{http_code}" -X POST "$SUPABASE_URL/rest/v1/tickets" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Authorization: Bearer $MEMBER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"org_id":"b5b897ee-1ab9-4266-8adf-a3474c96d24e","title":"Member test ticket","status":"open"}'
echo ""
echo "===== TEST 11: Outsider (wen) INSERT ticket into Acme (should FAIL 403) ====="
curl -s -w "\nHTTP %{http_code}" -X POST "$SUPABASE_URL/rest/v1/tickets" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Authorization: Bearer $OUTSIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"org_id":"b5b897ee-1ab9-4266-8adf-a3474c96d24e","title":"Outsider test ticket","status":"open"}'
echo ""
echo "===== TEST 12: Anonymous INSERT ticket (should FAIL 401) ====="
curl -s -w "\nHTTP %{http_code}" -X POST "$SUPABASE_URL/rest/v1/tickets" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"org_id":"b5b897ee-1ab9-4266-8adf-a3474c96d24e","title":"Anon test ticket","status":"open"}'===== TEST 9: Admin (uma) INSERT ticket into Acme (should SUCCEED) =====
===== TEST 10: Member (vic) INSERT ticket into Acme (should FAIL 403) =====
{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"tickets\""}
HTTP 403
===== TEST 11: Outsider (wen) INSERT ticket into Acme (should FAIL 403) =====
{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"tickets\""}
HTTP 403
===== TEST 12: Anonymous INSERT ticket (should FAIL 401) =====
{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"tickets\""}
HTTP 401Extra data: line 2 column 1 (char 103)
ADMIN_TOKEN=$(cat /tmp/admin_token)
echo "===== Verify admin insert worked - check all Acme tickets ====="
curl -s "$SUPABASE_URL/rest/v1/tickets?select=*&org_id=eq.b5b897ee-1ab9-4266-8adf-a3474c96d24e&order=id" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Authorization: Bearer $ADMIN_TOKEN"
echo ""
echo "===== TEST 13: Admin (uma) UPDATE ticket (should SUCCEED) ====="
curl -s -w "\nHTTP %{http_code}" -X PATCH "$SUPABASE_URL/rest/v1/tickets?id=eq.1" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d '{"status":"closed"}'
echo ""
echo "===== TEST 14: Member (vic) UPDATE ticket (should FAIL 403) ====="
curl -s -w "\nHTTP %{http_code}" -X PATCH "$SUPABASE_URL/rest/v1/tickets?id=eq.2" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Authorization: Bearer $MEMBER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status":"closed"}'
echo ""
echo "===== TEST 15: Outsider (wen) UPDATE Acme ticket (should FAIL 403) ====="
curl -s -w "\nHTTP %{http_code}" -X PATCH "$SUPABASE_URL/rest/v1/tickets?id=eq.1" \
-H "apikey: $SUPABASE_ANON_KEY" \
-H "Authorization: Bearer $OUTSIDER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status":"in_progress"}'===== Verify admin insert worked - check all Acme tickets =====
[{"id":1,"org_id":"b5b897ee-1ab9-4266-8adf-a3474c96d24e","title":"Fix login","status":"open"},
{"id":2,"org_id":"b5b897ee-1ab9-4266-8adf-a3474c96d24e","title":"Update billing","status":"open"},
{"id":3,"org_id":"b5b897ee-1ab9-4266-8adf-a3474c96d24e","title":"Rotate keys","status":"open"},
{"id":6,"org_id":"b5b897ee-1ab9-4266-8adf-a3474c96d24e","title":"Admin test ticket","status":"open"}]
===== TEST 13: Admin (uma) UPDATE ticket (should SUCCEED) =====
[{"id":1,"org_id":"b5b897ee-1ab9-4266-8adf-a3474c96d24e","title":"Fix login","status":"closed"}]
HTTP 200
===== TEST 14: Member (vic) UPDATE ticket (should FAIL 403) =====
{"code":"PGRST301","details":null,"hint":null,"message":"Empty JWT is sent in Authorization header"}
HTTP 401
===== TEST 15: Outsider (wen) UPDATE Acme ticket (should FAIL 403) =====
{"code":"PGRST301","details":null,"hint":null,"message":"Empty JWT is sent in Authorization header"}
HTTP 401# Re-fetch tokens in case they expired
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"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
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"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
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"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
echo "$ADMIN_TOKEN" > /tmp/admin_token
echo "$MEMBER_TOKEN" > /tmp/member_token
echo "$OUTSIDER_TOKEN" > /tmp/outsider_token
echo "Tokens refreshed."
echo "ADMIN: ${#ADMIN_TOKEN} chars"
echo "MEMBER: ${#MEMBER_TOKEN} chars"
echo "OUTSIDER: ${#OUTSIDER_TOKEN} chars"Tokens refreshed. ADMIN: 796 chars MEMBER: 798 chars OUTSIDER: 800 chars
9 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