|
#!/bin/bash -eu |
|
|
|
# TODO: |
|
# Define: AWS_REGION based on `aws configure get profile.security.region || aws configure get profile.default.region` |
|
|
|
if [[ -n ${DEBUG:-} ]]; then |
|
set -o xtrace |
|
fi |
|
|
|
# TODO: Find a way to prevent leaking this variable. Currently it can't be defined 'local' |
|
_arole_jq_functions=' |
|
def export: |
|
"export " + . |
|
; |
|
def prefix: |
|
"AWS" + . | sub("(?<a>ARN|ACCOUNT|PROFILE)"; "ASSUMED_ROLE_"+.a) |
|
; |
|
def snake_case: |
|
.key | gsub("(?<a>[A-Z])"; "_"+.a) | ascii_upcase | prefix |
|
; |
|
def equals_value: |
|
"=\"" + .value + "\"" |
|
; |
|
def account_id: |
|
{"AccountId": (.Arn | sub("([^:]*:){4}(?<a>[0-9]*):.*";.a))} |
|
; |
|
def aws_config_input: |
|
{ "config":input, "credentials":input } |
|
; |
|
def aws_config_field_profiles: |
|
.config | [ ( to_entries[] | select(.key | startswith("profile"))) | { (.key|sub("profile ";"")): .value } ] | add |
|
; |
|
def aws_config_field_plugins: |
|
.config.plugins |
|
; |
|
def aws_config_field_config: |
|
{"profiles": aws_config_field_profiles, "plugins": aws_config_field_plugins} |
|
; |
|
def aws_config_field_credentials: |
|
.credentials |
|
; |
|
def aws_config_output: |
|
{"config": aws_config_field_config, "credentials": aws_config_field_credentials} |
|
; |
|
def aws_config: |
|
aws_config_input | aws_config_output |
|
; |
|
def role_profiles: |
|
.config.profiles | to_entries | .[] | select(.value.role_arn) |
|
; |
|
def account_from_role_arn: |
|
sub("([^:]*:){4}";"")|sub(":.*";"") |
|
; |
|
def role_name_from_role_arn: |
|
sub("([^:]*:){5}[^/]*/";"")|sub("/.*";"") |
|
; |
|
def profile_flat: |
|
[ |
|
.config.profiles | to_entries | .[] | select(.value.role_arn) | |
|
{ |
|
"profile":.key, |
|
"account":(.value.role_arn|account_from_role_arn), |
|
"role":(.value.role_arn|role_name_from_role_arn) |
|
} |
|
] |
|
; |
|
def profile_nested: |
|
[ |
|
.config.profiles | to_entries | .[] | select(.value.role_arn) | |
|
{ |
|
(.key):{ |
|
"account":(.value.role_arn|account_from_role_arn), |
|
"role":(.value.role_arn|role_name_from_role_arn) |
|
} |
|
} |
|
] |
|
; |
|
' |
|
|
|
function _arole_config_to_json(){ |
|
python3 -c 'from configparser import ConfigParser as CP; import json, sys; cp = CP(); cp.read_file(sys.stdin); print(json.dumps(cp._sections));' |
|
} |
|
|
|
function _arole_get_aws_config(){ |
|
{ |
|
_arole_config_to_json <~/.aws/config; |
|
_arole_config_to_json <~/.aws/credentials; |
|
} | _arole_export_aws_config_json |
|
} |
|
|
|
function _arole_export_aws_config_json(){ |
|
jq -n " |
|
${_arole_jq_functions} |
|
aws_config |
|
" |
|
} |
|
|
|
function _arole_format_profiles(){ |
|
jq " |
|
${_arole_jq_functions} |
|
profile_nested |
|
" |
|
} |
|
|
|
function _arole_export_json_assumed_role(){ |
|
jq -s -r " |
|
${_arole_jq_functions} |
|
[(add|add) | account_id,.] | |
|
add | to_entries[] | snake_case + equals_value | export |
|
" |
|
} |
|
|
|
function _arole_export_json_session_token(){ |
|
jq -s -r " |
|
${_arole_jq_functions} |
|
add | |
|
add | to_entries[] | snake_case + equals_value | export |
|
" |
|
} |
|
|
|
function _arole_mfa_session(){ |
|
HELPERS=aws,mfa _arole_define_helpers |
|
echo "Creating mfa session" | _arole_err |
|
aws sts get-session-token \ |
|
--serial-number ${MFA_SERIAL:-$(aws configure get --profile=security mfa_serial)} \ |
|
--token-code $(NOCOPY=1 MFA_WAIT=1 mfa) |
|
} |
|
|
|
function _arole_assume_role(){ |
|
HELPERS=aws,mfa _arole_define_helpers |
|
local profile="${1:-}" |
|
echo "Assuming role: $profile" | _arole_err |
|
if [[ -n ${MOCK_AWS:-} ]]; then |
|
function aws(){ |
|
<<<"H4sIAJcFxV0AA5WS0UvDMBDG3/0z8rzQ1jlwgT0E2UNRENaK4FvW3rAszcldypyj/7vpRnFuTmvI7+Uj33fHXXZCMzc1lAu08MRAQu2OpbQUSujFo06fH8bj/Ai1pMbhktAhr7diJDS58NaQU2bDij0rdXM7mfQoc0iVFGIjYy1u5KqxVpqiAGa5Iqwl+legTsHGeY6+l2hH4o6gBOcrY7lrNIOCwOt9wD1sQ/mTJi8S+s2CqUKX4xrciXOeRl9naGRPPY3OtHPlEh8/uP8iG2b5f/BQkH/PjvRsFiY+f3+ryPgw9DDv6ziZyiSRcZLHsdrfl+4X9cs8/Lws1Wera9urTy2VCbm3AgAA" base64 -d | gzip -d | jq . |
|
} |
|
fi |
|
echo '{"merge1":{"Profile": "'$profile'"}}' |
|
local mfa_serial="${MFA_SERIAL:-$( aws configure get --profile="$profile" mfa_serial )}" |
|
if [[ -n $mfa_serial ]]; then |
|
echo "Using MFA serial $mfa_serial" | _arole_err |
|
mfa_serial=" |
|
--serial-number $mfa_serial |
|
--token-code $(NOCOPY=1 MFA_WAIT=1 mfa)" |
|
else |
|
echo "WARN: No MFA specified for profile $profile" | _arole_err |
|
fi |
|
local role_arn="${ROLE_ARN:-$( aws configure get --profile="$profile" role_arn )}" |
|
if [[ -n $role_arn ]]; then |
|
role_arn=" |
|
--role-arn $role_arn" |
|
fi |
|
local duration_seconds="${DURATION_SECONDS:-$( aws configure get profile.$profile.max_session_duration )}" |
|
if [[ -n $duration_seconds ]]; then |
|
duration_seconds=" |
|
--duration-seconds $duration_seconds" |
|
fi |
|
aws sts assume-role $mfa_serial $role_arn $duration_seconds \ |
|
--role-session-name "$( ( id -un; date +-%Y-%m-%d+%H.%M.%S ) | tr -d '\n' )" |
|
} |
|
|
|
function _arole_update_config_with_max_session_duration(){ |
|
HELPERS=aws _arole_define_helpers |
|
local profile="$1" |
|
local arole_cache="$2" |
|
eval $(cat $arole_cache) |
|
local duration_seconds="${DURATION_SECONDS:-$( aws configure get profile.$profile.max_session_duration )}" |
|
if [[ -z $duration_seconds ]]; then |
|
local role_name="$(<<<"$AWS_ASSUMED_ROLE_ARN" jq -sRr "${_arole_jq_functions} role_name_from_role_arn")" |
|
local max_session_duration="$( aws iam get-role --role-name "$role_name" | \ |
|
jq -r '.Role.MaxSessionDuration' |
|
)" |
|
aws configure set profile.$profile.max_session_duration "$max_session_duration" |
|
fi |
|
} |
|
|
|
function _arole_reuse_cache(){ |
|
cache_file="$1" |
|
if [[ -f $cache_file ]]; then |
|
source $cache_file |
|
if _arole_expiration_is_current; then |
|
return 0 |
|
else |
|
_arole_cleanup_aws_vars |
|
return 8 |
|
fi |
|
fi |
|
echo "Did not find cache file: $cache_file" | _arole_err |
|
return 9 |
|
} |
|
|
|
function _arole_expiration_is_current(){ |
|
_arole_check_requirements |
|
local date=$(which gdate date| head -n1) |
|
if [[ -z ${AWS_EXPIRATION:-} ]]; then |
|
echo "No expiration set" | _arole_err |
|
return 8 |
|
fi |
|
if [[ $($date +%s) -gt $($date --date="$AWS_EXPIRATION" +%s) ]]; then |
|
echo "Expiration is in the past: $AWS_EXPIRATION" | _arole_err |
|
return 8 |
|
else |
|
echo "Expiration is in the future: $AWS_EXPIRATION" | _arole_err |
|
return 0 |
|
fi |
|
} |
|
|
|
function _arole_entrypoint(){ |
|
local profile="${1:-}" |
|
if [[ -z $profile ]]; then |
|
_arole_usage |
|
return 1 |
|
fi |
|
local arole_cache="$_arole_tmp_dir/aws-role-$profile.sh" |
|
if [[ $profile == "mfa" ]]; then |
|
local cmd=_arole_mfa_session |
|
local filter=_arole_export_json_session_token |
|
else |
|
local cmd=_arole_assume_role |
|
local filter=_arole_export_json_assumed_role |
|
HELPERS=arole _arole_define_helpers |
|
arole - |
|
fi |
|
|
|
if [[ "${2:-}" == "-f" ]]; then shift; shift; fi |
|
if _arole_reuse_cache $arole_cache; then |
|
echo "Reused cached $arole_cache" | _arole_err |
|
else |
|
$cmd "$@" | tee $arole_cache.json | $filter > $arole_cache |
|
_arole_update_config_with_max_session_duration $1 $arole_cache #& |
|
fi |
|
if [[ ${AROLE_EVAL:-1} = 1 && -z ${MOCK_AWS:-} ]]; then |
|
eval $(cat $arole_cache) |
|
else |
|
cat $arole_cache |
|
fi |
|
} |
|
|
|
function _arole_usage(){ |
|
_arole_err <<'EOF' |
|
|
|
Usage: arole <profile> [-] [env] |
|
Passing the "-" end causes this script to use your cached token rather than requesting a new one. |
|
It's faster, but more importantly allows you to use for-loops without getting errors due to using |
|
"one time passwords" more than once, as a consequence of the speed. |
|
Passing literal "env" (with or without "-") causes output to be given in envFile format that is |
|
appropriate for VSCode launch.json and `npm i dotenv`. This is done without sourcing the temp |
|
file. |
|
|
|
EOF |
|
# Show all roles/profiles in ~/.aws/config |
|
#awk -F '[:/ ]' '/^ *;/{next} /^.profile/{gsub("]",""); printf("\nprofile: %s\n", $2)} /role_arn/{printf(" account: %s\n role: %s\n", $5, $7)}' ~/.aws/config |
|
_arole_get_aws_config | _arole_format_profiles | underscore --wrapwidth 240 pretty| column -t |
|
echo | _arole_err |
|
local brk=0 |
|
if [[ -n ${AWS_ASSUMED_ROLE_ACCOUNT:-} ]]; then |
|
echo "Current profile: ${AWS_ASSUMED_ROLE_ACCOUNT}" | _arole_err |
|
brk=1 |
|
fi |
|
if [[ -n "${AWS_ASSUMED_ROLE_ACCOUNT_ID:-}" ]]; then |
|
echo "Current account: ${AWS_ASSUMED_ROLE_ACCOUNT_ID}" | _arole_err |
|
brk=1 |
|
fi |
|
if [[ -n "${AWS_EXPIRATION:-}" ]]; then |
|
echo "Credential Expiration: ${AWS_EXPIRATION}$( |
|
date=$(which gdate date | head -n1) |
|
$date && [[ $($date +%s) -gt $($date --date="$AWS_EXPIRATION" +%s) ]] && printf ' (expired)' |
|
)" | _arole_err |
|
brk=1 |
|
fi |
|
[[ $brk -gt 0 ]] && echo |
|
} |
|
|
|
function _arole_copy_cat(){ |
|
if [[ -n ${NOCOPY:-} ]]; then |
|
cat |
|
else |
|
pbcopy |
|
pbpaste |
|
fi |
|
} |
|
|
|
function _arole_err(){ |
|
if [[ ${NOERR:-} -eq 1 ]]; then |
|
cat |
|
elif [[ ${NOERR:-} -eq 2 ]]; then |
|
cat >/dev/null |
|
else |
|
cat >/dev/stderr |
|
fi |
|
} |
|
|
|
function _arole_get_totp(){ |
|
local service="$1" |
|
local refresh_wait=$(date +%-S) |
|
((refresh_wait=30-$refresh_wait)) |
|
[[ $refresh_wait -eq 0 ]] && ((refresh_wait=30)) |
|
[[ $refresh_wait -lt 0 ]] && ((refresh_wait=30+$refresh_wait)) |
|
echo "Getting an MFA code for $service" | _arole_err |
|
echo "$(totp_generator -s $service) $refresh_wait" |
|
} |
|
|
|
function _arole_check_requirements(){ |
|
if ! ( $(which gdate date | head -n1) --version 2>/dev/null | grep -q GNU; ); then |
|
echo "This requires GNU `date` command." |
|
if [[ $(uname) == Darwin ]]; then |
|
echo -e "On macOS, try:""\n"" brew install coreutils" |
|
fi |
|
exit 3 |
|
fi |
|
} |
|
|
|
function _arole_render_template(){ |
|
local template="$(cat)" |
|
for varname in $(<<<"$template" grep -oE '\{\{([A-Za-z0-9_]+)\}\}' | sed -En 's/.*\{\{([A-Za-z0-9_]+)\}\}.*/\1/p' | sort | uniq); do |
|
template="$(<<<"$template" sed -E "s/\{\{$varname\}\}/$(sed 's_\\_\\\\\\\\_g;s_/_\\/_g' <<<"${!varname}")/g")" |
|
done |
|
printf '%s' "$template" |
|
} |
|
|
|
function _arole_define_helpers(){ |
|
_arole_rename_function() { |
|
test -n "$(declare -f "$1")" || return |
|
local function_def="${_/$1/$2}" |
|
eval "$(tee $_arole_tmp_dir/$2.template<<<"$function_def" | _arole_render_template | tee $_arole_tmp_dir/$2.src)" |
|
unset -f "$1" |
|
} |
|
|
|
local function_name=arole |
|
if [[ ,${HELPERS:-}, =~ ,($function_name|all), ]]; then |
|
eval "$_arole_wrapper_function" |
|
_arole_rename_function _arole_helper $function_name |
|
fi |
|
|
|
local function_name=aws |
|
if [[ ,${HELPERS:-}, =~ ,($function_name|all), ]]; then |
|
function _arole_helper(){ |
|
local hash="$(</dev/urandom LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w6 | head -n1)" |
|
local curr=${AWS_CURR:-{{_arole_tmp_dir}}/aws-curr.$hash.json} |
|
local last=${AWS_LAST:-{{_arole_tmp_dir}}/aws-last.json} |
|
local log=${AWS_LOG:-{{_arole_tmp_dir}}/aws-cli.log} |
|
printf '%q ' aws "$@" | sed -e 's/ $//'>>$log |
|
local ishelp=0 |
|
local pager_or_cat="cat" |
|
if [[ ${*: -1} == "--last" ]]; then |
|
cat "$last" |
|
else |
|
[[ ${*: -1} == "help" ]] && ishelp=1 && pager_or_cat="less" |
|
touch $curr |
|
$(which aws) "$@" | tee $curr | $pager_or_cat |
|
local retval=${PIPESTATUS[0]} |
|
[[ $retval -eq 0 ]] && [[ $ishelp -eq 0 ]] && cp $curr $last || true |
|
rm -f $curr |
|
return $retval |
|
fi |
|
} |
|
_arole_rename_function _arole_helper $function_name |
|
fi |
|
|
|
local function_name=mfa |
|
if [[ ,${HELPERS:-}, =~ ,($function_name|all), ]]; then |
|
function _arole_helper(){ |
|
local service=${1:-aws} |
|
local arole_tmp="{{_arole_tmp_dir}}/mfa.$service" |
|
[[ -n "$(cat $arole_tmp.curr 2>/dev/null)" ]] && mv $arole_tmp.curr $arole_tmp.last |
|
local totp=($(arole -f _arole_get_totp $service | tee >(awk '{print $1}' >$arole_tmp.curr))) |
|
if [[ ${MFA_WAIT:-0} != 0 ]] && diff $arole_tmp.{last,curr} >/dev/null 2>&1; then |
|
local sleep_time=$((${totp[1]}+1)) |
|
echo "Waiting $sleep_time for MFA to refresh..." | arole -f _arole_err |
|
sleep $sleep_time |
|
totp=($(arole -f _arole_get_totp $service | tee >(awk '{print $1}' >$arole_tmp.curr))) |
|
fi |
|
echo "Expires in: ${totp[1]}" | arole -f _arole_err |
|
arole -f _arole_copy_cat < $arole_tmp.curr |
|
} |
|
_arole_rename_function _arole_helper $function_name |
|
fi |
|
|
|
local function_name=awsconf |
|
if [[ ,${HELPERS:-}, =~ ,($function_name|all), ]]; then |
|
function _arole_helper(){ arole -f _arole_get_aws_config; } |
|
_arole_rename_function _arole_helper $function_name |
|
fi |
|
|
|
# TODO: figure out if this --last flag is actually reaching the aws function (is it in scope?) |
|
local function_name=awsres |
|
if [[ ,${HELPERS:-}, =~ ,($function_name|all), ]]; then |
|
function _arole_helper(){ [[ -p /dev/stdin ]] && cat /dev/stdin; aws --last; } |
|
_arole_rename_function _arole_helper $function_name |
|
fi |
|
|
|
local function_name=awspres |
|
if [[ ,${HELPERS:-}, =~ ,($function_name|all), ]]; then |
|
function _arole_helper(){ |
|
[[ -p /dev/stdin ]] && cmd=cat || cmd=awsres |
|
local red="$(tput setaf 001)"; local bred="$(tput setaf 009)"; |
|
local gre="$(tput setaf 002)"; local bgre="$(tput setaf 056)"; |
|
local yel="$(tput setaf 003)"; local byel="$(tput setaf 011)"; |
|
local blu="$(tput setaf 004)"; local bblu="$(tput setaf 012)"; |
|
local mag="$(tput setaf 005)"; local bmag="$(tput setaf 013)"; |
|
local cya="$(tput setaf 006)"; local bcya="$(tput setaf 014)"; |
|
local whi="$(tput setaf 007)"; local bwhi="$(tput setaf 015)"; |
|
local none="$(tput init)"; |
|
$cmd | \ |
|
sed -E " |
|
s/(196342108771)/\1$red(rtg2)$none/g; |
|
s/(224530485594)/\1$red(prod)$none/g; |
|
s/(254860608894)/\1$red(security)$none/g; |
|
s/(255672061625)/\1$red(rtg)$none/g; |
|
s/(327762698288)/\1$red(stage)$none/g; |
|
s/(361823887585)/\1$red(prdev)$none/g; |
|
s/(485559245296)/\1$red(plan)$none/g; |
|
s/(568386843263)/\1$red(shared)$none/g; |
|
s/(734994113314)/\1$red(prprod)$none/g; |
|
s/(742127223731)/\1$red(dev)$none/g; |
|
s/(984407606261)/\1$red(root)$none/g; |
|
" |
|
} |
|
_arole_rename_function _arole_helper $function_name |
|
fi |
|
} |
|
|
|
function _arole_list_aws_vars(){ |
|
compgen -A export | grep '^AWS' |
|
} |
|
|
|
function _arole_list_arole_vars(){ |
|
compgen -A variable | grep '^_arole' |
|
} |
|
|
|
function _arole_list_arole_functions(){ |
|
compgen -A function | grep '^_arole' |
|
} |
|
|
|
function _arole_cleanup_aws_vars(){ |
|
unset $(arole -f _arole_list_aws_vars) |
|
} |
|
|
|
function _arole_cleanup_arole_vars(){ |
|
unset $(arole -f _arole_list_arole_vars) |
|
} |
|
|
|
function _arole_cleanup_functions(){ |
|
unset -f $(arole -f _arole_list_arole_functions) |
|
} |
|
|
|
function _arole_cleanup(){ |
|
_arole_cleanup_arole_vars |
|
_arole_cleanup_functions |
|
} |
|
|
|
if [[ -d "${HOME:-}/.aws" ]]; then |
|
_arole_tmp_dir="$HOME/.aws/temp" |
|
[[ -d "$_arole_tmp_dir" ]] || mkdir -p "$_arole_tmp_dir" |
|
else |
|
_arole_tmp_dir="/tmp" |
|
fi |
|
|
|
_arole_script_name="$(basename "$BASH_SOURCE")" |
|
_arole_script_path="$(cd "$(dirname "$BASH_SOURCE")"; pwd;)" |
|
_arole_script="$_arole_script_path/$_arole_script_name" |
|
|
|
_arole_wrapper_function=" |
|
function _arole_helper(){ |
|
if [[ -z \${1:-} ]]; then |
|
${_arole_script} |
|
else |
|
if [[ \${TAIL_TEST:-0} -eq 1 ]]; then |
|
date; tail $_arole_tmp_dir/mfa.aws.last $_arole_tmp_dir/mfa.aws.curr $_arole_tmp_dir/aws-cli.log |
|
fi |
|
if [[ \$1 == '-' ]]; then |
|
unset \$(${_arole_script} -f _arole_list_aws_vars) |
|
elif [[ \$1 =~ -. ]]; then |
|
export DEBUG=\"\${DEBUG:-}\"; '${_arole_script}' \"\$@\" |
|
else |
|
eval \$(export DEBUG=\"\${DEBUG:-}\"; '${_arole_script}' \"\$@\") |
|
fi |
|
fi |
|
} |
|
" |
|
|
|
if [[ $0 == $BASH_SOURCE ]]; then |
|
export PS4="# (xtrace) \$(basename \"\${BASH_SOURCE:-}\") Line: \${LINENO:-}\$([[ -n \${FUNCNAME[0]:-} ]] && echo \" \${FUNCNAME[0]:-}()\")\n" |
|
export AROLE_EVAL=0 |
|
entrypoint=_arole_entrypoint |
|
if [[ "${1:-}" == "-f" ]]; then |
|
shift |
|
entrypoint=$1 |
|
shift |
|
HELPERS=all _arole_define_helpers |
|
fi |
|
$entrypoint "$@" |
|
else |
|
_arole_define_helpers |
|
_arole_check_requirements |
|
_arole_cleanup |
|
fi |