Skip to content
Merged
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
280 changes: 280 additions & 0 deletions utils/dev_acs_role_flip.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
#!/bin/bash
# Copyright The Linux Foundation and each contributor to CommunityBridge.
# SPDX-License-Identifier: MIT

# DEV-ONLY helper to flip LF-admin status and manage ACS role grants for a user,
# so DISALLOW_ADMIN EasyCLA v4 ops can be tested with your own token.
#
# Admin flag: the lfx-gateway acs-authorizer plugin asks the ACS warden
# (POST /acs/v1/api/warden/subjects/authorize/v2) which returns isAdmin=true
# when the user holds the 'system-admin' (admin/*) role - 'lf-staff' (staff/*)
# is toggled together with it for consistency. Both are restored losslessly
# (fixed role_id/object_id/object_type_id).
#
# Auth: mints a platform M2M token from the cla-* SSM parameters using the
# lfproduct-dev AWS profile - independent of your personal user token, so you
# can always flip admin back on.
#
# Caching notes:
# - warden caches successful (user,resource) answers for ~10 minutes; the
# authoritative current state is the rolescopes listing ('status'/'roles').
# - after flipping, pass -H 'Cache-Control: no-cache' on your first API call
# through the gateway to bypass its cached X-ACL for that path.
#
# Usage (mutating commands require an explicit username arg or ACS_USER env;
# read-only commands default to ACS_USER, then lgryglicki):
# ./dev_acs_role_flip.sh status [username]
# ./dev_acs_role_flip.sh roles [username] # full rolescopes JSON
# ./dev_acs_role_flip.sh admin on|off <username>
# ./dev_acs_role_flip.sh grant <role> <object_type> <object_id> <username>
# ./dev_acs_role_flip.sh revoke <role> <object_id> <username> # object_id '*' allowed
# ./dev_acs_role_flip.sh warden <resource> [method] [username] # raw warden probe
#
# Examples:
# ./dev_acs_role_flip.sh admin off lgryglicki
# ./dev_acs_role_flip.sh grant cla-manager 'project|organization' 'a09P000000DsCE5IAN|0014100000Te0G7AAJ' lgryglicki
# ./dev_acs_role_flip.sh revoke cla-manager 'a09P000000DsCE5IAN|0014100000Te0G7AAJ' lgryglicki
# ./dev_acs_role_flip.sh warden /v4/company/external/0014100000Te0yqAAB/cla-groups GET
#
# Safety: refuses to run unless the AWS profile resolves to the LF dev account
# (override with EXPECTED_AWS_ACCOUNT). M2M token is cached under
# $XDG_RUNTIME_DIR or ~/.cache in a mode-0700 dir, written atomically.
#
# Common roles: cla-manager, cla-manager-designee, cla-signatory (object_type
# 'project|organization', object_id '<projectSFID>|<orgSFID>'), system-admin
# ('admin'/'*'), lf-staff ('staff'/'*'). 'roles' shows what you already have.

set -euo pipefail

STAGE="${STAGE:-dev}"
if [ "$STAGE" != "dev" ]; then
echo "refusing to run: STAGE='$STAGE' - this tool is dev-only" >&2
exit 1
fi

PROFILE="${AWS_PROFILE_OVERRIDE:-lfproduct-dev}"
Comment thread
lukaszgryglicki marked this conversation as resolved.
REGION="${AWS_REGION_OVERRIDE:-us-east-1}"
ACS="https://api-gw.dev.platform.linuxfoundation.org/acs/v1/api"
DEFAULT_USER="${ACS_USER:-lgryglicki}" # read-only commands only
EXPECTED_AWS_ACCOUNT="${EXPECTED_AWS_ACCOUNT:-395594542180}" # LF dev account
CACHE_DIR="${XDG_RUNTIME_DIR:-$HOME/.cache}/easycla-acs-flip"
TOKEN_CACHE="$CACHE_DIR/m2m_${PROFILE}_${REGION}_${STAGE}.token"

require_user() { # explicit-arg-or-ACS_USER for mutating commands
local u="${1:-${ACS_USER:-}}"
if [ -z "$u" ]; then
echo "mutating command: pass <username> explicitly or set ACS_USER" >&2
exit 1
fi
echo "$u"
}

check_aws_account() {
local acct
acct=$(aws sts get-caller-identity --profile "$PROFILE" --region "$REGION" --query Account --output text)
if [ "$acct" != "$EXPECTED_AWS_ACCOUNT" ]; then
echo "refusing to run: AWS profile '$PROFILE' is account $acct, expected dev account $EXPECTED_AWS_ACCOUNT" >&2
exit 1
fi
}

ssm() {
aws ssm get-parameter --profile "$PROFILE" --region "$REGION" \
--name "cla-auth0-platform-$1-${STAGE}" --query Parameter.Value --output text
}

mint_token() {
local url cid sec aud
url=$(ssm url); cid=$(ssm client-id); sec=$(ssm client-secret); aud=$(ssm audience)
CID="$cid" SEC="$sec" AUD="$aud" python3 -c 'import json,os;print(json.dumps({"grant_type":"client_credentials","client_id":os.environ["CID"],"client_secret":os.environ["SEC"],"audience":os.environ["AUD"]}))' \
| curl -s --max-time 20 -X POST "$url" -H "Content-Type: application/json" --data-binary @- \
| python3 -c "import json,sys;d=json.load(sys.stdin);tok=d.get('access_token') or sys.exit('token mint failed: '+str(d));print(tok)"
}

get_token() {
check_aws_account
if [ -f "$TOKEN_CACHE" ] && [ -n "$(find "$TOKEN_CACHE" -mmin -50 2>/dev/null)" ]; then
cat "$TOKEN_CACHE"
return
fi
local tok tmp
tok=$(mint_token)
mkdir -p "$CACHE_DIR"
chmod 700 "$CACHE_DIR"
tmp=$(mktemp "$CACHE_DIR/.m2m.XXXXXX")
printf '%s\n' "$tok" > "$tmp"
chmod 600 "$tmp"
mv -f "$tmp" "$TOKEN_CACHE"
echo "$tok"
}

acs_get() { curl -sfS --max-time 20 -H "Authorization: Bearer $TOKEN" "$ACS$1"; }
acs_post() { curl -s --max-time 20 -X POST "$ACS$1" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$2" -w "\n%{http_code}"; }
acs_delete() { curl -s --max-time 20 -X DELETE "$ACS$1" -H "Authorization: Bearer $TOKEN" -o /dev/null -w "%{http_code}"; }

role_id() {
case "$1" in
system-admin) echo cce3551b-12cc-436e-a3c5-682b59afe3b1; return;;
lf-staff) echo 0049beb2-2b37-458d-866d-3e42bb1dd6d5; return;;
esac
acs_get "/roles?search=$1" | python3 -c "
import json,sys
want=sys.argv[1]
d=json.load(sys.stdin)
rows=d if isinstance(d,list) else d.get('data',[])
for r in rows:
if r.get('role_name')==want:
print(r['role_id']); break
else:
sys.exit('role not found: '+want)" "$1"
}

object_type_id() {
case "$1" in
project) echo 1; return;; organization) echo 2; return;; admin) echo 3; return;;
'project|organization') echo 11; return;; staff) echo 14; return;;
esac
acs_get "/object-types" | python3 -c "
import json,sys
want=sys.argv[1]
for t in json.load(sys.stdin):
if t.get('name')==want:
print(t['type_id']); break
else:
sys.exit('object type not found: '+want)" "$1"
}

rolescopes() { acs_get "/users/rolescopes?usernames=$1"; }

grant_ids_for() { # user role [object_id]
rolescopes "$1" | python3 -c "
import json,sys
user,role=sys.argv[1],sys.argv[2]
objid=sys.argv[3] if len(sys.argv)>3 else None
d=json.load(sys.stdin)
for r in d.get(user,[]):
if r['role_name']!=role: continue
for s in r.get('scopes',[]):
if objid is None or s.get('object_id')==objid:
print(r['role_id'],s['grant_id'],s.get('object_type_name',''),s.get('object_id',''))" "$@"
}

do_grant() { # role objtype objid user
local rid tid out code
rid=$(role_id "$1"); tid=$(object_type_id "$2")
out=$(acs_post "/roles/$rid/members/users" \
"{\"role_id\":\"$rid\",\"usernames\":[\"$4\"],\"object_ids\":[\"$3\"],\"object_type_id\":$tid}")
code=${out##*$'\n'}
case "$code" in
201) echo "granted: $1 $2/$3 -> $4";;
409) echo "already granted: $1 $2/$3 -> $4";;
*) echo "grant $1 $2/$3 -> $4 failed: HTTP $code"; echo "$out" | head -c 400; echo; exit 1;;
esac
}

do_revoke() { # role objid user
local found rid gid rest code
found=$(grant_ids_for "$3" "$1" "$2")
if [ -z "$found" ]; then
echo "no matching grant: $1 $2 for $3"
return
fi
while read -r rid gid rest; do
code=$(acs_delete "/roles/$rid/members/users/$gid")
case "$code" in
204) echo "revoke $1 grant $gid ($rest): HTTP 204";;
404) echo "revoke $1 grant $gid ($rest): already gone (404 - ACS read lag)";;
*) echo "revoke $1 grant $gid ($rest) failed: HTTP $code" >&2; return 1;;
esac
done <<< "$found"
}

warden_probe() { # resource method user
curl -s --max-time 20 -X POST "$ACS/warden/subjects/authorize/v2" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"username\":\"$3\",\"resource\":\"$1\",\"action\":\"$2\"}"
}

admin_state() { # user -> prints true/false; isAdmin comes from system-admin only
rolescopes "$1" | python3 -c "
import json,sys
names=[r['role_name'] for r in json.load(sys.stdin).get(sys.argv[1],[])]
print('true' if 'system-admin' in names else 'false')" "$1"
}

wait_admin_state() { # user expected(true|false)
local i state
for i in $(seq 1 10); do
state=$(admin_state "$1")
if [ "$state" = "$2" ]; then
echo "verified: LF admin = $2 for $1 (rolescopes settled)"
return 0
fi
sleep 2
done
echo "WARNING: rolescopes still report LF admin = $state (expected $2) after 20s - ACS read lag; re-check with: $0 status $1" >&2
return 1
}

cmd="${1:-}"; shift || true
case "$cmd" in
status)
USER_NAME="${1:-$DEFAULT_USER}"; TOKEN=$(get_token)
rolescopes "$USER_NAME" | python3 -c "
import json,sys
user=sys.argv[1]
d=json.load(sys.stdin)
roles=d.get(user,[])
names=[r['role_name'] for r in roles]
admin='system-admin' in names
print(f'user: {user}')
print(f'LF admin (isAdmin source = system-admin): {admin} (lf-staff present: {\"lf-staff\" in names})')
print('roles:', ', '.join(sorted(names)) or '(none)')
for r in roles:
if r['role_name'] in ('cla-manager','cla-manager-designee','cla-signatory'):
for s in r.get('scopes',[]):
print(f' {r[\"role_name\"]}: {s.get(\"object_type_name\")}/{s.get(\"object_id\")}')" "$USER_NAME"
echo "note: gateway/warden may serve cached answers for up to ~10 min; use -H 'Cache-Control: no-cache' on API calls"
;;
roles)
USER_NAME="${1:-$DEFAULT_USER}"; TOKEN=$(get_token)
rolescopes "$USER_NAME" | python3 -m json.tool
;;
admin)
ONOFF="${1:-}"; USER_NAME=$(require_user "${2:-}"); TOKEN=$(get_token)
case "$ONOFF" in
off)
do_revoke system-admin '*' "$USER_NAME"
do_revoke lf-staff '*' "$USER_NAME"
wait_admin_state "$USER_NAME" false || true
echo "admin OFF for $USER_NAME (restore with: $0 admin on $USER_NAME)"
;;
on)
do_grant system-admin admin '*' "$USER_NAME"
do_grant lf-staff staff '*' "$USER_NAME"
wait_admin_state "$USER_NAME" true || true
echo "admin ON for $USER_NAME"
;;
*) echo "usage: $0 admin on|off <username>" >&2; exit 1;;
esac
;;
grant)
[ $# -ge 3 ] || { echo "usage: $0 grant <role> <object_type> <object_id> <username>" >&2; exit 1; }
USER_NAME=$(require_user "${4:-}"); TOKEN=$(get_token)
do_grant "$1" "$2" "$3" "$USER_NAME"
;;
revoke)
[ $# -ge 2 ] || { echo "usage: $0 revoke <role> <object_id> <username>" >&2; exit 1; }
USER_NAME=$(require_user "${3:-}"); TOKEN=$(get_token)
do_revoke "$1" "$2" "$USER_NAME"
;;
warden)
[ $# -ge 1 ] || { echo "usage: $0 warden <resource> [method] [username]" >&2; exit 1; }
RES="$1"; METHOD="${2:-GET}"; USER_NAME="${3:-$DEFAULT_USER}"; TOKEN=$(get_token)
warden_probe "$RES" "$METHOD" "$USER_NAME" | python3 -m json.tool
;;
*)
grep '^# ' "$0" | sed 's/^# //'
exit 1
;;
esac
Loading