На момент написания этой страницы существует множество систем управления пользователями и т.д. Хостеры предоставляют доступ к каталогам сайтов для написания кода для одного пользователя. Причем, запуск 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
И файл констант.