Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions scripts/cleanup-openfga-member-type.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#!/usr/bin/env bash
# Copyright The Linux Foundation and each contributor to LFX.
Comment thread
emsearcy marked this conversation as resolved.
# SPDX-License-Identifier: MIT
#
# cleanup-openfga-member-type.sh
#
# Deletes all tuples associated with the now-removed `member` OpenFGA type:
# 1. All `team:membership-auditors#member -> auditor -> member:<uuid>` tuples
# 2. All `user:* -> member -> team:membership-auditors` tuples
#
# This is the inverse of scripts/create-membership-auditors-team.sh, which is
# also being removed as part of this cleanup.
#
# Prerequisites:
# - kubectl port-forward to the target OpenFGA on localhost:8080
# - jq installed
#
# Usage:
# ./scripts/cleanup-openfga-member-type.sh [--dry-run]

set -euo pipefail

BASE_URL="${OPENFGA_URL:-http://localhost:8080}"
STORE_ID="${OPENFGA_STORE_ID:-01K3S60BS505DDR3VF9RAZDVHG}"
BATCH_SIZE=100
DRY_RUN=false

if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=true
echo "=== DRY RUN MODE — no deletions will be performed ==="
fi

echo "Store: $STORE_ID"
echo "Base URL: $BASE_URL"
echo ""

Comment thread
emsearcy marked this conversation as resolved.
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

# Send a batch delete to /write; on dry-run just print the tuples
delete_batch() {
local tuples_json="$1"
local count
count=$(echo "$tuples_json" | jq 'length')

if [[ "$DRY_RUN" == true ]]; then
echo "[DRY RUN] Would delete $count tuples:"
echo "$tuples_json" | jq -r '.[] | " \(.user) -> \(.relation) -> \(.object)"'
return
fi

local payload
payload=$(jq -n --argjson keys "$tuples_json" '{"deletes": {"tuple_keys": $keys}}')

local resp
resp=$(curl -s -X POST "${BASE_URL}/stores/${STORE_ID}/write" \
-H 'Content-Type: application/json' \
-d "$payload")

# OpenFGA returns an empty body on success, or a JSON error object on failure
if echo "$resp" | jq -e '.code' >/dev/null 2>&1; then
echo "ERROR deleting batch: $(echo "$resp" | jq -r '.message')"
Comment on lines +56 to +63
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both /write and /read calls use curl -s without failing on non-2xx HTTP responses. If OpenFGA returns an HTTP error with an empty/non-JSON body (or a proxy returns HTML), the .code check can be bypassed and the script may incorrectly treat the operation as successful. Use curl --fail-with-body (or -f plus capturing the status code) and validate that the response is valid JSON before parsing error fields.

Suggested change
local resp
resp=$(curl -s -X POST "${BASE_URL}/stores/${STORE_ID}/write" \
-H 'Content-Type: application/json' \
-d "$payload")
# OpenFGA returns an empty body on success, or a JSON error object on failure
if echo "$resp" | jq -e '.code' >/dev/null 2>&1; then
echo "ERROR deleting batch: $(echo "$resp" | jq -r '.message')"
local http_status
local resp
local http_output
http_output=$(curl -sS -w '%{http_code}' -X POST "${BASE_URL}/stores/${STORE_ID}/write" \
-H 'Content-Type: application/json' \
-d "$payload") || {
echo "ERROR deleting batch: HTTP request to OpenFGA failed"
echo "Raw response:"
echo "$http_output"
exit 1
}
http_status=${http_output: -3}
resp=${http_output::-3}
# Treat any non-2xx HTTP status as an error
if [[ "$http_status" != 2?? ]]; then
echo "ERROR deleting batch: OpenFGA returned HTTP status $http_status"
if [[ -n "$resp" ]] && echo "$resp" | jq -e . >/dev/null 2>&1; then
# Try to surface a JSON error message if present
if echo "$resp" | jq -e '.message' >/dev/null 2>&1; then
echo "Message: $(echo "$resp" | jq -r '.message')"
else
echo "Response body (JSON):"
echo "$resp" | jq .
fi
elif [[ -n "$resp" ]]; then
echo "Response body (non-JSON):"
echo "$resp"
fi
exit 1
fi
# OpenFGA returns an empty body on success, or a JSON error object on failure.
# If a JSON body with a .code field is present, treat it as an error.
if [[ -n "$resp" ]] && echo "$resp" | jq -e . >/dev/null 2>&1 && \
echo "$resp" | jq -e '.code' >/dev/null 2>&1; then
echo "ERROR deleting batch: $(echo "$resp" | jq -r '.message // "Unknown error"')"

Copilot uses AI. Check for mistakes.
echo "Failing batch:"
echo "$tuples_json" | jq .
exit 1
fi

echo " Deleted $count tuples"
}

# Paginate through /read using the given tuple_key JSON fragment, collecting all
# results into the global variable COLLECTED_TUPLES as a JSON array of
# {user, relation, object} objects.
collect_tuples() {
local filter_json="$1"
COLLECTED_TUPLES="[]"
local token=""
local page=0

while true; do
local body=""
if [[ -z "$token" ]]; then
body=$(jq -n --argjson tk "$filter_json" '{"tuple_key": $tk, "page_size": 100}')
else
body=$(jq -n --argjson tk "$filter_json" --arg ct "$token" \
'{"tuple_key": $tk, "page_size": 100, "continuation_token": $ct}')
Comment thread
emsearcy marked this conversation as resolved.
fi

local resp
resp=$(curl -s -X POST "${BASE_URL}/stores/${STORE_ID}/read" \
-H 'Content-Type: application/json' \
-d "$body")

if echo "$resp" | jq -e '.code' >/dev/null 2>&1; then
echo "ERROR reading tuples: $(echo "$resp" | jq -r '.message')"
exit 1
fi

local batch
batch=$(echo "$resp" | jq '[.tuples[] | {user: .key.user, relation: .key.relation, object: .key.object}]')
local batch_count
batch_count=$(echo "$batch" | jq 'length')
page=$((page + 1))

COLLECTED_TUPLES=$(printf '%s\n%s' "$COLLECTED_TUPLES" "$batch" | jq -s 'add')
echo " Page $page: $batch_count tuples (running total: $(echo "$COLLECTED_TUPLES" | jq 'length'))"

token=$(echo "$resp" | jq -r '.continuation_token // ""')
[[ -z "$token" ]] && break
done
}

# Flush COLLECTED_TUPLES to the API in batches of BATCH_SIZE
delete_all_collected() {
local total
total=$(echo "$COLLECTED_TUPLES" | jq 'length')
echo " Total to delete: $total"
[[ "$total" -eq 0 ]] && return

local offset=0
while [[ $offset -lt $total ]]; do
local batch
batch=$(echo "$COLLECTED_TUPLES" | jq --argjson o "$offset" --argjson s "$BATCH_SIZE" '.[$o:$o+$s]')
delete_batch "$batch"
offset=$((offset + BATCH_SIZE))
done
}

# ---------------------------------------------------------------------------
# Step 1: Delete all member:* object tuples
# team:membership-auditors#member -> auditor -> member:<uuid>
#
# The /read API requires either a full object ID or both a user and an object
# type prefix. We use user + object-type-prefix to page through all of them.
# ---------------------------------------------------------------------------

echo "=== Step 1: Collecting all member:* object tuples ==="
collect_tuples '{"user": "team:membership-auditors#member", "object": "member:"}'
Comment thread
emsearcy marked this conversation as resolved.

echo ""
echo "=== Step 1: Deleting ==="
delete_all_collected

echo ""
echo "=== Step 1 complete ==="
echo ""

# ---------------------------------------------------------------------------
# Step 2: Delete all user -> member -> team:membership-auditors tuples
# ---------------------------------------------------------------------------

echo "=== Step 2: Collecting user memberships of team:membership-auditors ==="
collect_tuples '{"object": "team:membership-auditors", "relation": "member"}'

echo ""
echo " Users found:"
echo "$COLLECTED_TUPLES" | jq -r '.[] | " \(.user)"'

echo ""
echo "=== Step 2: Deleting ==="
delete_all_collected

echo ""
echo "=== Step 2 complete ==="
echo ""

echo "=== All done ==="
63 changes: 0 additions & 63 deletions scripts/create-membership-auditors-team.sh

This file was deleted.

Loading