#!/bin/bash
set -u
# For usage, run: enable_sso_for_ap_users.sh -man
#==============================================================================
# Declarations and Environment
declare ThisScript=${0##*/}
declare Version=1.0.7
declare ThisUser=
declare Args="$*"
declare CmdLine="$0 $Args"
declare ThisHost=${HOSTNAME%%.*}
declare -i Debug=${SDP_DEBUG:-0}
declare -i ErrorCount=0
declare -i WarningCount=0
declare -i SilentMode=0
declare -i Found=0
declare Log=
declare H1="=============================================================================="
declare H2="------------------------------------------------------------------------------"
declare SDPInstance=${SDP_INSTANCE:-}
declare SDPRoot=${SDP_ROOT:-/p4}
declare SDPCommon="$SDPRoot/common"
declare SDPCommonBin="$SDPCommon/bin"
#declare SDPCommonLib="$SDPCommon/lib"
declare SDPCommonLib=/p4/common/site/sso_cutover
declare SDPEnv="$SDPCommonBin/p4_vars"
declare UserListFile=
declare NewUserListFile=
declare UserSpecFile=
declare PasswordFile=
declare Password=
declare -a UsersToIgnore
declare -i UsersToIgnoreCount=0
# Color support for interactive operation.
declare GREEN=
declare RED=
declare YELLOW=
declare RESET=
declare -i NoOp=0
#==============================================================================
# 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 ]] || msg "DEBUG: $*"; }
function dbg2 () { [[ "$Debug" -lt 2 ]] || msg "DEBUG: $*"; }
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 show_users_in_group () {
local group="$1"
local -a users=
# Global array of users to ignore.
UsersToIgnore=()
# Local reusable array of users in a group.
users=()
# Extract Users: section safely
while IFS= read -r line; do
# Stop when we hit a non-indented line after Users:
[[ "$line" =~ ^[^[:space:]] ]] && break
# Skip blank lines
[[ -z "$line" ]] && continue
# Trim leading whitespace
user="${line#"${line%%[![:space:]]*}"}"
users+=("$user")
done < <(
"$P4BIN" group -o "$group" | awk '
/^Users:/ {in_users=1; next}
in_users && /^[^[:space:]]/ {exit}
in_users {print}
'
)
printf '%s\n' "${users[@]}"
}
#------------------------------------------------------------------------------
# This function updates the globals UsersToIgnore and UsersToIgnoreCount.
function build_list_of_ignored_users () {
local extensionSpecFile=
local group=
local groupList=
local user=
local userList=
extensionSpecFile="$(mktemp "${P4TMP}/p4as.XXXXXX.extension.p4s")"
dbg "Running: $P4BIN extension --configure Auth::loginhook --name loginhook-a1 -o .GT. $extensionSpecFile"
if "$P4BIN" extension --configure Auth::loginhook --name loginhook-a1 -o > "$extensionSpecFile"; then
dbg2 "Extension spec file is: $extensionSpecFile"
groupList=$(grep -A1 'non-sso-groups:' "$extensionSpecFile"|tail -1)
#shellcheck disable=SC2086 disable=SC2116
groupList=$(echo $groupList)
userList=$(grep -A1 'non-sso-users:' "$extensionSpecFile"|tail -1)
#shellcheck disable=SC2086 disable=SC2116
userList=$(echo $userList)
dbg2 "SSO-exempt group list: [$groupList]"
dbg2 "SSO-exempt user list: [$userList]"
for group in $groupList; do
for user in $(show_users_in_group "$group"); do
dbg2 "Adding user $user from group $group to list of users to ignore."
UsersToIgnore[UsersToIgnoreCount]="$user"
UsersToIgnoreCount+=1
done
done
for user in $userList; do
dbg2 "Adding user $user to list of users to ignore."
UsersToIgnore[UsersToIgnoreCount]="$user"
UsersToIgnoreCount+=1
done
dbg "There are ${#UsersToIgnore[@]} users to ignore."
[[ "$Debug" -eq 0 ]] && rm -f "$extensionSpecFile"
else
errmsg "Could not read P4AS extension config."
return 1
fi
return 0
}
#==============================================================================
# Load 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."
# shellcheck disable=SC1090 disable=SC1091
source "$SDPCommonLib/run.lib" ||\
bail "Failed to load bash lib [$SDPCommonLib/run.lib]. Aborting."
fi
#------------------------------------------------------------------------------
# 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 a
# 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 "Missing required parameter <RequiredParameter>."
#------------------------------------------------------------------------------
function usage
{
local style=${1:--h}
local usageErrorMessage=${2:-Unset}
if [[ "$usageErrorMessage" != Unset ]]; then
msg "\n\nUsage Error:\n\n$usageErrorMessage\n\n"
fi
msg "USAGE for $ThisScript v$Version:
$ThisScript [-i <instance>] [-si] [-n] [-d|-d2|-D]
or
$ThisScript [-h|-man|-V]
"
if [[ $style == -man ]]; then
msg "
DESCRIPTION:
This script enables auto-provisioned users for SSO.
Auto-provisoned users are those created with 'p4 ldapsync -c -u -U' (or
similar).
The SSO_default.sh trigger script enables SSO for manually created users;
this script does the same for auto-provisioned users.
This is intended to be run occaisonally in cron, perhaps every half hour
with weekly log cleanup, with crontab entries like examples:
0,30 * * * * [ -e /p4/common/bin ] && /p4/common/bin/run_if_master.sh 1 /p4/common/site/sso_cutover/enable_sso_for_ap_users.sh -si
15 * * * 6 [ -e /p4/common/bin ] && /p4/common/bin/run_if_master.sh 1 rm -f /p4/1/logs/enable_sso_for_ap_users.log
This crontab is installed by the SSO_Cutover.sh script.
This script appends to this same log each time it runs:
$LOGS/${ThisScript%.sh}.log
An optional sample crontab wipes the log each week.
OPTIONS:
-i <sdp_instance>
Specify the SDP Instance. If not specified, the \$SDP_INSTANCE variable is
referenced, otherwise the default is '1'.
-si
Operate silently. All output (stdout and stderr) are redirected to the log
only; no output appears on the terminal (except for help/usage info, usage
errors, or version checks on startup). The '-si' option cannot be used with
'-L off'.
This is intended to be used when running scripts from the crontab. By
preventing any output, it prevents email from being sent by cron daemon
directly, as cron does when a script called from cron generates any output.
This script is then responsible for email handling or other notifications, if
any is to be done.
-n
Enable DRY RUN/Preview mode, displaying commands that would affect data
rather than executing them.
-d Enable higher verbosity for debugging.
-d2 Enable even higher verbosity for debugging than '-d.
-D
Set extreme debugging verbosity using bash 'set -x' mode.
HELP OPTIONS:
-h Display short help message.
-man Display man-style help message.
-V Display version info for this script.
LOGGING:
All output (stdout and stderr) goes to:
$LOGS/${ThisScript%.sh}.log
NOTE: This script is self-logging. That is, output displayed on the screen
is simultaneously captured in the log file. Using redirection operators like
like '> log' or '2>&1' or using 'tee' are unnecessary (but harmless).
FILES:
Persistent log: $LOGS/${ThisScript%.sh}.log
EXAMPLES:
Example 1: Manual preview usage:
${ThisScript} -n
Example 2: Crontab usage:
${ThisScript} -si
SEE ALSO:
See /p4/common/site/bin/SSO_default.sh
See /p4/common/site/sso_cutover/SSO_Cutover.sh
"
fi
exit 2
}
#==============================================================================
# Command Line Processing
declare -i ShiftArgs=0
set +u
while [[ $# -gt 0 ]]; do
case $1 in
(-h) usage -h;;
(-man|--help) usage -man;;
(-V|--version) msg "$ThisScript version $Version"; exit 0;;
(-si) SilentMode=1;;
(-n) NoOp=1;;
(-d) Debug=1;;
(-d2) Debug=2;;
(-D) Debug=1; set -x;; # Use bash 'set -x' extreme debug mode.
(-*) usage -h "Unknown option ($1).";;
(*) usage -h "Unknown arg ($1).";;
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
# shellcheck disable=SC1090
source "$SDPEnv" "$SDPInstance" ||\
bail "Could not do: source \"$SDPEnv\" \"$SDPInstance\""
#==============================================================================
# Command Line Verification
[[ "$SilentMode" -eq 1 && "$Log" == off ]] && \
usage -h "Cannot use '-si' with '-L off'."
#==============================================================================
# Main Program
trap terminate EXIT SIGINT SIGTERM
# Detect support for colors
if [[ $SilentMode -eq 0 ]] \
&& 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 [[ "$Log" != off ]]; then
# If $Log is not yet defined, set it to a reasonable default.
if [[ -z "$Log" ]]; then
[[ -d "${LOGS:-}" ]] ||\
bail "Log directory does not exist: [${LOGS:-unset}]. Verify SDP_ROOT [$SDPRoot] and SDP instance [$SDPInstance] are correct."
Log="$LOGS/${ThisScript%.sh}.log"
fi
Log="$LOGS/${ThisScript%.sh}.log"
# Redirect stdout and stderr to a log file. When redirecting to a log,
# suppress color codes because they detract from readability in logs.
if [[ "$SilentMode" -eq 0 ]]; then
if [[ -n "$GREEN" ]]; then
exec > >( tee -a \
>(sed -r \
-e 's/\x1B\[[0-9;]*[a-zA-Z]//g' \
-e 's/\x1B\(B//g' >>"$Log"))
else
exec > >(tee -a "$Log")
fi
exec 2>&1
else
exec >>"$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 "\nOperating in Live Operation mode."
else
msg "\nOperating in DRY RUN (Preview) mode."
fi
# The User List file is an optimization. We create the user list file if it doesn't exist,
# and it persists between runs on any given machine. (After a failover, it'll just get
# recreated on the next run of this script).
UserListFile="${P4TMP}/${ThisScript%.sh}.user_list.txt"
if [[ -r "$UserListFile" ]]; then
msg "Prior user list file is: $UserListFile"
else
if "$P4BIN" -ztag -F %User% users > "$UserListFile"; then
msg "Generated user list file: $UserListFile"
warnmsg "No further processing due to lack of pre-existing user list file. On the next run, checks will commence."
exit 0
else
# Don't leave incomplete cruft text around.
rm -f "$UserListFile"
bail "Failed to generate user list file '$UserListFile'."
fi
fi
NewUserListFile="$(mktemp "${P4TMP}/new_user_list.XXXXXXX.txt")"
if "$P4BIN" -ztag -F %User% users > "$NewUserListFile"; then
msg "Generated new user list file: $NewUserListFile"
else
# Don't leave incomplete cruft text around.
rm -f "$NewUserListFile"
bail "Failed to generate new user list file '$NewUserListFile'."
fi
build_list_of_ignored_users ||\
bail "Failed to build list of exempt users from P4AS extension info."
while read -r User; do
if grep -q -E "^$User$" "$UserListFile"; then
dbg2 "Ignoring pre-existing user $User."
else
dbg2 "Possible new user detected: [$User]"
UserSpecFile="$(mktemp "${P4TMP}.user.XXXXXXX.p4s")"
if "$P4BIN" -ztag -F %AuthMethod%:%User% user --exists -o "$User" > "$UserSpecFile"; then
if grep -q ^ldap: "$UserSpecFile"; then
dbg2 "AuthMethod for new user [$User] is LDAP. Checking SSO-exempt user list."
Found=0
for u in "${UsersToIgnore[@]}"; do
if [[ "$u" == "$User" ]]; then
Found=1
break
fi
done
if [[ "$Found" -eq 1 ]]; then
dbg2 "User [$User] is exempt from SSO; ignoring."
else
dbg "Detected new auto-provisioned user [$User]. Fixing ..."
if [[ "$NoOp" -eq 0 ]]; then
"$P4BIN" --field AuthMethod=perforce user -o "$User" | p4 user -fi
else
msg "NO_OP: Would do: $P4BIN --field AuthMethod=perforce user -o $User | p4 user -fi"
fi
PasswordFile=$(mktemp)
Password=$(uuidgen)
if echo -e "$Password\\n$Password" > "$PasswordFile"; then
if "$P4BIN" passwd "$User" < "$PasswordFile"; then
msg "SSO user [$User] now has unusable P4PASSWD."
else
errmsg "Failed to set UUID P4PASSWD for user [$User]."
fi
else
errmsg "Failed to create temp password file for user [$User]."
fi
rm -f "$PasswordFile"
fi
else
dbg2 "AuthMethod for new user [$User] is not LDAP. Ignoring this user."
fi
[[ "$Debug" -eq 0 ]] && rm -f "$UserSpecFile"
else
errmsg "Unable to extract user spec for user [$User]."
[[ "$Debug" -eq 0 ]] && rm -f "$UserSpecFile"
fi
fi
done < "$NewUserListFile"
if [[ "$ErrorCount" -eq 0 ]]; then
msg "\nUpdating user list file with latest users: $UserListFile"
if [[ "$NoOp" -eq 0 ]]; then
mv -f "$NewUserListFile" "$UserListFile" ||\
errmsg "Failed to update user list file. This command failed:\n\tmv -f \"$NewUserListFile\" \"$UserListFile\""
else
msg "NO_OP: Would do: mv -f \"$NewUserListFile\" \"$UserListFile\""
fi
fi
[[ "$Debug" -eq 0 && -r "$NewUserListFile" ]] && rm -f "$NewUserListFile"
if [[ "$ErrorCount" -eq 0 && "$WarningCount" -eq 0 ]]; then
msg_green "${H2}\nAll processing completed successfully.\n"
elif [[ "$ErrorCount" -eq 0 ]]; then
warnmsg "${H2}\nProcessing completed with no errors, but there were $WarningCount warnings. Review the above output carefully.\n"
else
errmsg "${H2}\nProcessing completed, but there were $ErrorCount errors and $WarningCount warnings. Scan above output carefully.\n"
fi
# Illustrate using $SECONDS to display runtime of a script.
msg "Time: That took $((SECONDS/3600)) hours $((SECONDS%3600/60)) minutes $((SECONDS%60)) seconds.\n"
# The terminate() function (defined in logging.lib) handles the actual exit,
# printing the final log path and removing the EXIT trap before exiting.
exit "$ErrorCount"