#!/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&amp;redirect_uri=${REDIRECT}&amp;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
