[Restricted Shells] Snippets are small examples of REPLs useful for various restricted applications #bash #shell #restricted shell #limited shell
#!/bin/bash
activity_log=/tmp/rshell.log
declare -A command_handlers
declare -A allowed_zfs_subcmds
ROOT=p01/tenants
# For each command that we pass through the screen we create a command
# handler, which is a function to which we dispatch command arguments,
# or if no arguments are expected, we call the dispatch function without
# any arguments.
command_handlers=(
["free"]=dispatch_free
["usage"]=dispatch_usage
["ls"]=dispatch_ls
["list"]=dispatch_ls
["ps"]=dispatch_ps
["whoami"]=dispatch_whoami
["uptime"]=dispatch_uptime
["zfs"]=dispatch_zfs
["zpool"]=dispatch_zpool
)
# zfs command is a complex entity with a large number of subcommands.
# We have to support some subset of said subcommands, but not all.
allowed_zfs_subcmds=(
["create"]=1 ["destroy"]=1 ["diff"]=1 ["get"]=1 ["hold"]=1
["list"]=1 ["mount"]=1 ["recv"]=1 ["receive"]=1 ["release"]=1
["rename"]=1 ["rollback"]=1 ["send"]=1 ["snapshot"]=1
)
# These are all the possible commands that we have implemented.
commands=("free" "ls" "list" "ps" "pwd" "usage" "whoami" "zfs")
function timestamp { date +'%Y-%m-%d %H:%M:%S'; }
function log { echo -e "$(timestamp)\t$1\t$(whoami)\t$2" >> ${activity_log}; }
#
# get_customer_ident: Extract only first string from GECOS field.
# This string is assumed to be Customer ID.
#
function get_customer_ident {
# set -x
if [ ! -z ${CUSTOMER} ] ; then echo ${CUSTOMER} ; return ; fi
v=`getent passwd replication \
| /usr/bin/awk '{ FS=":" ; print $5}'`
if [ -z ${v} ] ; then
echo "unknownCustomer"
else
# Cache customer ID for next time.
echo ${v}
fi
}
#
# dispatch_ls: List ZFS filesystems or snapshots starting from
# ZFS subtree belonging to customer identified by $customer_id.
#
function dispatch_ls {
customer_id=`get_customer_ident`
if [[ -z ${customer_id} ]]; then
echo "Error: Missing Customer ID"
return 1
fi
if [[ $1 =~ "snap" ]] ; then
# Recursively list all snapshots under customer's subtree.
/usr/sbin/zfs list -t snapshot -r ${ROOT}/${customer_id}
else
# Recursively list all filesystems under customer's subtree.
/usr/sbin/zfs list -r ${ROOT}/${customer_id}
fi
}
#
# dispatch_free: Obtain available capacity allocated to given
# customer identified by $customer_id.
#
function dispatch_free {
customer_id=`get_customer_ident`
if [[ -z ${customer_id} ]]; then
echo "Error: Missing Customer ID"
return 1
fi
/usr/sbin/zfs list -Ho space \
p01/tenants/${customer_id} | \
/usr/bin/awk '{printf("Available Capacity: %s\n", $2)}'
}
#
# dispatch_ps: List running processes
#
function dispatch_ps {
/usr/bin/ps aux
}
#
# dispatch_usage: Print information about how much capacity
# has been used by datasets belonging to current zone.
#
function dispatch_usage {
# We build a suffix here, first checking to make sure customer ID
# is not empty. If this is an empty string, we do not continue,
# because we cannot confirm that this customer is actually allowed
# access to given path.
customer_id=`get_customer_ident`
if [[ -z ${customer_id} ]]; then
echo "Error: Missing Customer ID"
return 1
fi
/usr/sbin/zfs list -o space -r ${ROOT}/${customer_id}
}
#
# dispatch_zfs: Perform various ZFS-specific operations on datasets
# below the subtree owned by customer identified by $customer_id.
#
function dispatch_zfs {
args=("$@")
# end: is length of line, i.e. number of strings in array.
# last_arg: is index of last argument in the array (zfs path).
end=${#@}
last_arg=$((end - 1))
head=${args[0]} # Head of arguments array
tail=${args[@]:1:$end} # All but the head of arguments array
# If last argument has index 0, then we know there is no path supplied
# as the last positional parameter of the line. We are being asked
# to do something globally. Most subcommands require some path, but
# some, like `list` allow us to obtain a full list without any special
# privs. We do not want to allow this in such a restricted setting.
if [ ${last_arg} -gt 0 ]; then
zfs_path=${args[$last_arg]} # Path to ZFS filesystem
fi
# If there is no path, we prohibit running global commands like
# zfs list, because that exposes view into the bowels of the beast.
if [[ -z ${zfs_path} ]]; then
echo "Error: Command must contain valid filesystem path"
return 1
fi
# First, subcommand validation. If this subcommand is prohibited
# we return 1 and do not attempt to continue any further.
zfs_subcmd_is_allowed ${head} || return 1
# Next, we validate the path handed to us. If the path does not
# include part of the ZFS tree which was allowed to given user,
# we cannot continue.
# We build a prefix here, first checking to make sure customer ID
# is not empty. If this is an empty string, we do not continue,
# because we cannot confirm that this customer is actually allowed
# access to given path.
customer_id=`get_customer_ident`
if [[ -z ${customer_id} ]]; then
echo "Error: Missing Customer ID"
return 1
fi
regex=${ROOT}/${customer_id} # We must match this string
# If the value of safe_path is an empty string, it did not pass
# validation, which means either we have a bug, or path passed in
# by customer does not match their customer ID.
safe_path=`/usr/bin/awk -v regex=${regex} \
'{
if (match($1, regex)) { print "Allowed", $0 }
}' <<< ${zfs_path}`
zfs_path="" # clear value to make sure we don't reuse it again
if [[ -z ${safe_path} ]]; then
echo "Error: Failed safety check. Did you forget to give me a dataset path?"
return 1
fi
/usr/sbin/zfs ${head} ${tail[@]}
}
#
# dispatch_whoami: Basic information about the authenticated user
#
function dispatch_whoami {
/usr/bin/whoami
}
#
# zfs_subcmd_is_allowed: Check that subcommand argument is one
# of the allowed subcommands. If not, return 1, 0 otherwise.
#
function zfs_subcmd_is_allowed {
cmd=$1
if [ ${allowed_zfs_subcmds[$cmd]:-0} -eq 1 ]; then
# echo "DEBUG: Command Allowed"
return 0
else
# echo "DEBUG: Command Denied"
return 1
fi
}
#
# trycmd: Process command and arguments and dispatch if allowed.
#
function trycmd {
# We can exit the shell with the following commands
if [[ "$ln" == "exit" || "$ln" == "ex" || "$ln" == "q" ]]
then
exit
# You can do exact string matching for some alias:
elif [[ "$ln" == "help" ]]
then
echo "Type exit or q to quit."
echo "Commands you can use:"
echo " help"
echo " echo"
for cmd in ${commands[@]}; do
printf " %s\n" $cmd
done
elif [[ "$ln" =~ ^echo\ .*$ ]]
then
ln="${ln:5}"
echo "$ln" # Double-quotes necessary to prevent code injection
log COMMAND "echo $ln"
else
ok=false
# Iterate all allowed commands and check against command
# passed in from the line we just read in. If there is no match
# we simply ignore what was passed in.
for cmd in "${commands[@]}"
do
split_ln=(${ln})
if [[ "${split_ln[0]}" == ${cmd} ]]
then
# line_length: Number of elements in the line
line_length=${#split_ln[@]}
# head: First argument in the args array
head=${split_ln[0]}
# tail: Array of all but the first argument
tail=${split_ln[@]:1:$line_length}
ok=true ; break
else
log DENIED "${split_ln[0]}"
fi
done
if $ok
then
# We use eval as a way of stitching together name of command
# and proper dispatch function for that command, such that if
# command is `zfs`, we call `dispatch_zfs`.
eval ${command_handlers[$head]} "${tail[@]}"
else
if [ ! -z "$ln" ]; then
echo "I'm sorry, `get_customer_ident`. I'm afraid I can't do that."
fi
log DENIED "$cmd"
fi
fi
}
# Optionally show a friendly welcome-message with instructions since it is a custom shell
echo "$(timestamp) Welcome, $(whoami). Type 'help' for information."
# Sets a global CUSTOMER variable once, and all further lookups
# should end-up getting this value from the environment.
readonly CUSTOMER=`get_customer_ident`
# Optionally log the login
log LOGIN "$@"
# Optionally log the logout
trap "trap=\"\";log LOGOUT;exit" EXIT
# Optionally check for '-c custom_command' arguments passed directly to shell
# Then you can also use ssh user@host custom_command, which will execute /bin/cush
if [[ "$1" == "-c" ]]
then
shift
ln="$@"
trycmd "$ln"
else
while echo -n "`get_customer_ident`:> " && read ln
do
trycmd "$ln"
done
fi