admin.bash (362 lines of code) (raw):
#!/bin/bash
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
GREEN="\e[32m"
RED="\e[31m"
RESET="\e[0m"
# Default "Admin Tool" client IDs and secrets. Can override via "set ...".
IC_CLIENT_ID="1b2b57c0-46dc-48ce-bd5b-389f26489bcd"
IC_CLIENT_SECRET="94cb4a9b-2f96-4b59-851e-5791dd3040b4"
DAM_CLIENT_ID="0ef2f928-ba67-47b6-9cd6-288be82e3497"
DAM_CLIENT_SECRET="64f1b6b3-abbe-48b4-bfa9-67f6d1ab910d"
STATE_FILE=~/.fa_admin
API_VERSION="v1alpha"
REALM="master"
ENVIRONMENT=""
# For curl debug use:
# CURL_OPTIONS="-v --trace-ascii /dev/stdout -s"
CURL_OPTIONS="-s"
# Commands are added here and usage is auto-generated. Note that user supplied
# args can be supplied (e.g. "<name>") starting on the 4th input and must then
# be on every second input. Avoid conflicts with arg commands and non-arg
# commands by pretending all <named_arg> fields are removed and seeing if there
# is a collision. Example: "print hello" and "print hello <name>" collide.
#
# Most service endpoint commands will make one of three types of calls:
# 1. curl_public <dam|ic> <resource_path>: when anyone can hit the endpoint.
# 2. <dam_curl_client|ic_curl_client> <resource_path>: when client_id and
# client_secret are needed to identify the application.
# 3. <dam_curl_auth|ic_curl_auth> <resource_path>: when client id/secret and
# an access_token are required to identify the app and the user.
declare -A COMMANDS=(
# DAM commands
["check dam clients"]='dam_curl_client "/dam/${API_VERSION?}/${REALM?}/clients:sync"'
["print dam config"]='dam_curl_auth "/dam/${API_VERSION?}/${REALM?}/config"'
["print dam config history"]='dam_curl_auth "/dam/${API_VERSION?}/${REALM?}/config/history"'
["print dam info"]='curl_public "dam" "/dam"'
["print dam processes"]='dam_curl_auth "/dam/${API_VERSION?}/${REALM?}/processes"'
["print dam process <name>"]='dam_curl_auth "/dam/${API_VERSION?}/${REALM?}/processes/$4"'
["print dam resource <name>"]='dam_curl_client "/dam/${API_VERSION?}/${REALM?}/resources/$4"'
["print dam resource <name> views"]='dam_curl_client "/dam/${API_VERSION?}/${REALM?}/resources/$4/views"'
["print dam resource <name> view <name>"]='dam_curl_client "/dam/${API_VERSION?}/${REALM?}/resources/$4/views/$6"'
["print dam resource <name> view <name> roles"]='dam_curl_client "/dam/${API_VERSION?}/${REALM?}/resources/$4/views/$6/roles"'
["print dam resource <name> view <name> role <name>"]='dam_curl_client "/dam/${API_VERSION?}/${REALM?}/resources/$4/views/$6/roles"'
["print dam resources"]='dam_curl_client "/dam/${API_VERSION?}/${REALM?}/resources"'
["print dam services"]='dam_curl_client "/dam/${API_VERSION?}/${REALM?}/services"'
["print dam personas"]='dam_curl_client "/dam/${API_VERSION?}/${REALM?}/testPersonas"'
["print dam roles"]='dam_curl_client "/dam/${API_VERSION?}/${REALM?}/damRoleCategories"'
["print dam translators"]='dam_curl_client "/dam/${API_VERSION?}/${REALM?}/passportTranslators"'
["print dam views"]='dam_curl_client "/dam/${API_VERSION?}/${REALM?}/flatViews"'
["sync dam clients"]='dam_curl_client "/dam/${API_VERSION?}/${REALM?}/clients:sync" "POST"'
# IC commands
["check ic clients"]='ic_curl_client "/identity/${API_VERSION?}/${REALM?}/clients:sync"'
["print ic config"]='ic_curl_auth "/identity/${API_VERSION?}/${REALM?}/config"'
["print ic config history"]='ic_curl_auth "/identity/${API_VERSION?}/${REALM?}/config/history"'
["print ic idps"]='ic_curl_client "/identity/${API_VERSION?}/${REALM?}/identityProviders"'
["print ic info"]='curl_public "ic" "/identity"'
["print ic me"]='ic_curl_auth "/identity/scim/v2/${REALM?}/Me"'
["print ic translators"]='ic_curl_client "/identity/${API_VERSION?}/${REALM?}/passportTranslators"'
["print ic user <name>"]='ic_curl_auth "/identity/scim/v2/${REALM?}/Users/$4"'
["print ic users"]='ic_curl_auth "/identity/scim/v2/${REALM?}/Users"'
["sync ic clients"]='ic_curl_client "/identity/${API_VERSION?}/${REALM?}/clients:sync" "POST"'
["print ic user <name> consent"]='ic_curl_auth "/identity/${API_VERSION?}/${REALM?}/users/$4/consents"'
["delete ic user <name> consent <consent_id>"]='ic_curl_auth "/identity/${API_VERSION?}/${REALM?}/users/$4/consents/$6" "DELETE"'
# Admin state commands
# ["login dam"]='curl_login "dam" "/dam" "DAM_ACCESS_TOKEN" "DAM_REFRESH_TOKEN"'
["login ic"]='curl_login "ic" "/identity" "IC_ACCESS_TOKEN" "IC_REFRESH_TOKEN"'
# ["state_refresh dam"]='state_refresh "dam" "${DAM_CLIENT_ID}" "${DAM_CLIENT_SECRET}"'
# ["state_refresh ic"]='state_refresh "ic" "${IC_CLIENT_ID}" "${IC_CLIENT_SECRET}"'
["set dam access_token <token>"]='state_update "DAM_ACCESS_TOKEN" "$4"'
["set dam client <client_id>"]='state_update "DAM_CLIENT_ID" "$4"'
["set dam refresh_token <token>"]='state_update "DAM_REFRESH_TOKEN" "$4"'
["set dam secret <secret>"]='state_update "DAM_CLIENT_SECRET" "$4"'
["set dam user <email>"]='state_update "DAM_USER_EMAIL" "$4"'
["set env <environment>"]='state_update "ENVIRONMENT" "$3"'
["set ic access_token <token>"]='state_update "IC_ACCESS_TOKEN" "$4"'
["set ic client <client_id>"]='state_update "IC_CLIENT_ID" "$4"'
["set ic refresh_token <token>"]='state_update "IC_REFRESH_TOKEN" "$4"'
["set ic secret <secret>"]='state_update "IC_CLIENT_SECRET" "$4"'
["set ic user <email>"]='state_update "IC_USER_EMAIL" "$4"'
["set project <project_id>"]='state_update "PROJECT" "$3"'
["set realm <realm>"]='state_update "REALM" "$3"'
["print state"]='state_print'
# ["refresh dam"]='refresh_access "dam" ${DAM_CLIENT_ID?} ${DAM_CLIENT_SECRET?} ${DAM_REFRESH_TOKEN?}'
# ["refresh ic"]='refresh_access "ic" ${IC_CLIENT_ID?} ${IC_CLIENT_SECRET?} ${IC_REFRESH_TOKEN?}'
["reset state"]='state_reset'
)
# Only commands on this list are allowed in the COMMANDS list above.
declare -A COMMAND_ALLOWLIST=(
["curl_login"]=true
["curl_public"]=true
["dam_curl_auth"]=true
["dam_curl_client"]=true
["ic_curl_auth"]=true
["ic_curl_client"]=true
["refresh_access"]=true
["state_print"]=true
["state_refresh"]=true
["state_reset"]=true
["state_update"]=true
)
#####################################################
# State helper functions #
#####################################################
# Sets variable $1 to value $2 and saves it to disk to be retrieved on
# subsequent runs.
state_update() {
if [[ "$1" == "" || "$2" == "" ]]; then
print_usage
exit 1
fi
export "$1"="$2"
state_save
}
# Loads a set of variables (i.e. state) from disk. See the "set ..." commands
# for a list of such variables.
state_load() {
if test -f "${STATE_FILE?}"; then
local settings=`cat "${STATE_FILE?}"`
local settings=(${settings/$'\n'/ })
# Don't blindly eval() something we loaded from elsewhere. Instead, process
# one line at a time and verify of the form of A=B with explicit "export"
# command for context within this script.
for setting in "${settings[@]}"; do
local parts=(${setting//=/ })
NAME="${parts[0]}"
VALUE="${parts[1]}"
if [[ "${NAME}" == "" ]]; then
echo -e ${RED?}'invalid state "'"${setting}"'": recommend running "admin.bash reset state"'${RESET?}
exit 2
fi
# This check prevents injecting commands into this script by ensuring
# that input is simple identifier characters and doesn't a means to
# manipulate/escape string quote state, etc.
if grep '^[-0-9a-zA-Z_.@=]*$' <<<${setting} > /dev/null; then
export "$NAME"="$VALUE"
else
echo -e "${RED?}invalid characters in state \"${setting}\": recommend running \"admin.bash state reset\"${RESET?}"
exit 2
fi
done
fi
}
# Generates the contents of the state as a string to be saved or printed.
state_string() {
STATE_STRING="PROJECT=${PROJECT}\nENVIRONMENT=${ENVIRONMENT}\nREALM=${REALM}\nDAM_CLIENT_ID=${DAM_CLIENT_ID}\nDAM_CLIENT_SECRET=${DAM_CLIENT_SECRET}\nIC_CLIENT_ID=${IC_CLIENT_ID}\nIC_CLIENT_SECRET=${IC_CLIENT_SECRET}\nDAM_REFRESH_TOKEN=${DAM_REFRESH_TOKEN}\nDAM_ACCESS_TOKEN=${DAM_ACCESS_TOKEN}\nIC_REFRESH_TOKEN=${IC_REFRESH_TOKEN}\nIC_ACCESS_TOKEN=${IC_ACCESS_TOKEN}\nDAM_USER_EMAIL=${DAM_USER_EMAIL}\nIC_USER_EMAIL=${IC_USER_EMAIL}\n"
}
# Generate all state as a string and print it.
state_print() {
state_string
printf "${STATE_STRING}"
}
# Generate all state and a string and save it to disk.
state_save() {
state_string
printf "${STATE_STRING}" >"${STATE_FILE?}"
echo -e ${GREEN?}'state updated'${RESET?}
}
# Remove the state file from disk. Will inherit default state values on the
# next run.
state_reset() {
rm "${STATE_FILE?}" > /dev/null 2>&1
echo -e ${GREEN?}'state reset complete'${RESET?}
}
# state_refresh <dam|ic> <client_id> <client_secret>
# Present a login page link and ask user to paste refresh token.
state_refresh() {
local dash="-"
if [[ "${ENVIRONMENT}" == "" ]]; then
dash=""
fi
state_login_redirect "$1"
echo Visit: "${REDIRECT}?client_id=$2&client_secret=$3"
echo
read -p "Paste refresh token and press enter: " refresh
echo
if [[ "${refresh}" == "" ]]; then
echo -e ${RED?}'refresh token not provided'${RESET?}
exit 1
fi
if [[ "$1" == "ic" ]]; then
IC_REFRESH_TOKEN="${refresh}"
else
DAM_REFRESH_TOKEN="${refresh}"
fi
echo -e ${GREEN?}'received refresh token'${RESET?}
state_save
}
# state_login_redirect <dam|ic>
# Generate a URL to use for login
state_login_redirect() {
local dash="-"
if [[ "${ENVIRONMENT}" == "" ]]; then
dash=""
fi
REDIRECT="https://$1demo${dash}${ENVIRONMENT}-dot-${PROJECT}.appspot.com/test"
}
#####################################################
# Curl helper functions #
#####################################################
# curl_public <dam|ic> <resource_path>
# Generates a URL then performs a GET using curl as part of a RESTful API.
curl_public() {
if [[ "$2" == "" ]]; then
echo -e "${RED?}must provide RESTful path to resource${RESET?}"
exit 2
fi
local dash="-"
if [[ "${ENVIRONMENT}" == "" ]]; then
dash=""
fi
RESULT=`curl -s "$1${dash}${ENVIRONMENT}-dot-${PROJECT}.appspot.com$2"`
curl_print
}
# curl_client <dam|ic> <resource_path> <client_id> <client_secret> [method] [input] [quiet]
# Generates a URL then performs a GET using curl as part of a RESTful API.
# Unlike curl_public, this function adds the client id/secret to the request.
curl_client() {
if [[ "$4" == "" ]]; then
echo -e "${RED?}client id and secret required: use \"admin set $1 secret <paste_secret>\" etc${RESET?}"
exit 2
fi
local method="$5"
if [[ "${method}" == "" ]]; then
method="GET"
fi
local dash="-"
if [[ "${ENVIRONMENT}" == "" ]]; then
dash=""
fi
local input=""
if [[ "$6" != "" ]]; then
input="&$6"
fi
RESULT=`curl ${CURL_OPTIONS} -X "${method}" -H "Content-Length: 0" -H "Content-Type: application/x-www-form-urlencoded" "$1${dash}${ENVIRONMENT}-dot-${PROJECT}.appspot.com$2?client_id=$3&client_secret=$4${input}"`
if [[ "$7" == "" ]]; then
curl_print
fi
}
# curl_auth <dam|ic> <resource_path> <client_id> <client_secret> <access_token> [method]
# Generates a URL then performs a GET using curl as part of a RESTful API.
# Unlike curl_public, this function adds the auth headers and client id/secret
# to the request.
curl_auth() {
if [[ "$5" == "" ]]; then
echo -e "${RED?}login access_token required: use \"admin set $1 access_token <paste_token>\"${RESET?}"
exit 2
fi
local dash="-"
if [[ "${ENVIRONMENT}" == "" ]]; then
dash=""
fi
local method="GET"
if [[ "$6" != "" ]]; then
method=$6
fi
RESULT=`curl ${CURL_OPTIONS} -X "${method}" -H "Authorization: bearer $5" -H "Content-Type: application/x-www-form-urlencoded" "$1${dash}${ENVIRONMENT}-dot-${PROJECT}.appspot.com$2?client_id=$3&client_secret=$4"`
curl_print
}
curl_print() {
# Check to see if result is JSON, then pretty print in JSON if available.
`jq -e <<< "${RESULT}" > /dev/null 2>&1`
if [[ "$?" == "0" ]]; then
printf "`jq -e <<< "${RESULT}"`"
else
echo "${RESULT}"
fi
echo
}
# dam_curl_auth <resource_path>
# Wrapper for curl_auth that supplies a set of DAM inputs for client id/secret
# and access token that were loaded from disk. Makes it easier to write COMMANDS
# by abstracting all of these details.
dam_curl_auth() {
curl_auth "dam" "$1" "${DAM_CLIENT_ID}" "${DAM_CLIENT_SECRET}" "${DAM_ACCESS_TOKEN}" "$2"
}
# dam_curl_client <resource_path> [method]
# Wrapper for curl_client that supplies a set of DAM inputs for client id/secret.
dam_curl_client() {
curl_client "dam" "$1" "${DAM_CLIENT_ID}" "${DAM_CLIENT_SECRET}" "$2"
}
# ic_curl_auth <resource_path>
# Wrapper for curl_auth that supplies a set of IC inputs for client id/secret
# and access token that were loaded from disk. Makes it easier to write COMMANDS
# by abstracting all of these details.
ic_curl_auth() {
curl_auth "ic" "$1" "${IC_CLIENT_ID}" "${IC_CLIENT_SECRET}" "${IC_ACCESS_TOKEN}" "$2"
}
# ic_curl_client <resource_path> [method] [input] [quiet]
# Wrapper for curl_client that supplies a set of IC inputs for client id/secret.
ic_curl_client() {
curl_client "ic" "$1" "${IC_CLIENT_ID}" "${IC_CLIENT_SECRET}" "$2" "$3" "$4"
}
# curl_login <dam|ic> <rootPath> <accessTokenVar> <refreshTokenVar>
# Walks user through the steps to capture auth access_token and refresh_token.
curl_login() {
local cmd=ic_curl_client
local user=${IC_USER_EMAIL}
if [[ "$1" == "dam" ]]; then
cmd=dam_curl_client
user=${DAM_USER_EMAIL}
fi
if [[ "${user}" == "" ]]; then
echo -e "${RED?}Must set user first using 'admin.bash set $1 user <email>'${RESET?}"
exit 2
fi
$cmd "$2/cli/register/auto" "POST" "email=${user}" "quiet"
if [[ "$?" != "0" ]]; then
curl_print
echo
echo -e "${RED?}Login failed${RESET?}"
exit 2
fi
local id=`jq -r '.id' <<< "${RESULT}"`
local url=`jq -r '.authUrl' <<< "${RESULT}"`
local secret=`jq -r '.secret' <<< "${RESULT}"`
echo "Login via this link in a browser: $url"
echo "Press enter after login is successful..."
read
$cmd "/identity/cli/register/${id}" "GET" "login_secret=${secret}" "quiet"
if [[ "$?" != "0" ]]; then
curl_print
echo
echo -e "${RED?}Get login state curl command failed${RESET?}"
exit 2
fi
local atok=`jq -r '.accessToken' <<< "${RESULT}"`
local rtok=`jq -r '.refreshToken' <<< "${RESULT}"`
if [[ "${atok}" == "" || "${atok}" == "null" || "${rtok}" == "" || "${rtok}" == "null" ]]; then
curl_print
echo
echo -e "${RED?}Get login state values failed${RESET?}"
exit 2
fi
export "$3"="${atok}"
export "$4"="${rtok}"
state_save
}
# refresh_access <dam|ic> <client_id> <client_secret> <refresh_token>
# Use a refresh token to fetch a new access token and store it in state.
refresh_access() {
local basic_auth=`echo "$2:$3" | base64 -w 0 | sed 's/+/-/g; s/\//_/g'`
local dash="-"
if [[ "${ENVIRONMENT}" == "" ]]; then
dash=""
fi
state_login_redirect "$1"
RESULT=`curl ${CURL_OPTIONS} -X POST -H "Authorization: Basic ${basic_auth}" -H "Content-Type: application/x-www-form-urlencoded" -H "Accept: application/json" -d "grant_type=refresh_token&redirect_uri=${REDIRECT}&refresh_token=$4" https://$1${dash}${ENVIRONMENT}-dot-${PROJECT}.appspot.com/oauth2/token`
curl_print
}
#####################################################
# Generate COMMAND_LOOKUP and Usage #
#####################################################
# Generates the usage from COMMANDS and prints them.
print_usage() {
echo -e ${RED?}'Usage: admin <command>'${RESET?}
echo -e ${RED?}' commands:'${RESET?}
for cmd in "${!COMMANDS[@]}"; do
echo -e "${RED?} ${cmd}${RESET?}"
done | sort
echo
echo -e "${RED?}Note: some commands are only available in experimental mode${RESET?}"
echo
}
# Removes any input parameters from COMMANDS usage key. This is used to generate
# a lookup table of commands and match incoming commands with parameters like
# "<name>".
remove_key_params() {
words=( $1 )
keys=()
for word in "${words[@]}"; do
if [[ "${word:0:1}" != "<" ]]; then
keys+=( "${word}" )
fi
done
keys=$(printf " %s" ${keys[@]})
LOOKUP_KEY=${keys:1}
}
# Generates COMMAND_LOOKUP from COMMANDS by removing input parameters.
declare -A COMMAND_LOOKUP
for cmd in "${!COMMANDS[@]}"; do
remove_key_params "$cmd"
COMMAND_LOOKUP["${LOOKUP_KEY?}"]="${COMMANDS[${cmd}]}"
done
#####################################################
# Command Execution #
#####################################################
# Executes a command by expanding the input parameters from $1.
run_command() {
echo -e "${GREEN?}${PROJECT?} ${ENVIRONMENT?} ${REALM?}${RESET?}"
# Manually replace variables without eval() and strip quotes from args.
local line=$1
local re='(.*)(\$\{([^\}]+)\})(.*)'
while [[ $line =~ $re ]]; do
varname=${BASH_REMATCH[3]/\?/}
line="${BASH_REMATCH[1]}${!varname}${BASH_REMATCH[4]}"
done
args=()
declare -a "array=( $(echo ${line} | tr '`$<>' '????') )"
for arg in "${array[@]}"; do
arg=${arg/\"/}
arg=${arg/\"/}
args+=( $arg )
done
# Check allowlist to see if this command is authorized to limit risk from
# variables injecting bad stuff.
if [[ "${COMMAND_ALLOWLIST[${args[0]}]}" == "" ]]; then
echo -e "${RED?}command \"${args[0]}\" is not on the allowlist and therefore this command is unauthorized${RESET?}"
exit 2
fi
# Execute the allowlisted command with parameters.
${args[@]}
if [[ "$?" == "0" ]]; then
echo -e "${GREEN?}done${RESET?}"
else
echo -e "${RED?}command failed${RESET?}"
fi
echo
exit $?
}
# Prepare for processing input.
state_load
# Lookup the command from COMMANDS, but adjust for the last input being a
# variable if the first word in the command is "set".
lookup="$@"
if [[ "${lookup}" == "" ]]; then
print_usage
exit 1
fi
words=( $lookup )
if [[ "${words[0]}" == "set" ]]; then
words[-1]=""
lookup=$(printf " %s" ${words[@]})
lookup=${lookup:1}
fi
# Now we have the lookup string for commands in COMMAND_LOOKUP, so find what to
# execute ("exec") and if it is empty, there is no such command.
exec="${COMMAND_LOOKUP[$lookup]}"
if [[ "${exec}" != "" ]]; then
# Do substitutions here before the function call changes the args.
exec=${exec/\$3/$3}
exec=${exec/\$4/$4}
exec=${exec/\$5/$5}
exec=${exec/\$6/$6}
run_command "${exec}"
fi
# Not found. Try substituting <name> on every second parameter starting at 4th.
for ((i=3;i<${#words[@]};i=i+2)); do
words[i]="<name>"
done
lookup=$(printf " %s" ${words[@]})
lookup=${lookup:1}
remove_key_params "${lookup}"
exec="${COMMAND_LOOKUP[${LOOKUP_KEY?}]}"
if [[ "${exec}" != "" ]]; then
# Do substitutions here before the function call changes the args.
exec=${exec/\$3/$3}
exec=${exec/\$4/$4}
exec=${exec/\$5/$5}
exec=${exec/\$6/$6}
run_command "${exec}"
fi
# Fell through from the last 2 command lookups. Command not found. Print usage.
print_usage