#! /bin/bash
#  Copyright 2016 by Cumulus Networks, Inc.

# This script needs to be run as root or via sudo

# Set up an existing account to be use a restricted shell
# (only rbash is supported), changing the home directory so the
# user can not remove the dot files, and have the dot files
# owned by bin.bin.

# The intent is to set up an account for use by users who
# are only allowed to use commands (and command arguments)
# permitted by a TACACS+ server.

# Set up a ~user/bin directory and PATH to point to that.
# Create a symlink from /usr/sbin/tacplus-auth to each of
# the the requested commands to the bin directory

# Those will be the only commands accessible to the user, and
# only if also permitted by the TACACS+ server

# Use the -i option to initialize the directory
# permissions and dot files.  It will only be run
# once, unless the -f option is given

# Use the -a to add command links that the user will be able
# to potentially run (subject to authorization by the TACACS+ server)
# The list of commands is given after all other arguments

# -i and -a can be combined

# The -u username argument is required

# See
#   man tacplus-auth
# for additional information.

prog=${0##*/}

# could add rzsh or rksh in future, but keeping it simple for now
shell=/bin/rbash origshell=/bin/bash
tacacs_restrict=.tacacs_restricted # flag file for inited

authprog=/usr/sbin/tacplus-auth # command that does authorization

# set defaults
user= firstarg=
addcmds=0 delcmds=0 doinit=0 dorestore=0 force=0

warn()
{
    echo $prog: Warning: "$@" 1>&2
}

usage()
{
   echo Usage: $prog '-u user [-f (force)] [-i (init)] [-R (restore)] [(-a|-d) cmd1 cmd2 ...]' 1>&2
   exit 1
}

fatal()
{
   warn "$@"
   exit 2
}

initialize()
{
    set -e # exit if any of the next group of commands fail

    chsh -s $shell $user || fatal Use tacacs0, tacacs15, etc. for -u arg
    chmod 755 $home || $warn Unable to chmod ${user} home directory $home

    dots=$(echo .[a-zA-Z]*)
    if [ -n "${dots}" -a "${dots}" != ".[a-zA-Z]*" ]; then
	    # create dir if needed.  If this is the 2nd init
	    # or later with dotfiles, the original backup will
	    # be saved in the new backup.
	    mkdir -p DOTback
	    mv -i .[a-zA-Z]* DOTback/
	    chown bin:bin DOTback
	    chmod 750 DOTback
	    mv DOTback .DOTback
	    echo Moved old dot files to DOTback: ; (cd .DOTback && ls -a)
    fi

    for f in .bash_login .profile .bash_profile .bash_logout
    do echo '. ~/.bashrc' > $f
    done

    echo "export PATH=$home/bin" > .bashrc

    mkdir -p bin
    touch ${tacacs_restrict}
    chown -R bin:bin .
    chmod -R o-w . # be paranoid
    set +e # initialization complete
}


# dorestore the initialization.  Return shell to /bin/bash, restore dotfiles, etc.
restore()
{
    local l cmd lev=${user#tacacs}

    warn Restoring $user '(tacacs priv='$lev')' to normal use,'
    able to run all commands.'
    warn You have 5 seconds to interrupt to prevent the restore
    sleep 5

    chsh -s $origshell $user || warn unable to restore shell $origshell for $user

    for f in .bash_login .profile .bash_profile .bash_logout
    do rm -f $f # remove the files we created in initialize()
    done

    dots=$(echo .DOTback/.[a-zA-Z]* | sed 's,.DOTback/.DOTback,,')
    if [ -z "${dots}" -o "${dots}" = ".DOTback/.[a-zA-Z]*" ]; then
        warn No saved files '(.bashrc, etc.)', using /etc/skel
        cp -a /etc/skel/.[a-zA-Z0-9]* .
    else
        mv ${dots} .
    fi

    [ -e ${tacacs_restrict} ] && rm ${tacacs_restrict}

    # Only remove .DOTback if empty, not rm -rf.  Sometimes multiple levels
    rmdir .DOTback 2>/dev/null
    [ -d .DOTback ] && chown -R ${user}:tacacs .DOTback # make user can see files

    for cmd in bin/*; do
        [ -L "${cmd}" ] && {
            l=$(readlink -s "${cmd}")
            case "$l" in
            $authprog) rm -f "${cmd}" ;;
            esac
        }
    done
    rmdir bin 2>/dev/null # remove if empty
}

linkcmds()
{
    [ -x $authprog ] || fatal TACACS authorization command $authprog not found
    for c in "${firstarg}" "$@"; do
       cmd=${c##*/} # make relative in case they gave full name
       case $cmd in
       sh|dash|bash|csh|tcsh|zsh|ksh|sh.distrib)
	  echo $prog: Skipping shell $cmd because it bypasses restrictions
          continue ;;
       esac
       ln -s -f $authprog bin/$cmd
    done
}

# inverse of linkcmds, but supports '*' to delete all
unlinkcmds()
{
    (
    [ ! -d bin ] && { warn All commands for $user have already been removed; exit 1; }
    cd bin || exit 1
    for c in "${firstarg}" "$@"; do
       [ -L "${c}" ] || warn "${c}" is not currently an enabled command # but remove anyway
       case "${c}" in
       '*') warn Removing all commands for $user: $(echo *) ; rm -f -- * ;;
       *)  rm -f -- "${c}" ;;
       esac
    done
    )
}

listcmds()
{
    local lev=${user#tacacs}
    if [ ! -d bin ]; then
        warn No commands available to $user, tacacs priv=${lev}.
        return
    fi
    (
    cd bin || exit 1
    p=${user/tacacs}
    case "$p" in
    [0-9]|1[0-5]) umsg="$user (TACACS+ priv=$p)" ;;
    *) umsg=$user ;;
    esac
    cmds=$(echo *)
    case "$cmds" in
    '*'|'') warn No commands available to user $umsg ;;
    *)      echo Commands available to $umsg are:
            /bin/ls -C
    esac
    )
}

# With both arrays and using set -- $(getopt ...), if any of the
# getopts has similar issues, and I want to detect things like
#   -a cmd1 cmd2 -d cmd3
# getopts builtin can't do that, but getopt does.
# There is an issue with quoted arguments not remaining quoted
# but that's OK for this use.
# Unfortunately, the quoting itself (getopt uses apostrophe quotes)
# can cause problems.  So use getopts, and step through all args.

while getopts a:d:fhiRu: opt
do
    case "$opt" in
    a) addcmds=1 firstarg="$OPTARG" ;;
    d) delcmds=1 firstarg="$OPTARG" ;;
    u) user=$OPTARG ;;
    i) doinit=1 ;;
    f) force=1 ;;
    R) dorestore=1;;
    h|?) usage ;;
    esac
done

shift $(( OPTIND - 1 ))

# catch things like -a b c -d e f g
for a in $@; do
    case "$a" in
    -d) [ $addcmds -eq 1 ] && usage ;;
    -a) [ $delcmds -eq 1 ] && usage ;;
    esac
done

# sanity check that we have valid argument sets, and no
# extra arguments
[ $addcmds -eq 1 -a $delcmds -eq 1 ] && usage
[ $addcmds -eq 0 -a $delcmds -eq 0 -a $# -ne 0 ] && usage
[ -z "$user" ] && usage
[ $doinit -eq 1 -a $addcmds -eq 0 -a $delcmds -eq 0 -a $# -ne 0 ] && usage
[ $addcmds -eq 0 -a $doinit -eq 0 -a $delcmds -eq 0 -a $dorestore -eq 0 ] && usage
[ $dorestore -eq 1 -a \( $addcmds -eq 1 -o $delcmds -eq 1 -o $doinit -eq 1 -o $# -ne 0 \) ] && usage

home=$(eval echo ~"${user}")
[ -z "$home" -o "$home" = ~${user} ] &&
	fatal Unable to get home directory for $user

cd $home || fatal home dir "${home}" for $user does not exist

[ -e ${tacacs_restrict} -a $doinit -eq 1 -a $force -eq 0 ] &&
   fatal initialization requested without '-f', and already initialized

[ \( $addcmds -eq 1 -o $delcmds -eq 1 \) -a ! -e ${tacacs_restrict} ] &&
	warn not yet initialized

[ -x $shell ] || fatal shell "${shell}" is not executable

umask 222 # no write perms on any dirs or files

[ $dorestore -eq 1 ] && { restore ; exit 1; }

[ $doinit -eq 1 ] && initialize

[ $addcmds -eq 1 ] && linkcmds "$@"
[ $delcmds -eq 1 ] && unlinkcmds "$@"

listcmds # Always show which commands are avilable now
