#!/bin/bash

#==============================================================================
# Copyright and license info is available in the LICENSE file included with
# the Server Deployment Package (SDP), and also available online:
# https://swarm.workshop.perforce.com/projects/perforce-software-sdp/view/main/LICENSE
#------------------------------------------------------------------------------

#==============================================================================
# Declarations and Environment
set -u

# Version ID Block. Relies on +k filetype modifier.
#------------------------------------------------------------------------------
# shellcheck disable=SC2016
declare VersionID='$Id: //p4-sdp/dev_c2s/Server/Unix/setup/gen_sudoers.sh#3 $ $Change: 31580 $'
declare VersionStream=${VersionID#*//}; VersionStream=${VersionStream#*/}; VersionStream=${VersionStream%%/*};
declare VersionCL=${VersionID##*: }; VersionCL=${VersionCL%% *}
declare Version=${VersionStream}.${VersionCL}
[[ "$VersionStream" == r* ]] || Version="${Version^^}"

declare ThisScript=${0##*/}
declare ThisUser=
declare LiveSudoersFile=
declare TempSudoersFile=
declare BackupSudoersFile=
declare SDPInstance=
declare P4Service=
declare SystemCtlCmd=
declare TmpFile=
declare -i LimitedSudoers=2
declare -a SystemCtlCmdList
declare Service=
declare TimerService=
declare -i ErrorCount=0
declare -i i=0
declare -i NoOp=1
declare -i Force=0
declare Log=
declare H1="=============================================================================="
declare H2="------------------------------------------------------------------------------"
declare RootHome=

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

function msg () { echo -e "$*"; }
function errmsg () { msg "\\nError: ${1:-Unknown Error}\\n"; ErrorCount+=1; }
function bail () { errmsg "${1:-Unknown Error}"; exit "$ErrorCount"; }

#------------------------------------------------------------------------------
# Function: terminate
function terminate
{
   # Disable signal trapping.
   trap - EXIT SIGINT SIGTERM

   [[ "$Log" != off ]] && \
      msg "Log is: $Log\\n${H1}\\n"

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

#------------------------------------------------------------------------------
# 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
{
   declare style=${1:--h}
   declare errorMessage=${2:-Unset}

   if [[ "$errorMessage" != Unset ]]; then
      msg "\\n\\nUsage Error:\\n\\n$errorMessage\\n\\n"
   fi

   msg "USAGE for $ThisScript version $Version:

$ThisScript {-full|-limited} [-y [-f]] [-L <log>] [-D]

or

$ThisScript [-h|-man]
"
   if [[ $style == -man ]]; then
      msg "
DESCRIPTION:
	This script generates a sudoers file for the OS user that
	owns /p4/common, which is expected to be the same user that the
	Perforce Helix Core service runs as (typically 'perforce').

	By default, the sudoers file is generated for review.  If the '-y'
	option is specified, the newly generated files is installed as
	the live sudoers file by copying to /etc/sudoers.d/<OSUSER> and
	adjusting permissions to 0400.

	If '-full' (full sudo) is specified, a one-line sudoers file is
	generated that looks something like this:

	perforce ALL=(ALL) NOPASSWD: ALL

	If '-limited' is specified, a limited sudoers file is generated
	granting only necessary access to the perforce user.

	If the sudoers file already exits, it will not be updated unless
	'-f' (force) is provided.

	The limited sudoers is recommended for production deployments.

OPTIONS:
 -full
 	Specify '-full' to indicate that a sudoers file is to be generated
	granting full root access to the server machine.

	The '-full' or '-limited' option must be specified.

	This option is discouraged as it is not as secure as the
	'-limited' option.

 -limited
	Specify '-limited' to indicate that a sudoers file is to be
	generated granting limited access to the server machine.

	The '-full' or '-limited' option must be specified.

	This option is recommended for optimal security.

 -y	This is confirmation to install the generated sudoers as the live
	sudoers file.

 -f	Specify '-f' to overwrite an existing limited sudoers file,
	/etc/sudoers.d/<OSUSER>

 -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:

	$RootHome/${ThisScript%.sh}.<Datestamp>.log

	NOTE: This script is self-logging.  That is, output displayed on the screen
	is simultaneously captured in the log file.

 -D     Enable bash 'set -x' extreme debugging verbosity.

HELP OPTIONS:
 -h	Display short help message
 -man	Display man-style help message
	
EXAMPLES:
	EXAMPLE 1: Generate a limited sudoers file for review.

	cd /p4/sdp/Server/Unix/setup
	./gen_sudoers.sh -limited

	EXAMPLE 2: Generate a limited sudoers file and install it.

	cd /p4/sdp/Server/Unix/setup
	./gen_sudoers.sh -limited -y

	EXAMPLE 3: Generate a limited sudoers file and install it, replacing an
	existing one.

	cd /p4/sdp/Server/Unix/setup
	./gen_sudoers.sh -limited -f -y

	EXAMPLE 4: Generate a full sudoers file and install it, replacing an

	cd /p4/sdp/Server/Unix/setup
	./gen_sudoers.sh -full -f -y
"
   fi

   exit 2
}

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

#------------------------------------------------------------------------------
# Get the home for Root. This is reliably /root on Linux, but varies in UNIX
# distros. If /root does not exist, attempt detection with getent.  As
# a fallback, use /tmp.  This is only used for a location of the log file.
# It must be set before processing command line arguments because this value
# is used in the usage() function.
if [[ -d /root ]]; then
   RootHome=/root
else
   RootHome=$(getent passwd root | cut -d: -f6)
fi

RootHome=${RootHome:-/tmp}

#------------------------------------------------------------------------------
declare -i shiftArgs=0

set +u
while [[ $# -gt 0 ]]; do
   case $1 in
      (-h) RootHome=/root; usage -h;;
      (-man) RootHome=/root; usage -man;;
      (-L) Log="$2"; shiftArgs=1;;
      (-f) Force=1;;
      (-full) LimitedSudoers=0;;
      (-limited) LimitedSudoers=1;;
      (-y) NoOp=0;;
      (-D) set -x;; # Debug; use 'set -x' mode.
      (-*) usage -h "Unknown flag ($1).";;
      (*) usage -h "Unknown parameter ($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

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

[[ -n "${Log:-}" ]] || \
   Log="$RootHome/${ThisScript%.sh}.$(date +'%Y%m%d-%H%M%S').log"

[[ "$LimitedSudoers" -eq 2 ]] &&
   usage -h "Specify either '-limited' (limited sudoers) for '-full' (full sudoers)."

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

trap terminate EXIT SIGINT SIGTERM

if [[ "$Log" != off ]]; then
   touch "$Log" || bail "Couldn't touch log file [$Log]."

   # Redirect stdout and stderr to a log file.
      exec > >(tee "$Log")
      exec 2>&1

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

ThisUser=$(id -n -u)
msg "Starting $ThisScript version $Version as $ThisUser@${HOSTNAME%%.*} on $(date)."

[[ "$ThisUser" == root ]] || bail "Run this as root, not $ThisUser."

# Determine list of SDP instances based in /p4/*/root dirs.
i=0
# shellcheck disable=SC2012
for SDPInstance in $(ls -d /p4/*/logs/ 2>/dev/null|cut -d '/' -f 3); do
   P4ServiceList[i]="p4d_${SDPInstance}"
   i+=1
   P4ServiceList[i]="p4broker_${SDPInstance}"
   i+=1
   P4ServiceList[i]="p4p_${SDPInstance}"
   i+=1
   P4ServiceList[i]="p4dtg_${SDPInstance}"
   i+=1
done

SystemCtlCmdList[0]="start"
SystemCtlCmdList[1]="stop"
SystemCtlCmdList[2]="restart"
SystemCtlCmdList[3]="status"
SystemCtlCmdList[4]="cat"
SystemCtlCmdList[5]="enable"
SystemCtlCmdList[6]="disable"
SystemCtlCmdList[7]="is-enabled"

SystemCtlPath="$(command -v systemctl)"
LslocksPath="$(command -v lslocks)"

OSUSER="$(stat -c %U /p4/common 2>/dev/null)"
[[ -n "$OSUSER" ]] || bail "Could not owner of /p4/common. Aborting."

TempSudoersFile=$(mktemp)
LiveSudoersFile="/etc/sudoers.d/$OSUSER"
BackupSudoersFile="$RootHome/etc_sudoers.d_$OSUSER.bak.$(date +'%Y-%m-%d-%H%M%S')"

if [[ "$LimitedSudoers" -eq 1 ]]; then
   { 
      echo "Cmnd_Alias P4_SVC = \\"

      for Service in helix-auth node_exporter p4prometheus; do
         for SystemCtlCmd in "${SystemCtlCmdList[@]}"; do
            echo "   $SystemCtlPath $SystemCtlCmd $Service, \\"
            echo "   $SystemCtlPath $SystemCtlCmd ${Service}.service, \\"
         done
      done

      for TimerService in opt_perforce_sdp_backup; do
         for SystemCtlCmd in "${SystemCtlCmdList[@]}"; do
            echo "   $SystemCtlPath $SystemCtlCmd $TimerService, \\"
            echo "   $SystemCtlPath $SystemCtlCmd ${TimerService}.service, \\"
            echo "   $SystemCtlPath $SystemCtlCmd ${TimerService}.timer, \\"
         done
      done

      for P4Service in "${P4ServiceList[@]}"; do
         for SystemCtlCmd in "${SystemCtlCmdList[@]}"; do
            echo "   $SystemCtlPath $SystemCtlCmd $P4Service, \\"
            echo "   $SystemCtlPath $SystemCtlCmd ${P4Service}.service, \\"
         done
      done

      echo "   /opt/perforce/helix-sdp/sdp/Server/Unix/p4/common/sdp_upgrade/sdp_upgrade.sh, \\"
      echo -e "   $LslocksPath\\n"

      echo "$OSUSER $HOSTNAME = (root) NOPASSWD: P4_SVC"

   } > "$TempSudoersFile"
else
   echo "perforce ALL=(ALL) NOPASSWD: ALL" > "$TempSudoersFile"
fi

msg "Generated limited sudoers temp file:\\n${H2}\\n$(cat "$TempSudoersFile")\\n${H2}\\n"

if [[ "$NoOp" -eq 0 ]]; then
   if [[ -r "$LiveSudoersFile" ]]; then
      msg "This file already exists: $LiveSudoersFile:\\n${H2}\\n$(cat "$LiveSudoersFile")\\n${H2}\\n"

      if diff -q "$TempSudoersFile" "$LiveSudoersFile" > /dev/null 2>&1; then
         msg "\\nVerified: The new generated sudoers file matches the currently installed one.\\nNo further processing required.\\n"
         exit 0
      fi

      if [[ "$Force" -eq 1 ]]; then
         msg "\\nOverwriting existing live sudoers file due to -f."
         msg "Creating backup file [$BackupSudoersFile]."
         cp -p "$LiveSudoersFile" "$BackupSudoersFile" ||\
            bail "Aborting: Failed to do: cp -p \"$LiveSudoersFile\" \"$BackupSudoersFile\""
      else
         bail  "Aborting because sudoers file already exists (displayed above).\\n\\nUse '-f' to replace this file with the newly generated one."
      fi
   fi

   msg "Installing $LiveSudoersFile"

   mv -f "$TempSudoersFile" "$LiveSudoersFile" ||\
      bail "Failed to move generated temp file in place as: $LiveSudoersFile"
   chmod 0400 "$LiveSudoersFile" ||\
      bail "Failed to do: chmod 0400 \"$LiveSudoersFile\""

   TmpFile=$(mktemp)
   # shellcheck disable=SC2024
   if sudo -l -U "$OSUSER" > "$TmpFile" 2>&1; then
      if grep -q 'syntax error' "$TmpFile"; then
         errmsg "The newly generated file failed a syntax check. Rolling back to prior version."
         mv -f "$BackupSudoersFile" "$LiveSudoersFile" ||\
            bail "Failed to do: mv -f \"$BackupSudoersFile\" \"$LiveSudoersFile\""
         bail "Aborted after successful rollback to earlier version of [$LiveSudoersFile]."
      else
         msg "Newly generated sudoers file passed a syntax check."
      fi
   else
      bail "Could not test generated sudoers file. If needed, the backup file is: $BackupSudoersFile"
   fi

   msg "\\nNew sudoers file successfully installed.\\n"
else
   msg "\\nBecause -y was not specified, not installing generated file as: $LiveSudoersFile"
fi

rm -f "$TempSudoersFile"

exit "$ErrorCount"
