Управление разработчиками сайтов

На момент написания этой страницы существует множество систем управления пользователями и т.д. Хостеры предоставляют доступ к каталогам сайтов для написания кода для одного пользователя. Причем, запуск www-демона происходит под этим пользователем, что не есть хорошо.
А если нужна возможность нескольких пользователей под своими логинами писать свои части кода, тогда как?
Суть сложности в правах доступа к каталогам и файлам. Для нормальной работы www-демона у него должны быть свои права, а не права конкретного пользователя! Для обеспечения прав пользователя/пользователей часто просто включают пользователя в группу www-демона. Но это не решает полностью проблему, владельцем каталога или файла может быть только один пользователь (или www-демон)!
Есть хороший вариант - использование bindfs. На нем и построен данный скрипт. Конечно, есть нюансы.

ManagingDevelops.bash

ManagingDevelops.bash

#!/bin/bash

# set -e
# set -x

# Скрипт обрабатывает параметры:
# debug_menu_main - показ массива главного меню
# show_menu_edit - показ меню редактирования файла констант

clear

name_scr=${0##*/}
name_scr=${name_scr%.*}
readonly name_scr
readonly title_scr="Управление разработчиками на \"${HOSTNAME}\""

##########
# Проверки
##########
if [ -z "$(command -v whiptail)" ]; then
    echo "Не установлен пакет \"whiptail\"!"
    exit 0
fi

if [ -z "$(command -v bindfs)" ]; then
    whiptail --title "${title_scr}" --msgbox "Не установлен пакет bindfs!" 7 50
    exit 0
fi

if [ $(whoami) != "root" ]; then
    whiptail --title "${title_scr}" --msgbox "Запуск только под рутом!\nМожно запускать через команду sudo." 8 50
    exit 0
fi

# pgrep не понимает имен более 15 символов!
if [ $(pgrep -c "${name_scr:0:15}") -gt 1 ]; then
    whiptail --title "${title_scr}" --msgbox "Скрипт уже запущен!" 7 50
    exit
fi

###########
# Константы
###########
# SSH
readonly sshd_config='/etc/ssh/sshd_config'
if ! [ -f "${sshd_config}" ]; then
    whiptail --scrolltext --title "${title_scr}" --msgbox "Не найден файл:\n${sshd_config}\"" 9 50
    exit
fi

# Файл ошибок
readonly file_errors="$(dirname $0)/${name_scr}Error.txt"

# Константы для статусов разработчиков
readonly ustatus_locked='locked'
#readonly ustatus_password='password'
readonly ustatus_password='unlocked'
readonly ustatus_nopassword='nopassword'
readonly ustatus_logined='logined'

# Для валидации имени разработчика
readonly reg_exp_name='^[a-zA-Z]+[-_.]?[a-zA-Z0-9]+$'

# Для whiptail
whiptail_textbox_height=30
whiptail_textbox_width=60

# Для оформления вывода
# отступ
indent='   '

# Создание файла констант
if ! [[ -f "$(dirname $0)/${name_scr}.constants" ]]; then
cat << EOF > "$(dirname $0)/${name_scr}.constants"
#!/bin/bash

###################################
# Константы для конкретного сервера
###################################
EOF
fi

if ! [[ -f "$(dirname $0)/${name_scr}.constants" ]]; then
    whiptail --scrolltext --title "${title_scr}" --msgbox "Не найден файл:\n$(dirname $0)/${name_scr}.constants" 9 50
    exit
fi

# Подключение файла констант
. "$(dirname $0)/${name_scr}.constants"
if [ $? -ne 0 ]; then
    whiptail --scrolltext --title "${title_scr}" --msgbox "Ошибка подключения файла:\n${name_scr}.constants\"!\nПроверьте файл и перезапустите скрипт." 10 50
    nano "$(dirname $0)/${name_scr}.constants"
    exit
fi

#######################
# Глобальные переменные
#######################
# Текущий разработчик
current_developer=''

#########
# Функции
#########
function press_enter {
    # Вывод сообщений в консоль и ожидание нажатия Enter
    # использовать echo здесь не получится
    # из-за возврата результата через echo в некоторых вызывающих функциях!

    set +x
    if [ -z "$@" ]; then
        read -p "\"Enter\" - продолжение, \"Ctrl+c\" - прервать"
    else
        read -p "\"$@\" \"Enter\" - продолжение, \"Ctrl+c\" - прервать"
    fi
}

function whiptail_msgbox {
    [[ -z "$@" ]] && return

    local strings="$@"

    local lines=$(echo -e "${strings}" | wc -l)
    local height=7
    local height_max=12
    [[ "${lines}" -gt "1" ]] && height=$((height + ${lines}))

    local width=60
    local width_max=80
    for string in ${strings}; do
        [[ "${#string}" -gt "${width}" ]] && width=${#string}
    done
    [[ "${width}" -gt "${width_max}" ]] && width=${width_max}

    if [ "${height}" -gt "${height_max}" ]; then
        whiptail --scrolltext --title "${title_scr}" --msgbox "${strings}" ${height_max} ${width}
    else
        whiptail --title "${title_scr}" --msgbox "${strings}" ${height} ${width}
    fi
}

function arr_keys_sort {
    # Возвращает отсортированную строку ключей массива
    # "$1" - имя массива

    [[ -z "$1" ]] && return

    declare -n arr_keys_declare="$1"
    if [[ ${#arr_keys_declare[@]} == 0 ]]; then
        press_enter "$FUNCNAME: Массив \"$1\" не существует или пуст!"
        return
    fi

    local sorted_arr_keys
    local OLD_IFS="$IFS"

    IFS=$'\n'
    sorted_arr_keys=($(sort <<< "${!arr_keys_declare[*]}"))
    IFS="$OLD_IFS"

    echo "${sorted_arr_keys[@]}"
}

function debug_menu_main {
    # Показ массива главного меню для отладки
    # Можно вставлять для отладки в разные места и передавать параметр $@
    # Например передавать $FUNCNAME для отображения имени вызывающей функции

    set +x
    echo -e "\n##### menu_main ($@):"

    for key in $(arr_keys_sort "main_menu"); do
        echo "${key}: ${main_menu[${key}]}"
    done

    press_enter "$FUNCNAME"
}

function debug_menu_current {
    # Показ массива текущего меню для отладки
    # Можно вставлять для отладки в разные места и передавать параметр $@
    # Например передавать $FUNCNAME для отображения имени вызывающей функции

    set +x
    echo -e "\n##### menu_current ($@):"

    for element in "${menu_current[@]}"; do
        echo "${element}"
    done

    press_enter "$FUNCNAME"
}

function error_log {
    # Запись ошибок в файл
    [[ -z "$@" ]] && return

    echo "$(date +%F) $(date +%T)" >> ${file_errors}
    echo "$@""" >> ${file_errors}

    whiptail_msgbox "$@"
    exit
}

function show_errors {
    # Показ файла ошибок
    if [ -f "${file_errors}" ]; then
        whiptail --scrolltext --textbox "${file_errors}" ${whiptail_textbox_height} ${whiptail_textbox_width}

        if (whiptail --defaultno --title  "${title_scr}" --yesno  "Удалить файл ошибок?" 10 50)  then
            rm "${file_errors}"
        fi
    else
        whiptail_msgbox "Файл ошибок не найден!"
    fi
}

function menu_name {
    # Возвращает имя пункта меню
    [[ -z "$@" ]] && return

    echo "$(echo "$@" | awk -F'=' '{print $1}')"
}

function info_script {
    local height=30
    local width=100

    local file_dir="$(dirname $0)"
    local file_name="${name_scr}Info.txt"
    local text=''

    if [ -f "${file_dir}/${file_name}" ]; then
        source ${file_dir}/${file_name}
        whiptail --scrolltext --title "${title_scr}" --msgbox "${text}" ${height} ${width}
    else
        whiptail_msgbox "Не найден файл: ${file_name}"
    fi
}

function edit_constants {
    # Редактирование файла констант
    [[ ! "$@" == *'restart'* ]] && local  md5_old=($(md5sum $(dirname $0)/${name_scr}.constants))
    nano "$(dirname $0)/${name_scr}.constants"
    [[ ! "$@" == *'restart'* ]] && local  md5_new=($(md5sum $(dirname $0)/${name_scr}.constants))

    if [[  "$@" == *'restart'*  ]] || [[ ! "${md5_old}" == "${md5_new}" ]]; then
        whiptail_msgbox "Перезапустите скрипт!"
        exit
    fi
}

function add_constants {
    # Добавление констант
    local msg=''
    local val=''
    local added_constants=''
    local array=()

    constants["name_owner"]="msg='Для смены владельца';val=alex"

    constants["dir_www"]="msg='Основной каталог';val='/var/www'"
    constants["dirs_parent_devs"]="msg='Массив имен подкаталогов разработки основного каталога';val='(*)'"
    constants["dirs_exceptions"]="msg='Массив исключений имен каталогов';val='('html' 'letsencrypt')'"

    constants["developer_first_id"]="msg='Минимальный id для разработчиков';val=1100"
    constants["developer_last_id"]="msg='Максимальный id для разработчиков';val=1500"
    constants["developer_name_min"]="msg='Минимальная длина имени для разработчиков';val=8"

    for constant in $(arr_keys_sort "constants"); do
        if [[ -z ${!constant} ]]; then
            msg=''
            val=''

            OLD_IFS="$IFS"
            IFS=';' array=(${constants["${constant}"]})
            IFS="$OLD_IFS"

            for element in "${array[@]}"; do
                [[ "${element}" == *'='* ]] && eval ${element}
            done

            [[ -n ${msg} ]] && echo -e "\n# ${msg}" >> "$(dirname $0)/${name_scr}.constants"
            if [[ -n ${val} ]]; then
                if [[ "${val}" == '(*)' ]]; then
                    echo "${constant}=('*')" >> "$(dirname $0)/${name_scr}.constants"
                else
                    echo "${constant}=${val}" >> "$(dirname $0)/${name_scr}.constants"
                fi
            fi

            if [ $? -ne 0 ]; then
                whiptail_msgbox "Ошибка добавления константы \"${constant}\"!"
                exit
            fi

            if [[ "${val}" == '(*)' ]]; then
                added_constants+="\n ${constant}=('*')"
            else
                added_constants+="\n ${constant}=${val}"
            fi
        fi
    done

    if [[ -n "${added_constants}" ]]; then
        whiptail_msgbox "Добавлены константы:${added_constants}\nпроверьте их!"
        edit_constants 'restart'
    fi
}

function check_constants {
    # Проверка констант
    if [ -z "$(id ${name_owner} 2>/dev/null)" ]; then
        whiptail_msgbox "Не найден пользователь:\n name_owner=${name_owner}.\nИсправьте!"
        edit_constants 'restart'
    fi

    if ! [ -d "${dir_www}" ]; then
        whiptail_msgbox "Не найден каталог:\n dir_www=${dir_www}.\nИсправьте!"
        edit_constants 'restart'
    fi

    if [ "${developer_last_id}" -gt "50000" ]; then
        whiptail_msgbox "Максимальное значение константы:\n developer_last_id=${developer_last_id}\nравно 50000.\nИсправьте!"
        edit_constants 'restart'
    fi

    if [ "${developer_first_id}" -ge "${developer_last_id}" ]; then
        whiptail_msgbox "Значение константы:\n developer_first_id=${developer_first_id}\nдолжно быть меньше ${developer_last_id}.\nИсправьте!"
        edit_constants 'restart'
    fi

    if [ "${developer_first_id}" -lt "1100" ]; then
        whiptail_msgbox "Значение константы:\n developer_first_id=${developer_first_id}\nдолжно быть не меньше 1100.\nИсправьте!"
        edit_constants 'restart'
    fi

    if [ "${developer_name_min}" -lt "8" ]; then
        whiptail_msgbox "Значение константы:\n developer_name_min=${developer_name_min}\nдолжно быть не меньше 8.\nИсправьте!"
        edit_constants 'restart'
    fi
}

function is_developer_logined {
    [[ -z "${1}" ]] && return

    for user in $(who | awk '{print $1}'); do
        if [ "${user}" == "${1}" ]; then
            echo ",${ustatus_logined}"
            return
        fi
    done
}

function get_developer_status {
    [[ -z "${1}" ]] && return

    case "$(passwd -S ${1} | awk '{print $2}')" in
        "L")
            echo ${ustatus_locked}
        ;;
        "P")
            echo ${ustatus_password}
        ;;
        "NP")
            echo ${ustatus_nopassword}
        ;;
    esac
}

function get_dev_dirs {
    # Заполнение массива имен каталогов разработки
    unset dev_dirs

    local string=''
    if [ ${#dirs_exceptions[@]} -ne 0 ]; then
        for dir in ${dirs_exceptions[@]}; do
            string=${string}' --hide='${dir}
        done
    fi

    if [ ${#dirs_parent_devs[@]} -eq 1 ] && [[ "${dirs_parent_devs}" == '*' ]]; then
        if cd "${dir_www}" 2>/dev/null; then
            for name in $(ls -1 ${string}); do
                if [ -d "${dir_www}/${name}" ]; then
                    dev_dirs+=(${name})
                fi
            done
        fi
    else
        for dir in ${dirs_parent_devs[@]}; do
            [[ "${dir}" == '*' ]] && continue
            if [ -n "${dir}" ] && [ -d "${dir_www}/${dir}" ]; then
                if cd "${dir_www}/${dir}" 2>/dev/null; then
                    for name in $(ls -1 ${string}); do
                        if [ -d "${dir_www}/${dir}/${name}" ]; then
                            dev_dirs+=(${dir}/${name})
                        fi
                    done
                fi
            fi
        done
    fi
}

function get_developer_names {
    # Заполнение ассоциативного массива имен разработчиков и их статусы
    local name=''

    # ассоциативный массив приходится обнулять так
    for developer in ${!developer_names[@]}; do
        unset developer_names[${developer}]
    done

    for id in $(getent passwd | awk -F: '{print $3}'); do
        if [ "${id}" -ge "${developer_first_id}" ] && [ "${id}" -le "${developer_last_id}" ]; then
            name=$(getent passwd ${id} | awk -F: '{print $1}')
            developer_names[${name}]="$(get_developer_status ${name})"
        fi
    done
}

function ssh_add {
    # $1 - имя разработчика
    [[ -z "${1}" ]] && return

    local string=''
    local num=$(grep -nw ^AllowUsers ${sshd_config} | awk -F: '{print $1}')

    if [ ${num} -gt 0 ]; then
        string=$(sed "${num}q;d" ${sshd_config})
        for word in ${string[@]}; do
            [[ "${word}" == "$1" ]] && return
        done

        cp ${sshd_config} /etc/ssh/sshd_config_old
        string+=" ${1}"
        sed -i "${num}c\\${string}" ${sshd_config}
    fi
}

function ssh_del {
    # $1 - имя разработчика
    [[ -z "${1}" ]] && return

    local string='AllowUsers'
    local num=$(grep -nw ^${string} ${sshd_config} | awk -F: '{print $1}')

    if [ ${num} -gt 0 ]; then
        for word in $(sed "${num}q;d" ${sshd_config}); do
            [[ "${word}" == "${string}" ]] && continue
            [[ "${word}" != "$1" ]] && string+=" ${word}"
        done

        cp ${sshd_config} /etc/ssh/sshd_config_old
        sed -i "${num}c\\${string}" ${sshd_config}
    fi
}

function change_owner {
    # Смена бывшего владельца
    # $1 - имя разработчика
    [[ -z "${1}" ]] && return

    local lname_owner=''

    if [ -z "$(id ${1} 2>/dev/null)" ]; then
        error_log "Ошибка имени разработчика: ${1} (\"${FUNCNAME}\")!"
        return
    fi

    lname_owner=${name_owner}
    if [ -z "$(id ${name_owner} 2>/dev/null)" ]; then
        lname_owner="$(id -un 1000)"
    fi

    if [ ${#dev_dirs[@]} -ne 0 ]; then
        echo "Обрабатываю каталоги, ждите..."
        for dir in ${dev_dirs[@]}; do
            if [ -n "${dir}" ] && [ -d "${dir_www}/${dir}" ]; then
                echo "каталог: \"${dir_www}/${dir}\""
                find "${dir_www}/${dir}/" -user ${1} -exec chown ${lname_owner} {} \;
                if [ $? -gt 0 ]; then
                    error_log "Ошибка смены владельца каталога \"${dir_www}/${dir}\" (\"${FUNCNAME}\")!"
                fi
            fi
        done
    fi

    # clear
}

function new_developer {
    local next_id=""
    local developer_name=''
    local developer_password=''
    local error=''

    for ((i=${developer_first_id}; i <= ${developer_last_id}; i++)); do
        if [ -z "$(id ${i} 2>/dev/null)" ]; then
            next_id=${i}
            break
        fi
    done

    if [ -z "${next_id}" ]; then
        whiptail_msgbox "Закончились id для разработчиков!"
        return
    fi

    while true; do
        developer_name=$(whiptail --title "${title_scr}" --inputbox "Имя нового разработчика (не менее ${developer_name_min} символов)" 10 50 "${developer_name}" 3>&1 1>&2 2>&3)

        developer_name="${developer_name// /''}"
        if [ $? -eq 0 ];  then
            if [ -n "${developer_name}" ]; then
                # проверяю имя на валидность
                if ! echo ${developer_name} | egrep -q ${reg_exp_name}; then
                    whiptail_msgbox "Недопустимое имя: ${developer_name}"
                    continue
                fi

                if [ ${#developer_name} -ge ${developer_name_min} ]; then
                    if [ -z "$(id ${developer_name} 2>/dev/null)" ]; then
                        developer_password=$(date +%s | sha256sum | base64 | head -c 15)
                        whiptail_msgbox "Пароль разработчика: ${developer_password}"
                        useradd -u ${next_id} -s /bin/bash -m ${developer_name}
                        if [ $? -eq 0 ]; then
                            lastlog -C -u ${developer_name}
                            echo ${developer_name}:${developer_password} | chpasswd
                            chfn -f "Developer ${developer_name}" ${developer_name}
                            [[ -d "/home/${developer_name}" ]] && chmod 0750 "/home/${developer_name}"

                            # для случая если уже был такой uid
                            change_owner "${developer_name}"
                            ssh_add ${developer_name}
                            systemctl restart ssh.service

                            if [ -n "$(command -v mc)" ] && [ -d "/home/${developer_name}" ]; then
                                if (whiptail --defaultno --title "${title_scr}" --yesno  "Создан разработчик с именем: ${developer_name}\nЗапустить mc под ним?" 10 60); then
                                    su -c "mc /home/${developer_name} /home/${developer_name}" ${developer_name}
                                fi
                            fi

                            whiptail_msgbox "Создан разработчик с именем: ${developer_name}"
                        else
                            error_log "Ошибка создания разработчика с именем: ${developer_name} (\"${FUNCNAME}\")!"
                            whiptail_msgbox "Ошибка создания разработчика с именем: ${developer_name}!"
                        fi
                    else
                        whiptail_msgbox "Разработчик с именем ${developer_name} уже есть!"
                    fi
                else
                    whiptail_msgbox "Имя менее ${developer_name_min} символов!"
                fi
            else
                break
            fi
        else
            developer_name=''
            break
        fi
    done
}

function select_developer {
    # $@ - добавка к тексту
    local items=''
    local item=''
    local msg=''
    local height=1
    local width=5

    [[ -n "$@" ]] && msg=' '$@

    for developer in $(arr_keys_sort "developer_names"); do
        item="${developer} ${developer_names[${developer}]}$(is_developer_logined ${developer})"
        items+="${item} OFF "
        (( height < 15 )) && let height++
        (( ${#item} > width )) && width=${#item}
    done

    current_developer=$(whiptail --title "${title_scr}" --radiolist "Выберите разработчика${msg}" $(( height + 8 )) $(( width + 20 )) ${height} ${items} 3>&1 1>&2 2>&3)
}

function is_mounted {
    # $1 - полное имя каталога
    [[ -z "${1}" ]] && return

    local result=''
    for mounted in $(mount | grep -w ${1} | awk '{print $3}'); do
        if [ "${1}" == "${mounted}" ]; then
            result='yes'
            break
        fi
    done

    echo ${result}
}

function mount_dir {
    # $1 - имя каталога
    [[ -z "${1}" ]] && return

    local developer_www="/home/${current_developer}/www"

    [ -d "${developer_www}/${1}" ] || mkdir -p ${developer_www}/${1}
    if [ -d "${developer_www}/${1}" ]; then
        if [ -z "$(search_in_fstab "${developer_www}/${1}")" ]; then
            # echo "/mnt/data/www/${1}   ${developer_www}/${1} fuse.bindfs   perms=0660:ug+X,mirror=${current_developer},force-group=${current_developer}   0 0" >> /etc/fstab
            echo "/mnt/data/www/${1}  ${developer_www}/${1}  fuse.bindfs  create-for-user=www-data,create-with-perms=0640:ug+X,perms=0640:ug+X,mirror=${current_developer},force-group=${current_developer}  0 0" >> /etc/fstab
            if [ $? -gt 0 ]; then
                error_log "Ошибка добавления строки в fstab (\"${FUNCNAME}\")!"
                return
            fi
        fi

        if [ -z "$(is_mounted "${developer_www}/${1}")" ]; then
            mount ${developer_www}/${1}
            if [ $? -gt 0 ]; then
                error_log "Ошибка монтирования ${developer_www}/${1} (\"${FUNCNAME}\")!"
            fi
        fi
    else
        error_log "Ошибка создания каталога ${developer_www}/${1} (\"${FUNCNAME}\")!"
    fi
}

function del_in_home_dir {
    # $1 - точка монтирования!
    [[ -z "${1}" ]] && return
    [[ -d "${1}" ]] || return

    local parent_dir=$(dirname ${1})

    # проверка на пустоту
    local total=$(ls -l ${1} | grep "total" | awk '{print $2}')
    if [ ${total} -eq 0 ]; then
        rmdir ${1}
        if [ $? -gt 0 ]; then
            error_log "Ошибка удаления каталога ${1} (\"${FUNCNAME}\")!"
        fi
    fi

    local total=$(ls -l ${parent_dir} | grep "total" | awk '{print $2}')
    if [ ${total} -eq 0 ]; then
        rmdir ${parent_dir}
        if [ $? -gt 0 ]; then
            error_log "Ошибка удаления каталога ${parent_dir} (\"${FUNCNAME}\")!"
        fi
    fi
}

function search_in_fstab {
    # $1 - точка монтирования!
    # возвращает номер строки в fstab-е
    [[ -z "${1}" ]] && return

    local result=''
    local nums=''
    for num in $(grep -nw "${1}" /etc/fstab | awk -F: '{print $1}'); do
        result=$(sed "${num}q;d" /etc/fstab | awk '{print $2}')
        if [ "${result}" == "$1" ]; then
            if [ -z "${nums}" ]; then
                nums=${num}
            else
                # дубликаты!?
                cp /etc/fstab /etc/fstab_old
                sed -i ${num}d /etc/fstab
            fi
        fi
    done
    echo "${nums}"
}

function del_in_fstab {
    # $1 - точка монтирования!
    [[ -z "${1}" ]] && return

    local num=$(search_in_fstab $1)
    if [ -n "${num}" ]; then
        cp /etc/fstab /etc/fstab_old
        sed -i ${num}d /etc/fstab
        if [ $? -eq 0 ]; then
            del_in_home_dir ${1}
        fi
    fi
}

function umount_dir {
    # $1 - точка монтирования!
    [[ -z "${1}" ]] && return
    [[ -z "$(is_mounted "$1")" ]] && return

    local error=''
    error="$(umount ${1})"
    if [ $? -eq 0 ]; then
        del_in_fstab ${1}
    else
        error_log "Ошибка \"$?\" отмонтирования ${1}: \"${error}\" (\"${FUNCNAME}\")!"
    fi
}

function purpose_developer_dirs {
    local height=1
    local width=5
    local items
    local remove_dir
    local msg
    local tag_dirs=''
    local developer_logined=''
    local developer_www=''

    if [ ${#dev_dirs[@]} -eq 0 ]; then
        whiptail_msgbox "Массив каталогов для разработки пуст!"
        return
    fi

    if [ ${#developer_names[@]} -eq 0 ]; then
        whiptail_msgbox "Массив разработчиков пуст!"
        return
    fi

    while true;do
        select_developer 'для назначения каталогов'
        if [ -z "${current_developer}" ]; then
            break
            #return
        fi

        developer_www="/home/${current_developer}/www"
        developer_logined=$(is_developer_logined ${current_developer})

        if [ -n "${developer_logined}" ]; then
            msg="Разработчик ${current_developer} залогинен!\nМожно только добавлять каталоги!\nПродолжить?"
            if ! (whiptail --defaultno --title  "${title_scr}" --yesno  "${msg}" 12 40)  then
                return
            fi
        fi

        if [ -z "${developer_logined}" ]; then
            msg="Назначьте"
        else
            msg="Добавьте"
        fi

        msg+=" каталоги разработчику ${current_developer} (${developer_names[${current_developer}]}${developer_logined})"

        items=''
        for dev_dir in ${dev_dirs[@]}; do
            if [ -z "$(is_mounted "${developer_www}/${dev_dir}")" ]; then
                items+="${dev_dir} OFF "
            else
                if [ -z "${developer_logined}" ]; then
                    items+="${dev_dir} ON "
                fi
            fi
            (( height < 10 )) && let height++
            (( ${#dev_dir} > width )) && width=${#dev_dir}
        done

        tag_dirs=$(whiptail --noitem --separate-output --title "${title_scr}" --checklist "${msg}" $(( height + 8 )) $(( width + 20 )) ${height} ${items} 3>&1 1>&2 2>&3)

        if [ $? -eq 0 ];  then
            for dev_dir in ${tag_dirs[@]}; do
                if [ -z "$(is_mounted "${developer_www}/${dev_dir}")" ]; then
                    mount_dir ${dev_dir}
                fi
            done

            for mounted in $(mount | grep -w ${developer_www}* | awk '{print $3}'); do
                remove_dir='yes'
                for dev_dir in ${tag_dirs[@]}; do
                    if [[ "${mounted}" == "${developer_www}/${dev_dir}" ]]; then
                        remove_dir=''
                        break
                    fi
                done

                if [ -n "${remove_dir}" ]; then
                    umount_dir ${mounted}
                fi
            done
        fi
    done
}

function lock_unlock_developer {
    local items=''
    local item=''
    local tag_developers=''
    local lock=''
    local height=1
    local width=5

    if [ ${#developer_names[@]} -eq 0 ]; then
        whiptail_msgbox "Массив разработчиков пуст!"
        return
    fi

    for developer in $(arr_keys_sort "developer_names"); do
        item="${developer} ${developer_names[$developer]}$(is_developer_logined ${developer})"
        case "${developer_names[$developer]}" in
            "${ustatus_locked}")
                items+="${item} ON "
            ;;
            "${ustatus_password}")
                items+="${item} OFF "
            ;;
            "${ustatus_nopassword}")
                items+="${item} OFF "
            ;;
        esac
        (( height < 15 )) && let height++
        (( ${#item} > width )) && width=${#item}
    done

    tag_developers=$(whiptail --separate-output --title "${title_scr}" --checklist "Выберите разработчиков для блокировки" $(( height + 8 )) $(( width + 20 )) ${height} ${items} 3>&1 1>&2 2>&3)

    if [ $? -eq 0 ];  then
        for developer in ${!developer_names[@]}; do
            lock='false'
            for select in ${tag_developers}; do
                if [ "${developer}" == ${select} ]; then
                    lock='true'
                fi
            done

            if [ "${lock}" == 'true' ]; then
                if ! [ "$(passwd -S ${developer} | awk '{print $2}')" == "L" ]; then
                    passwd -q -l ${developer} 2>/dev/null
                fi
                ssh_del ${developer}
            else
                if [ "$(passwd -S ${developer} | awk '{print $2}')" == "L" ]; then
                    passwd -q -u ${developer} 2>/dev/null
                fi
                ssh_add ${developer}
            fi
        done

        systemctl restart ssh.service
    fi
}

function del_developer {
    local msg=''

    if [ ${#developer_names[@]} -eq 0 ]; then
        whiptail_msgbox "Массив разработчиков пуст!"
        return
    fi

    select_developer 'для удаления'
    [[ -z "${current_developer}" ]] && return

    local developer_logined=$(is_developer_logined ${current_developer})
    local id=$(id -u ${current_developer})

    if [ -n "${developer_logined}" ]; then
        msg="Разработчик ${current_developer} залогинен!\nМожно только блокировать разработчика для очередного вхождения!\nПродолжить?"
        if (whiptail --defaultno --title  "${title_scr}" --yesno  "${msg}" 10 70)  then
            passwd -q -l ${current_developer} 2>/dev/null
            ssh_del ${current_developer}
            systemctl restart ssh.service
        fi
        return
    fi

    if ! (whiptail --defaultno --title  "${title_scr}" --yesno  "Точно удалить разработчика: ${current_developer}?" 10 60)  then
        return
    fi

    passwd -q -l ${current_developer} 2>/dev/null
    ssh_del ${current_developer}
    systemctl restart ssh.service

    for mounted in $(mount | grep -w /home/${current_developer} | awk '{print $3}'); do
        umount_dir ${mounted}
    done

    # Файлы и каталоги "сироты"
    change_owner "${current_developer}"

    # Полное удаление разработчика
    deluser -q --remove-home ${current_developer} 2>/dev/null
    if [ $? -gt 0 ]; then
        error_log "Ошибка удаления разработчика ${current_developer} (\"${FUNCNAME}\")!"
    fi
}

function info_dirs {
    local height=1
    local height_max=30
    local width=5
    local string=''

    if [ ${#dev_dirs[@]} -eq 0 ]; then
        whiptail_msgbox "Массив каталогов разработок пуст!"
        return
    fi

    local text_dev_dirs="Каталоги разработок:\n"
    local header=$(menu_name ${main_menu["1.2"]})

    for dev_dir in "${dev_dirs[@]}"; do
        string="${indent}${dev_dir}"
        text_dev_dirs+="${string}\n"
        (( height < height_max )) && let height++
        (( ${#string} > width )) && width=${#string}
        for developer in $(arr_keys_sort "developer_names"); do
            if [ -n "$(is_mounted "/home/${developer}/www/${dev_dir}")" ]; then
                string="${indent}${indent}${developer}${indent}${developer_names[${developer}]}$(is_developer_logined ${developer})"
                text_dev_dirs+="${string}\n"
                (( height < height_max )) && let height++
                (( ${#string} > width )) && width=${#string}
            fi
            (( height < height_max )) && let height++
        done
    done

    (( width < 40 )) && width=40

    whiptail --scrolltext --title "${title_scr}" --msgbox "${header}\n\n${text_dev_dirs}" ${height} $(( width + 10 ))
}

function info_developer {
    local height=30
    local width=100

    local text_status="Статусы:\n"
    local text_lastlog="Последние подключения:\n   Username         Port     From             Latest\n"
    local text_dev_dirs="Каталоги разработок:\n"
    local header=$(menu_name ${main_menu["2.4"]})

    for developer in $(arr_keys_sort "developer_names"); do
        text_status+="${indent}${developer}${indent}${developer_names[${developer}]}$(is_developer_logined ${developer})\n"
        text_lastlog+="${indent}$(lastlog -u ${developer} | tail -n1 -)\n"
        text_dev_dirs+="${indent}${developer}\n"
        for dev_dir in ${dev_dirs[@]}; do
            if [ -n "$(is_mounted "/home/${developer}/www/${dev_dir}")" ]; then
                text_dev_dirs+="${indent}${indent}${dev_dir}\n"
            fi
        done
    done

    whiptail --scrolltext --title "${title_scr}" --msgbox "${header}\n\n${text_status}\n${text_lastlog}\n${text_dev_dirs}" ${height} ${width}
}

function menu_function {
    # Возвращает имя функции для вызова, что назначена пункту меню

    [[ -z "$@" ]] && return
    echo "$(echo "$@" | awk -F'=' '{print $2}')"
}

##############
# Основной код
##############

# Ассоциативный массив констант, обработывается функцией add_constants
declare -A constants
add_constants
check_constants

# Глобальный массив имен каталогов разработки, заполняется функцией get_dev_dirs
declare -a dev_dirs

# Глобальный ассоциативный массив имен разработчиков, заполняется функцией get_developer_names
declare -A developer_names

# Главное меню
declare -A main_menu

main_menu["0"]='Главное меню=show_menu'

main_menu["1"]='Управление каталогами разработок=show_menu'
main_menu["1.1"]='Назначения каталогов разработчику=purpose_developer_dirs'
main_menu["1.2"]='Информация о каталогах=info_dirs'

main_menu["2"]='Управление разработчиками=show_menu'
main_menu["2.1"]='Управление блокировками разработчиков=lock_unlock_developer'
main_menu["2.2"]='Создать разработчика=new_developer'
main_menu["2.3"]='Удалить разработчика=del_developer'
main_menu["2.4"]='Информация о разработчиках=info_developer'

main_menu["3"]='Информация=show_menu'
main_menu["3.1"]=${main_menu["2.4"]}
main_menu["3.2"]=${main_menu["1.2"]}
main_menu["3.3"]='Информация о скрипте=info_script'

if [[ "$@" == *'show_menu_edit'* ]]; then
    main_menu["4"]='Редактирование файла констант=edit_constants'
fi

if [ -f "${file_errors}" ]; then
    main_menu["5"]='Просмотр ошибок=show_errors'
fi

# для отладки
# -----------
[[ "$@" == *'debug_menu_main'* ]] && debug_menu_main

# Текущий массив меню
declare -a menu_current

# Массив "хлебные крошки"
declare -a breadcrumbs

# для возвращения к выбранному пункту меню
default_item=''

while true;do
    # clear

    get_developer_names
    get_dev_dirs

    main_item=''
    unset menu_current
    height=1
    width=${#title_scr}
    width_max=60

    while [ ${#breadcrumbs[@]} -gt 0 ]; do
        if [ -z "${main_menu[${breadcrumbs[-1]}]}" ]; then
            unset breadcrumbs[-1]
        else
            break
        fi
    done

    if [ ${#breadcrumbs[@]} = 0 ]; then
        prefix=''
        header="Главное меню"
    else
        prefix="${breadcrumbs[-1]}."
        header="$(menu_name ${main_menu[${breadcrumbs[-1]}]})"
    fi

    (( ${#header} > width )) && width=${#header}

    for ((i=1; i <= ${#main_menu[@]}; i++)); do
        if [ -n "${main_menu["${prefix}${i}"]}" ]; then
            main_item="$(menu_name ${main_menu["${prefix}${i}"]})"
            menu_current=("${menu_current[@]}" "${i}" "${main_item}")
            (( height++ ))
            (( ${#main_item} > width )) && width=${#main_item}
        fi
    done

    if [ ${#menu_current[@]} -eq 0 ]; then
        if [ ${#main_menu[@]} -eq 0 ]; then
            whiptail_msgbox "Главный массив меню пуст!"
            break
        else
            whiptail_msgbox "Массив меню:\n\"${header}\" пуст!"
        fi
    fi

    tag=$(whiptail --title "${title_scr}" --menu "${header}" --default-item "${default_item}" $(( height + 8 )) $(( width + 16 )) ${height} "${menu_current[@]}" 3>&1 1>&2 2>&3)

    if [ $? -eq 0 ];  then
        default_item=${tag}
        menu_function="$(menu_function ${main_menu["${prefix}${tag}"]})"
        if [ -z "${menu_function}" ]; then
            whiptail_msgbox "Нет функции для меню:\n$(menu_name ${main_menu["${prefix}${i}"]})"
            continue
        elif [ "${menu_function}" == 'show_menu' ]; then
            default_item=''
            breadcrumbs+=("${prefix}${tag}")
        else
            # запуск дочернего процесса
            # [[ "${menu_function}" != 'empty' ]] && (${menu_function})

            # без запуска дочернего процесса?
            [[ "${menu_function}" != 'empty' ]] && ${menu_function}
        fi
    else
        if [ ${#breadcrumbs[@]} == 0 ]; then
            break
        else
            default_item="${breadcrumbs[-1]: -1}"
            unset breadcrumbs[-1]
        fi
    fi
done

В принципе его одного достаточно для работы.
Но есть еще два вспомогательных файла.

ManagingDevelopsInfo.txt

ManagingDevelopsInfo.txt

text=$(cat <<EOF
Краткая информация о скрипте

Скрипт расчитан для управления только и только разработчиками (диаппазон ID-user: ${developer_first_id} - ${developer_last_id})!
Для других целей не применять!

Скрипт расчитан что разработчики работают по ssh!

1. Формируется массив имен каталогов разработки в подкаталогах `[[ -n "${dirs_parent_devs[@]//' '/''}" ]] && echo "\"${dirs_parent_devs[@]}\" "`основного каталога "${dir_www}".
В массив не включаются каталоги с именами "${dirs_exceptions[@]}".
`[[ -n "${dirs_parent_devs[@]//' '/''}" ]] && echo "Имена подкаталогов \"${dirs_parent_devs[@]}\", имя " || echo "Имя "`основного каталога "${dir_www}" и имена каталогов-исключений можно менять в "${name_scr}.constants" (см. ниже).

2. В каталоге "~/www/" разработчиков создаются каталоги куда монтируются нужные каталоги с нужными правами (bindfs, fstab).

ВНИМАНИЕ! Разработчики работают только в подкаталогах домашнего каталога "~/www/*".

3. Работа с каталогами разработок находится в "1 $(menu_name ${main_menu["1"]})" основного меню.

4. Назначение каталогов разработки разработчику находится в подменю "1 $(menu_name ${main_menu["1.1"]})".

5. В случае активного разработчика (разработчик залогинился) скрипт позволяет только добавлять каталоги (через "1 $(menu_name ${main_menu["1.1"]})")!
У не активных можно еще и удалять назначенные ему каталоги.
У активных разработчиков есть признак "${ustatus_logined}"!

6. Работа с разработчиками находится в "2 $(menu_name ${main_menu["2"]})" основного меню.

7. Скрипт позволяет блокировать разработчиков ("1 $(menu_name ${main_menu["2.1"]})").
Блокируется пароль и вход по ssh!
Блокировка по ssh проискодит через параметр "AllowUsers" при его наличии в "${sshd_config}".
Признаки для блокировки: "${ustatus_locked}" или "${ustatus_password}".

8. При создании нового разработчика (2 "$(menu_name ${main_menu["2.2"]})") используется "${reg_exp_name}" валидация имени разработчика.
После создания разработчика можно "1 $(menu_name ${main_menu["1.1"]})".

9. При попытке удалить активного разработчика (признак "${ustatus_logined}") через "3 $(menu_name ${main_menu["2.3"]})" скрипт только блокирует разработчика.
И в "1 $(menu_name ${main_menu["2.1"]})" появится признак "${ustatus_locked}".

Неактивный разработчик удаляется полностью (скрипт еще раз запросит подтверждение)!

10. Скрипт обрабатывает параметры:
    debug_menu_main - показ массива главного меню
    show_menu_edit - показ меню редактирования файла констант

11. Часть констант, которые зависят от конкретного сервера, вынесена в отдельный файл "${name_scr}.constants". Их можно аккуратно менять.
Если такого файла нет, то скрипт пытается создать его и записать туда константы.
После этого предлагается его скорректировать через редактор "nano".
При последующих запусках скрипта этот файл считывается и проверяется на корректность заданных констант.
Некорректные константы предлагается поправить и перезапустить скрипт.
Редактировать файл "${name_scr}.constants" можно при запуске скрипта с параметром "show_menu_edit" через меню скрипта.

EOF
)

Краткая информация о скрипте.

ManagingDevelops.constants

ManagingDevelops.constants

#!/bin/bash

###################################
# Константы для конкретного сервера
###################################

# Для смены владельца
name_owner="alex"

# Основной каталог
dir_www='/mnt/data/www'

# Массив имен подкаталогов разработки основного каталога
dirs_parent_devs=('c' 'dev')

# Массив исключений имен каталогов
dirs_exceptions=(
    'html'
    'letsencrypt'
    'test'
    'time'
)

# Максимальный id для разработчиков
developer_last_id=1500

# Минимальный id для разработчиков
developer_first_id=1100

# Минимальная длина имени для разработчиков
developer_name_min=8

И файл констант.