-
Notifications
You must be signed in to change notification settings - Fork 50
Add an util script that allows flipping ACS roles on dev account #5202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}" | ||
| 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 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.