#!/bin/sh
# Moves MKE3 UI team/org grants that an affected MKE3->MKE4 migration
# (BOP-7096) left on users named "<org>" or "<org>-<team>" back onto the
# matching organization or team, using only the MKE4 accounts API.
#
# Dry-run by default; pass --apply to change the cluster. Safe to re-run.
#
# Usage:
#   MKE4_ADDR=<host[:port]> TOKEN=<id_token> ./recover.sh [--apply]
#   API=<base url, e.g. https://host/api/auth/v1> TOKEN_FILE=<path> ./recover.sh [--apply]
#
# TOKEN / TOKEN_FILE: id_token of an MKE4 admin (e.g. from the MKE4 dashboard login).
#
# Env:
#   WORKERS      parallel moves (default: 8)
#   EXCLUDE      space-separated user names that are real users; left alone
#   INSECURE=1   skip TLS verification (curl -k)
#   VERBOSE=1    print every planned move
#
# Requires: curl, jq, xargs.
set -eu

# Worker mode: move one grant. Invoked by xargs as "$0 __move <add_url> <del_url> <desc>".
if [ "${1:-}" = "__move" ]; then
	add=$(curl $CURL_OPTS -o /dev/null -w '%{http_code}' -X PUT -H "Authorization: Bearer $TOKEN" "$2") || add=000
	case $add in
	2??) ;;
	*) echo "FAIL add ($add): $4" >&2; exit 0 ;;
	esac
	del=$(curl $CURL_OPTS -o /dev/null -w '%{http_code}' -X DELETE -H "Authorization: Bearer $TOKEN" "$3") || del=000
	case $del in
	2?? | 404) echo "ok" ;;
	*) echo "FAIL remove ($del): $4" >&2 ;;
	esac
	exit 0
fi

APPLY=0
[ "${1:-}" = "--apply" ] && APPLY=1

CURL_OPTS="-sS --fail-with-body"
[ "${INSECURE:-0}" = 1 ] && CURL_OPTS="$CURL_OPTS -k"
export CURL_OPTS

if [ -z "${API:-}" ]; then
	: "${MKE4_ADDR:?set MKE4_ADDR or API}"
	API="https://$MKE4_ADDR/api/auth/v1"
fi
[ -z "${TOKEN:-}" ] && [ -n "${TOKEN_FILE:-}" ] && TOKEN=$(cat "$TOKEN_FILE")
: "${TOKEN:?set TOKEN or TOKEN_FILE to an MKE4 admin id_token}"
export TOKEN

get() { curl $CURL_OPTS -H "Authorization: Bearer $TOKEN" "$API$1"; }

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

start=$(date +%s)
get /accounts/orgs >"$tmp/orgs.json"
get /accounts/teams >"$tmp/teams.json"
get /accounts/grants >"$tmp/grants.json"

# One line per move: add_url <TAB> del_url <TAB> description.
# A user name matching several org/team names (both contain "-") is skipped.
jq -r -n --arg api "$API" --arg exclude "${EXCLUDE:-}" \
	--slurpfile orgs "$tmp/orgs.json" --slurpfile teams "$tmp/teams.json" --slurpfile grants "$tmp/grants.json" '
	def q($g): [ (if $g.namespace then "namespace=\($g.namespace|@uri)" else empty end),
	             (if $g.roleKind == "Role" then "roleNamespace=\($g.namespace|@uri)" else empty end) ];
	def url($path; $params): $api + $path + (if ($params|length) > 0 then "?" + ($params|join("&")) else "" end);

	( [ $orgs[0].orgs[]?   | {key: .name, value: {org: .name}} ]
	+ [ $teams[0].teams[]? | {key: "\(.organization)-\(.name)", value: {org: .organization, team: .name}} ]
	) as $targets
	| ($targets | group_by(.key) | map({key: .[0].key, value: map(.value)}) | from_entries) as $index
	| ($exclude | split(" ") | map(select(. != ""))) as $excluded
	| $grants[0].grants[]?
	| select(.kind == "user" and $index[.name] != null and (.name | IN($excluded[]) | not))
	| . as $g | $index[.name] as $t
	| if ($t|length) > 1 then
	    "AMBIGUOUS\tuser \($g.name) \($g.roleKind):\($g.roleName) ns=\($g.namespace // "-") matches " + ([$t[] | if .team then "team \(.org)/\(.team)" else "org \(.org)" end] | join(", "))
	  else
	    $t[0] as $t
	    | (if $t.team then url("/accounts/grants/\($g.roleName|@uri)/team/\($t.team|@uri)"; q($g) + ["orgName=\($t.org|@uri)"])
	       else url("/accounts/grants/\($g.roleName|@uri)/org/\($t.org|@uri)"; q($g)) end) as $add
	    | url("/accounts/grants/\($g.roleName|@uri)/user/\($g.name|@uri)"; q($g)) as $del
	    | "\($add)\t\($del)\tuser \($g.name) \($g.roleKind):\($g.roleName) ns=\($g.namespace // "-") -> " + (if $t.team then "team \($t.org)/\($t.team)" else "org \($t.org)" end)
	  end
' >"$tmp/plan.tsv"

grep '^AMBIGUOUS' "$tmp/plan.tsv" | cut -f2 | sed 's/^/SKIPPED, ambiguous, resolve by hand: /' || true
grep -v '^AMBIGUOUS' "$tmp/plan.tsv" >"$tmp/moves.tsv" || true
total=$(wc -l <"$tmp/moves.tsv" | tr -d ' ')
echo "orgs: $(jq '.orgs | length' "$tmp/orgs.json"), teams: $(jq '.teams | length' "$tmp/teams.json"), grants: $(jq '.grants | length' "$tmp/grants.json"); to move: $total grants (planned in $(($(date +%s) - start))s)"
[ "${VERBOSE:-0}" = 1 ] && cut -f3 "$tmp/moves.tsv" | sed 's/^/  /'

if [ "$APPLY" != 1 ]; then
	echo "dry-run: re-run with --apply to make these changes"
	exit 0
fi

start=$(date +%s)
# Tabs separate fields; xargs -d is GNU-only, so turn each line into three quoted args.
moved=$(awk -F '\t' -v q="'" '{ for (i = 1; i <= 3; i++) gsub(q, q "\\" q q, $i); print q $1 q " " q $2 q " " q $3 q }' "$tmp/moves.tsv" |
	xargs -n 3 -P "${WORKERS:-8}" "$0" __move 2>"$tmp/errors" | grep -c '^ok$' || true)
took=$(($(date +%s) - start))
failed=$(wc -l <"$tmp/errors" | tr -d ' ')
cat "$tmp/errors" >&2
echo "moved $moved of $total grants, $failed failed, in ${took}s"
[ "$failed" = 0 ]
