SSO_Cutover.sh #11

  • //
  • guest/
  • tom_tyler/
  • sw/
  • main/
  • SSO_Cutover/
  • SSO_Cutover.sh
  • View
  • Commits
  • Open Download .zip Download (30 KB)
#!/bin/bash
set -u

#==============================================================================
# Declarations and Environment
declare ThisScript=${0##*/}
declare ThisUser=
declare ThisHost=${HOSTNAME%%.*}
declare Version=1.1.4
declare Args="$*"
declare CmdLine="$0 $Args"
declare Log=
declare Value=
declare H1="=============================================================================="
declare H2="------------------------------------------------------------------------------"

declare -i ErrorCount=0
declare -i WarningCount=0
declare -i NoOp=1
declare -i Debug=${DEBUG:-0}
declare -i i=0
declare -i ForcePasswordUpdates=0

declare -i DoConfigurableProcessing=1
declare -i DoExtensionProcessing=1
declare -i DoTriggerProcessing=1
declare -i DoUserProcessing=1
declare -i UserIsExempt=0
declare SwarmURL=
declare AccessLevel=
declare CaseHandling=
declare -A SeenGroups

declare SDPInstance=
declare SDPRoot="${SDP_ROOT:-/p4}"
declare SDPCommon="${SDPRoot}/common"
declare SDPCommonBin="${SDPCommon}/bin"
declare SDPCommonCfg="${SDPCommon}/config"
declare SDPCommonLib="${SDPCommon}/lib"
declare SDPCommonSiteBin="${SDPCommon}/site/bin"
declare SSOTriggerDeployed="$SDPCommonSiteBin/triggers/SSO_default.sh"
declare SSOTriggerSource="$SDPRoot/sdp/Unsupported/Samples/triggers/SSO_default.sh"
declare User=
declare UserAuthMethod=
declare UserPasswordSetKeyName=
declare UserPasswordSetKeyValue=
declare ExemptUsersGroup=
declare -i SkipPasswordSetCount=0
declare -i UsersToProcessCount=0
declare -i UsersProcessedOKCount=0
declare -i ExemptUsersToProcessCount=0
declare -a ExemptUsers
declare ExemptUser=
declare -i PrimaryUserExempt=0
declare UsersFile=
declare GroupUsersFile=
declare TriggersFile=
declare TempPasswordFile=
declare TempPassword=

# Color support.
declare GREEN=
declare RED=
declare YELLOW=
declare RESET=

#==============================================================================
# Local Functions

function msg () { echo -e "$*"; }
function msgn () { echo -e -n "$*"; }
function msg_green  { msg "${GREEN}$*${RESET}"; }
function msg_yellow { msg "${YELLOW}$*${RESET}"; }
function msg_red    { msg "${RED}$*${RESET}"; }

function dbg () { [[ "$Debug" -eq 0 ]] || echo -e "DEBUG: $*" >&2; }
function dbg2 () { [[ "$Debug" -ge 2 ]] && echo -e "DEBUG: $*" >&2; }
function errmsg () { msg_red "\nError: ${1:-Unknown Error}\n"; ErrorCount+=1; }
function warnmsg () { msg_yellow "\nWarning: ${1:-Unknown Warning}\n"; WarningCount+=1; }
function bail () { errmsg "${1:-Unknown Error}"; exit "${2:-1}"; }

#------------------------------------------------------------------------------
# Function get_all_p4_group_users
# This function gets all members of a p4 group, including direct  members as well
# as indirect members, i.e. members of subgroups.
function get_all_p4_group_users() {
    local group="${1:-}"
    [[ -n "$group" ]] || return
    [[ -v SeenGroups[$group] ]] && return
    SeenGroups[$group]=1

    local section=""
    while IFS= read -r line; do
        case "$line" in
            Users:*)      section="users" ;;
            Subgroups:*)  section="subgroups" ;;
            [A-Za-z]*:*)  section="" ;;
            $'\t'*)
                local val="${line#	}"  # strip leading tab
                [[ -z "$val" ]] && continue
                if   [[ "$section" == "users" ]];     then echo "$val"
                elif [[ "$section" == "subgroups" ]]; then get_all_p4_group_users "$val"
                fi
                ;;
        esac
    done < <("$P4BIN" group --exists -o "$1" 2>/dev/null)
}

#------------------------------------------------------------------------------
# Function: run ($cmdAndArgs, $desc, $honorNoOp, $showOutput)
#
# Runs a command, with optional description, showing command line to execute
# and optionally also the output, and capturing and returning the exit code.
# Honors the global NoOp unless honorNoOp is 0.
#
# Input:
# $1 - Command and arguments to execute. Defaults to 'echo'.
# $2 - Optional message to display describing what the command is doing.
# $3 - Honor NoOp mode. If set to one, honor the NoOp variable -- so display
#      rather than execute commands.
# $4 - Numeric flag to show output; '1' indicates to show output, 0 to
#      suppress it.
#------------------------------------------------------------------------------
function run () {
   local cmdAndArgs="${1:-echo}"
   local desc="${2:-}"
   local honorNoOp="${3:-1}"
   local -i showOutput="${4:-1}"
   local -i cmdReturnCode=0
   local log

   [[ -n "$desc" ]] && msg "$desc"
   msg "Executing: $cmdAndArgs"
   if [[ "$honorNoOp" -eq 1 && "$NoOp" -eq 0 ]]; then
      log="$(mktemp "${P4TMP:-/tmp}/run.${ThisScript%.sh}.XXXXXXXXXXX")"
      # shellcheck disable=SC2086
      eval $cmdAndArgs > "$log" 2>&1
      cmdReturnCode=$?
   else
      msg "NO_OP: Would run: $cmdAndArgs"
      cmdReturnCode=0

      # In NoOp mode, no command was executed, so there is
      # output to show.
      showOutput=0
   fi

   if [[ "$showOutput" -eq 1 ]]; then
      echo "EXIT_CODE: $cmdReturnCode" >> "$log"
      cat "$log"
   fi

   if [[ "$honorNoOp" -eq 0 && "$NoOp" -eq 0 ]]; then
      /bin/rm -f "$log"
   fi

   return "$cmdReturnCode"
}

#------------------------------------------------------------------------------
# Function: usage (required function)
#
# Input:
# $1 - style, either -h (for short form) or -man (for man-page like format).
# The default is -h.
#
# $2 - error message (optional).  Specify this if usage() is called due to
# user error, in which case the given message displayed first, followed by the
# standard usage message (short or long depending on $1).  If displaying an
# error, usually $1 should be -h so that the longer usage message doesn't
# obscure the error message.
#
# Sample Usage:
# usage
# usage -h
# usage -man
# usage -h "Incorrect command line usage."
#------------------------------------------------------------------------------
function usage
{
   local style=${1:--h}
   local usageErrorMessage=${2:-}

   # Set a default value for P4USER so we can run the '-man' option even on a
   # machine that does not have SDP properly installed. Any machine with SDP
   # installed will have P4USER set reliably.
   [[ -n "${P4USER:-}" ]] || P4USER=perforce

   [[ -n "$usageErrorMessage" ]] && msg "\n\nUsage Error:\n\n$usageErrorMessage\n\n"

   msg "USAGE for $ThisScript v$Version:

$ThisScript -g <GroupOfUsersExemptFromSSO> [-i <SDPInstance>] [-nc] [-ne] [-nt] [-nu] [-f] [-y] [-d|-D] [-L <Log>]

or

$ThisScript [-h|-man|-V]
"
   if [[ $style == -man ]]; then
      msg "
DESCRIPTION:
	This script supports the production cutover to enable Single Sign-On (SSO)
	using the Perforce Authentication Service (P4AS). The P4AS service is a
	bridge to your Identity Provider (IdP) system, e.g. Google OneLogin, Microsoft
	Entra, Okta, Perforce IdP, Ping Federate, etc.

	In a typical phased rollout of SSO, SSO is first deployed in a pilot phase
	in \"opt-in\" mode, where a few users are manually configured to use SSO.

	Then, after testing, the production rollout is done -- that's where this
	script comes in.  This script supports the production rollout process by
	changing a set of users to be ready for SSO rollout.  For the production
	rollout, this set of users is typically all human users.

	Digression: The P4 Server itself is not aware of \"human\" vs. \"non-human\"
	users.  The user spec does have a 'Type:' field, the value of which can have
	values of 'standard', 'service', or 'operator'. The distinction of 'standard'
	vs. 'service/operator' users is known to the P4 Server. However, 'service'
	and 'operator' users are extremely limited functionally, so much so that
	automated accounts are almost always of type 'standard' as far as the P4
	Server is concerned.

	To call this script, the name of a P4 group containing a list of exempt
	users must be provided.  This group must contain the user '$P4USER' and
	must also contain a list any others users that should not be configured
	for SSO. This typically is all non-human accounts such as CI/CD/DevOps
	automation, AI agents, etc. It may also contain any users that are not
	intended to use SSO for whatever reason (e.g. contractors who are not
	defined in your organization's IdP).

	The list of users to be processed is starts with the list of users
	reported by the 'p4 users' command without the '-a' option (thus naturally
	excluding users with a 'Type:' value of 'service' or 'operator', which
	cannot use SSO). Then the users in the exempt group are removed. The set
	of remaining users are configured for SSO.

	This script operates in these phases:

	Phase 0: Pre-flight checks.

	This phase evaluates readiness of the environment for the SSO cutover, and
	performs various checks. If any tests fail, further processing is aborted.
	Among the checks are:
	  - Verifies P4 super user access.
	  - Checks case-sensitivity of P4 Server.
	  - Ensures group of exempt users exists.
	  - Ensures group of exempt users contains SDP P4USER '$P4USER'.

	Phase 1: Process Configurables

	Check SSO configurables; set if needed:
	  - Set auth.sso.allow.passwd=1
	  - Set auth.sso.nonldap=1
	  - Set auth.default.method=perforce

   Phase 1 can be skipped with the '-nc' option.

	Phase 2: Process P4AS Extension

	Check the P4AS extension, add updated if needed:
	  - Ensure \"opt-in\" users/groups are NOT defined.
	  - Ensure \"opt-out\" group references exclusion group specified with '-g'.
	  - Ensure \"opt-out\" user is the P4USER '$P4USER'.

   Phase 2 can be skipped with the '-ne' option.

	Phase 3: Process Triggers

	Check triggers, add SSO_default.sh trigger if needed.
	  - Ensure SSO_default.sh trigger script is installed.
	  - Ensure SSO_default trigger is in the Triggers table.

	Phase 3 can be skipped with the '-nt' option.

	Phase 4: Processing Users

	For all non-exempt uesrs:
	  - Check AuthMethod, change to 'perforce' if needed.
	  - Set UUID password.

	Phase 4 can be skipped with the '-nu' option.

	By default, the password is set only once per user, even if this script is
	run multiple times.  Use '-f' to always set the password. A warning is
	displayed if the password reset is skipped becuase it had been set
	previously.  Keys named of the form '${ThisScript%.sh}.<NoOp>.<User>'
	are set when the password is set successfuly.  The '<NoOp>' value is '1'
	for a Dry Run and '0' for a Live Run, so that passwords set in Dry Run
	mode (which do NOT involve an actually password change) do not affect
	passwords set for the Live Run.

	Ideally, this script should be run exactly once in Live Run mode.  It is
	expected that a series of iterative Dry Runs may be needed to refine the
	set of users in the group of users excempt from SSO.

	Phase 5: P4 Code Review Update
	  - If P4.Swarm.URL is set, advise considering config.php update.

	There is no option to skip Phase 5 becuase it only displays optional
	advice; it takes no action.

SAFETY FEATURES:
	By default, this script operates in Dry Run (preview) mode, showing
	what it would do but not making any changes that affect data.  Run
	with '-y' to operate for real.

REQUIRED PARAMETERS:
 -g <GroupOfUsersExemptFromSSO>
	Specify the name of a group containing a list of users that are not to be
	configured for to SSO.  This group must exist and must contain at least the
	the '$P4USER' user.

	This parameter is required.

OPTIONS:
 -i <SDPInstance>
	Specify the SDP instance. If not specified, the \$SDP_INSTANCE variable from
	the shell environment is used.

 -nc	Specify '-nc' to skip SSO Configurables processing.

 -ne	Specify '-ne' to skip SSO Extension processing.

 -nt	Specify '-nt' to skip SSO Trigger processing.

 -nu	Specify '-nu' to skip SSO User processing.

 -f	Specify that passwords for users that have already been set to a UUID password
	are to be reset anyway.

 -y	Live operation mode.  By default, any commands that affect data, such as
	setting configurables, are displayed, but not executed.  With the '-y' option,
	commands affecting data may be executed.

HELP OPTIONS:
 -h	Display short help message.
 -man	Display man-style help message.
 -V	Display script name and version.

LOGGING AND DEBUGGING OPTIONS:
 -L <log>
	Specify the path to a log file, or the special value 'off' to disable
	logging.  By default, all output (stdout and stderr) goes to a log file
	pointed to by a symlink:

	\$LOGS/${ThisScript%.sh}.log

	The symlink is for convenience. It refers to the log from the most recent
	run if where '-L' was not used.

	Each time this script is run, a new timestamped log is started, and
	the symlink updated to reference the new/latest log during startup.

	NOTE: This script is self-logging.  That is, output displayed on the screen
	is simultaneously captured in the log file. Using redirection operators like
	'> log' or '2>&1' are unnecessary, as is using 'tee' (though using 'tee'
	or additional redirects will not interfere with the script).

 -d	Display debug messages.
 
 -D     Set extreme debugging verbosity using bash 'set -x' mode. Implies -d.

EXAMPLES:
	Example 1: Dry Run with debug-level verbosity.

	$ThisScript -g Non-SSO -d

	Example 2: Production Cutover

	$ThisScript -g Non-SSO -y
"
   fi

   exit 2
}

#------------------------------------------------------------------------------
# Function: terminate
# shellcheck disable=SC2317 disable=SC2329
function terminate ()
{
   # Disable signal trapping.
   trap - EXIT SIGINT SIGTERM

   [[ "$Log" == "off" ]] || msg "\nLog is: $Log\n${H1}"

   # With the trap removed, exit.
   exit "$ErrorCount"
}

#==============================================================================
# SDP Library Functions

if [[ -d "$SDPCommonLib" ]]; then
   # shellcheck disable=SC1090 disable=SC1091
   source "$SDPCommonLib/logging.lib" ||\
      bail "Failed to load bash lib [$SDPCommonLib/logging.lib]. Aborting."
fi

#==============================================================================
# Command Line Processing

declare -i shiftArgs=0

set +u
while [[ $# -gt 0 ]]; do
   case $1 in
      (-h) usage -h;;
      (-man) usage -man;;
      (-V|-version|--version) msg "$ThisScript version $Version"; exit 0;;
      (-g) ExemptUsersGroup="$2"; shiftArgs=1;;
      (-nc) DoConfigurableProcessing=0;;
      (-ne) DoExtensionProcessing=0;;
      (-nt) DoTriggerProcessing=0;;
      (-nu) DoUserProcessing=0;;
      (-i) SDPInstance="$2"; shiftArgs=1;;
      (-L) Log="$2"; shiftArgs=1;;
      (-f) ForcePasswordUpdates=1;;
      (-y|--yes) NoOp=0;;
      (-d) Debug=1;;
      (-d2) Debug=2;;
      (-D) Debug=2; set -x;; # Use bash 'set -x' extreme debug mode.
      (-*) usage -h "Unknown option ($1).";;
      (*) usage -h "Extra parameter [$1] is unknown.";;
   esac

   # Shift (modify $#) the appropriate number of times.
   shift; while [[ $shiftArgs -gt 0 ]]; do
      [[ $# -eq 0 ]] && usage -h "Incorrect number of arguments."
      shiftArgs=$shiftArgs-1
      shift
   done
done
set -u

#==============================================================================
# Command Line Verification

[[ -z "$SDPInstance" ]] && SDPInstance="${SDP_INSTANCE:-}"
[[ -z "$SDPInstance" ]] && \
   usage -h "The SDP instance parameter is required unless SDP_INSTANCE is set. To set SDP_INSTANCE, do:\n\tsource $SDPCommonBin/p4_vars INSTANCE\n\nreplacing INSTANCE with your SDP instance name."

SDPInstanceVars="$SDPCommonCfg/p4_${SDPInstance}.vars"
[[ -r "$SDPInstanceVars" ]] || \
   usage -h "The SDP instance specified [$SDPInstance] is missing Instance Vars file: $SDPInstanceVars"

[[ -z "$ExemptUsersGroup" ]] && \
   usage -h "The '-g <GroupOfUsersExemptFromSSO>' parameter is required; it was not specified."

# shellcheck disable=SC1090 disable=SC1091
source "$SDPCommonBin/p4_vars" "$SDPInstance" ||\
   bail "Could not do: source \"$SDPCommonBin/p4_vars\" \"$SDPInstance\""

#==============================================================================
# Main Program

trap terminate EXIT SIGINT SIGTERM

# Detect support for colors
if command -v tput >/dev/null 2>&1 && [[ -t 1 ]] && [[ "$(tput colors)" -ge 8 ]]; then
   RED="$(tput setaf 1)" 
   GREEN="$(tput setaf 2)"
   YELLOW="$(tput setaf 3)"
   RESET="$(tput sgr0)"
else
   RED=; GREEN=; YELLOW=; RESET=
fi

# If the user specifies a log file file with '-L', write to the specified file.
# If no log was specified, create a default log file using a timestamp in the
# LOGS dir, and immediately update the symlink for the default log to point to
# it.
if [[ "$Log" != off ]]; then
   # If $Log is not yet defined, set it to a reasonable default.
   if [[ -z "$Log" ]]; then
      LogTimestamp=$(date +'%Y-%m-%d-%H%M%S')
      Log="$LOGS/${ThisScript%.sh}.${LogTimestamp}.log"
      # Make sure we have a unique log file. Prefer a human-readable timestamp
      # using hours/minutes/seconds. Append milliseconds if needed to ensure
      # a unique filename.
      while [[ -e "$Log" ]]; do
         LogTimestamp=$(date +'%Y-%m-%d-%H%M%S.%3N')
         Log="$LOGS/${ThisScript%.sh}.${LogTimestamp}.$i.log"
         i+=1
      done
   fi
   # The LogLink symlink has no timestamp. It points to the most recent log file.
   LogLink="$LOGS/${ThisScript%.sh}.log"

   if [[ -e "$LogLink" ]]; then
      if [[ -L "$LogLink" ]]; then
         rm -f "$LogLink"
      else
         # If the name that should be a symlink is not a symlink, move it aside before
         # creating the symlink.
         OldLogTimestamp=$(get_old_log_timestamp "$LogLink")
         mv -f "$LogLink" "${LogLink%.log}.${OldLogTimestamp}.log" ||\
            bail "Could not move old log file aside; tried: mv -f \"$LogLink\" \"${LogLink%.log}.${OldLogTimestamp}.log\""
      fi
   fi

   touch "$Log" || bail "Couldn't touch new log file [$Log]."

   # Use a subshell so the 'cd' doesn't persist.
   ( cd "$LOGS"; ln -s "${Log##*/}" "${LogLink##*/}"; ) ||\
       bail "Couldn't initialize log symlink; tried: ln -s \"$Log\" \"$LogLink\""

   # Redirect stdout and stderr to a log file.
   if [[ -n "$GREEN" ]]; then
      exec > >( tee \
         >(sed -r \
         -e 's/\x1B\[[0-9;]*[a-zA-Z]//g' \
         -e 's/\x1B\(B//g' >>"$Log"))
      exec 2>&1
   else
      exec > >(tee "$Log")
      exec 2>&1
   fi

   msg "${H1}\nLog is: $Log\n"
fi

ThisUser=$(id -n -u)
msg "Starting $ThisScript v$Version as $ThisUser@$ThisHost on $(date) with \n$CmdLine"

if [[ "$NoOp" -eq 0 ]]; then
   msg "This is Live Operation Mode."
else
   msg "This is a Dry Run/Preview Mode."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 0: Pre-flight checks."

dbg "Getting case handling with: p4 -ztag -F %caseHandling% info -s"
CaseHandling="$("$P4BIN" -ztag -F %caseHandling% info -s)"

if [[ "$CaseHandling" =~ ^sensitive$ ]]; then
   dbg "Case-handling mode detected as sensitive."
elif [[ "$CaseHandling" =~ ^insensitive$ ]]; then
   dbg "Case-handling mode detected as insensitive."
else
   errmsg "Could not determine case handling; mode is [$CaseHandling]."
fi

GroupUsersFile="$(mktemp "${P4TMP:-/tmp}"/group.XXXXXXXX.p4s)"

dbg "Getting members of $ExemptUsersGroup ..."
get_all_p4_group_users "$ExemptUsersGroup" > "$GroupUsersFile"

if [[ -s "$GroupUsersFile" ]]; then
   msg_green "Verified: Group '$ExemptUsersGroup' containing users exempt from SSO exists."
else
   rm -f "$GroupUsersFile"
   errmsg "Could not verify that group '$ExemptUsersGroup' exists."
fi 

PrimaryUserExempt=0

while read -r User; do
   if [[ "$CaseHandling" == sensitive ]]; then
      [[ "$User" == "$P4USER" ]] && PrimaryUserExempt=1
   else
      # If P4 Server is case-insensitive, do a case-insensitive user comparison.
      [[ "${User^^}" == "${P4USER^^}" ]] && PrimaryUserExempt=1
   fi
   ExemptUsers[ExemptUsersToProcessCount]="$User"
   ExemptUsersToProcessCount+=1
done < "$GroupUsersFile"

[[ "$Debug" -eq 0 ]] && rm -f "$GroupUsersFile"

if [[ "$PrimaryUserExempt" -eq 1 ]]; then
   msg_green "Verified: Primary user '$P4USER' is among the exempt users."
else
   errmsg "Could not verify that the primary user $P4USER is in the exempt users group '$ExemptUsersGroup'. Aborting."
fi

msg "There are $ExemptUsersToProcessCount SSO-exempt users."

ExtensionFileOld=$(mktemp "${P4TMP:-/tmp}"/p4as_extension.XXXXXXXX.p4s)
ExtensionFileNew=$(mktemp "${P4TMP:-/tmp}"/p4as_extension.XXXXXXXX.p4s)
"$P4BIN" extension --configure Auth::loginhook --name loginhook-a1 -o | grep -v ^# > "$ExtensionFileOld" 2>/dev/null

if grep -q '^ExtUUID:' "$ExtensionFileOld" 2>/dev/null; then
   msg_green "Verified: P4AS Extension exists."
else
   errmsg "The P4AS extension does not exist."
fi

AccessLevel=$("$P4BIN" protects -m)

if [[ "$AccessLevel" == super ]]; then
   msg_green "Verified: Access Level is super, as required."
else
   errmsg "Access Level is '$AccessLevel'; super is required."
fi

if [[ "$ErrorCount" -eq 0 ]]; then
   msg_green "\nAll Pre-flight checks passed. Proceeding."
else
   errmsg "Aborting due to failed pre-flight checks."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 1: Setting SSO-related configurables.\n"

if [[ "$DoConfigurableProcessing" -eq 1 ]]; then
   for c in auth.sso.allow.passwd auth.sso.nonldap; do
      Value=$("$P4BIN" -ztag -F %Value% configure show "$c")

      if [[ "$Value" == 0 ]]; then
         run "p4 configure set $c=1" \
            "\nSetting configurable $c."
      else
         msg_green "Verified: $c is set to $Value."
      fi
   done

   Value=$("$P4BIN" -ztag -F %Value% configure show auth.default.method)
   if [[ "$Value" != perforce ]]; then
      run "p4 configure set auth.default.method=perforce" \
         "\nSetting configurable auth.default.method=perforce."
   else
      msg_green "Verified: auth.default.method is set to perforce."
   fi
else
   msg "Skipping configurable processing due to '-nc'."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 2: Changing P4AS extension to opt-out mode.\n"

if [[ "$DoExtensionProcessing" -eq 1 ]]; then
   export ExemptUsersGroup P4USER

   perl -0777 -pe '
      # 1. Replace sso-groups: and all following indented lines
      s/(sso-groups:\n)(\t\t[^\n]*\n)+/${1}\t\t... (none)\n/g;

      # 2. Replace sso-users: and all following indented lines
      s/(sso-users:\n)(\t\t[^\n]*\n)+/${1}\t\t... (none)\n/g;

      # 3. Target non-sso-groups: and ONLY the single line immediately following it
      s/(non-sso-groups:\n)\t\t[^\n]*\n/${1}\t\t$ENV{ExemptUsersGroup}\n/g;

      # 4. Target non-sso-users: and ONLY the single line immediately following it
      s/(non-sso-users:\n)\t\t[^\n]*\n/${1}\t\t$ENV{P4USER}\n/g;
    ' "$ExtensionFileOld" > "$ExtensionFileNew"

   if grep -q "$P4USER" "$ExtensionFileNew" 2>/dev/null; then
      msg_green "Sanity check on new Extension config is AOK."

      msg "Extension changes to be made:"
      diff "$ExtensionFileOld" "$ExtensionFileNew" 

      msg "Updating Extension to change from opt-in to opt-out mode."

      if [[ "$NoOp" -eq 0 ]]; then
         dbg "Running: $P4BIN extension --configure Auth::loginhook --name loginhook-a1 -i"
         dbg "Contents of updated extension:\n$(cat "$ExtensionFileNew")"

         if "$P4BIN" extension --configure Auth::loginhook --name loginhook-a1 -i < "$ExtensionFileNew"; then
            rm -f "$ExtensionFileOld" "$ExtensionFileNew"
         else
            errmsg "Error updating extension with this new config: $ExtensionFileNew"
         fi
      else
         msg "NO_OP: Would run: $P4BIN extension --configure Auth::loginhook --name loginhook-a1 -i"
         dbg "Contents of updated extension:\n$(cat "$ExtensionFileNew")"
      fi
   else
      errmsg "This updated Extension config failed a sanity check:"
      cat "$ExtensionFileNew"
   fi
else
   msg "Skipping extension processing due to '-ne'."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 3: Processing Triggers."

if [[ "$DoTriggerProcessing" -eq 1 ]]; then
   TriggersFile=$(mktemp "${P4TMP:-/tmp}"/triggers.XXXXXXXX.p4s)
   dbg "Extracting triggers with: p4 triggers -o | grep -v ^# | grep -v ^Update: > $TriggersFile"
   "$P4BIN" triggers -o | grep -v ^# | grep -v ^Update: | sed '$d' > "$TriggersFile"

   if grep -q SSO_default "$TriggersFile"; then
      msg_green "Verified; SSO_default trigger is defined in Triggers table."
   else
      echo -e "\tSSO_default form-save user \"$SSOTriggerDeployed %formfile% none\"\n\tSSO_default form-commit user \"$SSOTriggerDeployed %formfile% none\"\n" >> "$TriggersFile"

      run "p4 -s triggers -i < $TriggersFile" \
         "Updating Triggers table to add SSO_default triggers." ||\
         bail "Failed to append SSO_default trigger entries to Trigger table using file: $TriggersFile. Aborting"
   fi

   if [[ -x "$SSOTriggerDeployed" ]]; then
      msg_green "Verified: Trigger script is executable and installed as: $SSOTriggerDeployed"
   else
      if [[ -r "$SSOTriggerSource" ]]; then
         msg "Installing SSO_default trigger."
         run "mkdir -p ${SSOTriggerDeployed%/*}" "Ensuring triggers dir '${SSOTriggerDeployed%/*}' exists." ||\
            warnmsg "Attempt to create triggers dir failed; this may lead to more failures."
         run "cp -f $SSOTriggerSource $SSOTriggerDeployed" "Installing SSO_default trigger." ||\
            errmsg "Failed to copy SSO_default trigger."
         run "chmod +x $SSOTriggerDeployed" "chmod +x $SSOTriggerDeployed" ||\
            errmsg "Failed to do: chmod +x $SSOTriggerDeployed"
      else
         warnmsg "Could not find trigger source: $SSOTriggerSource; skipping install."
      fi
   fi

   [[ "$Debug" -eq 0 ]] && rm -f "$TriggersFile"
else
   msg "Skipping trigger processing due to '-nt'."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 4: Processing Users."

if [[ "$DoUserProcessing" -eq 1 ]]; then

   UsersFile="$(mktemp "${P4TMP:-/tmp}"/users.XXXXXXXX.txt)"
   dbg "Determining list of all users of type 'standard' with: p4 -ztag -F %User% users > $UsersFile"

   # If there is any stderr, allow it to flow thru to the log, don't redirect
   # to the temp file. We can safely assume there will be no stderr if the
   # exit code is zero.
   "$P4BIN" -ztag -F %User% users > "$UsersFile" ||\
      bail "Could not get list of users with: $P4BIN -ztag -F %User% users. Aborting."

   while read -r User; do
      dbg2 "Considering user [$User]."
      UserIsExempt=0
      for ExemptUser in "${ExemptUsers[@]}"; do
         if [[ "$ExemptUser" == "$User" ]]; then
            UserIsExempt=1
            break
         fi
      done

      if [[ "$UserIsExempt" -eq 1 ]]; then
         dbg "Skipping exempt user '$User'."
         continue
      fi

      msg "\nProcessing user '$User'."
      UsersToProcessCount+=1

      UserAuthMethod=$("$P4BIN" -ztag -F %AuthMethod% user -o "$User")

      if [[ "$UserAuthMethod" == perforce ]]; then
         msg_green "Verified: AuthMethod for user '$User' is already 'perforce'."
      else
         msg "Changing AuthMethod for user '$User' from '$UserAuthMethod' to 'perforce'."
         UserFile="$(mktemp "${P4TMP:-/tmp}"/user.XXXXXXXX.p4s)"
         "$P4BIN" --field AuthMethod=perforce user -o "$User" > "$UserFile"
         if run "$P4BIN user -f -i < $UserFile" \
            "Setting AuthMethod for user '$User' to 'perforce'."; then
            dbg "AuthMethod set OK for user '$User'."

         else
            errmsg "Failed to set AuthMethod for user '$User' to 'perforce'."
            continue
         fi
      fi

      # Use 'p4 keys' data to track whether the password has already been set.
      UserPasswordSetKeyName="${ThisScript%.sh}.$NoOp.$User"
      UserPasswordSetKeyValue=$("$P4BIN" key "$UserPasswordSetKeyName")
      if [[ "$UserPasswordSetKeyValue" == 1 ]]; then
         if [[ "$ForcePasswordUpdates" -eq 0 ]]; then
            warnmsg "UUID password for user '$User' was set previously; not setting it again. Use '-f' to force reset."
            UsersProcessedOKCount+=1
            SkipPasswordSetCount+=1
            continue
         else
            warnmsg "UUID password for user '$User' was set previously; setting it again due to '-f'."
         fi
      fi

      UserPasswordSetKeyValue=$((UserPasswordSetKeyValue+1))
      TempPassword=$(uuidgen 2>/dev/null)
      [[ -n "$TempPassword" ]] || TempPassword="$RANDOM$RANDOM$RANDOM"
      TempPasswordFile=$(mktemp)
      touch "$TempPasswordFile"
      chmod 600 "$TempPasswordFile"
      echo -e "$TempPassword" > "$TempPasswordFile"
      echo -e "$TempPassword" >> "$TempPasswordFile"

      if run "$P4BIN passwd $User < $TempPasswordFile" "Setting password for user '$User'"; then
         # We don't use 'run()' for this 'p4 key' command; just set the key.
         # The key name includes the '$NoOp' setting, so key values from
         # Dry Runs do not interact with those from Live Runs.
         # Undoc feature: The key counts the number of times the password was
         # reset for each user (if '-f' was used). Not sure if this is useful
         # but it's easy to track.
         "$P4BIN" key "$UserPasswordSetKeyName" "$UserPasswordSetKeyValue"
         UsersProcessedOKCount+=1
      else
         errmsg "Failed to set password for user '$User'."
      fi

      rm -f "$TempPasswordFile"
      TempPassword=
   done < "$UsersFile"

   [[ "$Debug" -eq 0 ]] && rm -f "$UsersFile"
else
   msg "Skipping user processing due to '-nu'."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 5: P4 Code Revew Updates."

SwarmURL=$("$P4BIN" property -n P4.Swarm.URL -l)

if [[ -n "$SwarmURL" ]]; then
   msg "P4 Code Review appears to be operating in this environment with URL: $SwarmURL\n"
   msgn "Becuase P4 Code Review is used, "
else
   msg "P4 Code Review does not appear to be operating in this environment; the P4.Swarm.URL property is not yet.\n"
   msgn "If P4 Code Review is used, "
fi

msg "update the config.php on the Swarm\nserver by adding the 'sso' block to the 'p4' array. See:\nhttps://help.perforce.com/helix-core/helix-swarm/swarm/current/Content/Swarm/admin-saml_php_config.html"

#------------------------------------------------------------------------------
msg "${H2}\nSummary:
   Users To Process:    $UsersToProcessCount
     Processed OK:      $UsersProcessedOKCount
     Passwords Skipped  $SkipPasswordSetCount

   Errors:              $ErrorCount
   Warnings:            $WarningCount
"

if [[ "$ErrorCount" -eq 0 && "$WarningCount" -eq 0 ]]; then
   msg_green "Success: Processing completed with no errors or warnings."

elif [[ "$ErrorCount" -eq 0 ]]; then
   warnmsg "Processing completed with no errors, but $WarningCount warnings were encountered."
else
   errmsg "Processing completed, but $ErrorCount errors and $WarningCount warnings were encountered."
fi

#------------------------------------------------------------------------------
# See the terminate() function where this script really exits.
exit "$ErrorCount"
# Change User Description Committed
#11 32462 C. Thomas Tyler Fixed so docs can be generated on Mac.
#10 32461 C. Thomas Tyler Added Phase 5 for Swarm changes.
 This phase takes no new action; it's just
  a reminder to update P4 Code Review if needed.
#9 32456 C. Thomas Tyler Bug fixes during QA.
#8 32455 C. Thomas Tyler Refined Perl regex.
#7 32454 C. Thomas Tyler Added code to change Extension from "opt-in" for PoC to "opt-out"
for full production rollout of P4AS/SSO.
#6 32453 C. Thomas Tyler Fixed path to SSO_default.sh trigger file.
#5 32451 C. Thomas Tyler Fixed so case-handling behavior of P4 Server affects whether
the comparison of the exempt user.

Added docs about pre-flight checks.
#4 32450 C. Thomas Tyler Added handling of auth.default.method.

Added logic to avoid setting passwords more than once in Dry Run and
separately in Live modes, with '-f' option to override.
#3 32449 C. Thomas Tyler Added logic to skip setting of passwords that were already skipped.
#2 32448 C. Thomas Tyler Semi-working version.
#1 32447 C. Thomas Tyler WIP version added.