diff --git a/scripts/cleanup-openfga-member-type.sh b/scripts/cleanup-openfga-member-type.sh new file mode 100755 index 0000000..063bb90 --- /dev/null +++ b/scripts/cleanup-openfga-member-type.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# Copyright The Linux Foundation and each contributor to LFX. +# 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:` 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 "" + +# --------------------------------------------------------------------------- +# 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')" + 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}') + 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: +# +# 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:"}' + +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 ===" diff --git a/scripts/create-membership-auditors-team.sh b/scripts/create-membership-auditors-team.sh deleted file mode 100755 index f11d514..0000000 --- a/scripts/create-membership-auditors-team.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env bash -# Copyright The Linux Foundation and each contributor to LFX. -# SPDX-License-Identifier: MIT -# -# Bootstrap the membership-auditors team in OpenFGA via fga-sync. -# -# This script adds one or more users as members of a team object in OpenFGA. -# It publishes messages to the fga-sync service via NATS using the generic -# member_put subject. The fga-sync service must be running and connected to -# the same NATS server. -# -# The "team" object is implicit in OpenFGA — it is created on the first write. -# -# Usage: -# NATS_URL=nats://localhost:4222 ./scripts/create-membership-auditors-team.sh [ ...] -# -# The values must match the principal claim in the Heimdall JWT -# for the users you want to grant access. Typically this is the OIDC subject -# (e.g. "user@example.com" or a UUID depending on your IdP). -# -# Example: -# ./scripts/create-membership-auditors-team.sh alice@example.com bob@example.com -# -# Environment variables: -# NATS_URL NATS server URL (default: nats://localhost:4222) -# TEAM_ID Team identifier in OpenFGA (default: membership-auditors) - -set -euo pipefail - -NATS_URL="${NATS_URL:-nats://localhost:4222}" -TEAM_ID="${TEAM_ID:-membership-auditors}" - -if [ "$#" -lt 1 ]; then - echo "Usage: $0 [ ...]" - echo "" - echo "Environment variables:" - echo " NATS_URL NATS server URL (default: nats://localhost:4222)" - echo " TEAM_ID Team ID in OpenFGA (default: membership-auditors)" - exit 1 -fi - -if ! command -v nats &>/dev/null; then - echo "Error: 'nats' CLI not found. Install it from https://github.com/nats-io/natscli" - exit 1 -fi - -echo "Team ID: $TEAM_ID" -echo "NATS URL: $NATS_URL" -echo "" - -for principal in "$@"; do - payload=$(printf \ - '{"object_type":"team","operation":"member_put","data":{"uid":"%s","username":"%s","relations":["member"]}}' \ - "$TEAM_ID" \ - "$principal" - ) - echo "Adding principal '$principal' to team '$TEAM_ID'..." - nats pub --server "$NATS_URL" "lfx.fga-sync.member_put" "$payload" -done - -echo "" -echo "Done. Verify with:" -echo " nats pub --server $NATS_URL lfx.fga-sync.member_put '{\"object_type\":\"team\",\"operation\":\"member_put\",...}'"