szaydel
8/11/2017 - 10:24 PM

[Restricted Shells] Snippets are small examples of REPLs useful for various restricted applications #bash #shell #restricted shell #limited

[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