Для bash это небольшая хитрость (хотя и задокументированная): попытаться использовать typeset
, чтобы удалите атрибут «массив»:
$ typeset +a BASH_VERSINFO
bash: typeset: BASH_VERSINFO: cannot destroy array variables in this way
echo $?
1
(Вы не можете сделать это в zsh
, он позволяет преобразовать массив в скаляр, в bash
это явно запрещено.)
Итак:
typeset +A myvariable 2>/dev/null || echo is assoc-array
typeset +a myvariable 2>/dev/null || echo is array
Или в функции, отмечая предостережения в конце:
function typeof() {
local _myvar="$1"
if ! typeset -p $_myvar 2>/dev/null ; then
echo no-such
elif ! typeset -g +A $_myvar 2>/dev/null ; then
echo is-assoc-array
elif ! typeset -g +a $_myvar 2>/dev/null; then
echo is-array
else
echo scalar
fi
}
Обратите внимание на использование typeset -g
(bash-4.2 или новее), это требуется внутри функции, чтобы typeset
(син. declare
) не работает как local
и уничтожает значение, которое вы пытаетесь проверить. Это также не обрабатывает "переменные" типы функций, вы можете добавить еще один тест ветвления, используя typeset -f
, если необходимо.
Другой (почти полный) вариант - использовать это:
${!name[*]}
If name is an array variable, expands to the list
of array indices (keys) assigned in name. If name
is not an array, expands to 0 if name is set and
null otherwise. When @ is used and the expansion
appears within double quotes, each key expands to a
separate word.
Однако есть одна небольшая проблема: массив с единственным нижним индексом 0 соответствует двум из вышеперечисленных условий. Это то, на что также ссылается mikeserv, bash действительно не имеет резкого различия, и в некоторых из них (если вы проверите журнал изменений) можно винить ksh и совместимость с тем, как $ {name [*]}
или $ {name [@]}
работают с не-массивом.
Итак, частичное решение:
if [[ ${!BASH_VERSINFO[*]} == '' ]]; then
echo no-such
elif [[ ${!BASH_VERSINFO[*]} == '0' ]]; then
echo not-array
elif [[ ${!BASH_VERSINFO[*]} != '0' ]];
echo is-array
fi
Я использовал в прошлом вариант этого:
while read _line; do
if [[ $_line =~ ^"declare -a" ]]; then
...
fi
done < <( declare -p )
это тоже требует подоболочки.
Еще один, возможно, полезный метод - это compgen
:
compgen -A arrayvar
Это перечислит все индексированные массивы, однако ассоциативные массивы не обрабатываются специально (вплоть до bash-4.4) и отображаются как обычные переменные ( compgen -A variable
)
Ну, оказывается, я на самом деле идиот, модуль pam_exec.so
прекрасно подходит для создания условий PAM.
Тим Смит был прав, оценив, что оба теста в моем скрипте /etc/security/deny-ssh-user.sh
НИКОГДА не устанавливали для переменной SSH_SESSION
значение true. Я не принял это во внимание, потому что скрипт работает в обычной оболочке, но контекст среды удаляется при выполнении pam_exec.so
.
В итоге я переписал сценарий, чтобы использовать утилиту last
, как в его примере, однако мне пришлось изменить некоторые из них, потому что переключатели для last
отличаются от Arch Linux до RedHat.
#!/bin/bash
# Returns 1 if the user is logged in through SSH
# Returns 0 if the user is not logged in through SSH
SSH_SESSION=false
function isSshSession {
local terminal="${1}"
if $(/usr/bin/last -i |
/usr/bin/grep "${terminal}" |
/usr/bin/grep 'still logged in' |
/usr/bin/awk '{print $3}' |
/usr/bin/grep -q --invert-match '0\.0\.0\.0'); then
echo true
else
echo false
fi
}
function stripTerminal {
local terminal="${1}"
# PAM_TTY is in the form /dev/pts/X
# Last utility displays TTY in the form pts/x
# Returns the first five characters stripped from TTY
echo "${terminal:5}"
}
lastTerminal=$( stripTerminal "${PAM_TTY}")
SSH_SESSION=$(isSshSession "${lastTerminal}")
if "${SSH_SESSION}"; then
exit 1
else
exit 0
fi
....
auth [success=ok default=1] pam_exec.so /etc/security/deny-ssh-user.sh
auth sufficient pam_module_to_skip.so
....
Вот решение, которое мне подходит. Мой/etc/pam.d/sudo
:
#%PAM-1.0
auth [success=1] pam_exec.so /tmp/test-pam
auth required pam_deny.so
auth include system-auth
account include system-auth
session include system-auth
И/tmp/test-pam
:
#! /bin/bash
/bin/last -i -p now ${PAM_TTY#/dev/} | \
/bin/awk 'NR==1 { if ($3 != "0.0.0.0") exit 9; exit 0; }'
У меня такое поведение:
$ sudo date
[sudo] password for jdoe:
Thu Jun 28 23:51:58 MDT 2018
$ ssh localhost
Last login: Thu Jun 28 23:40:23 2018 from ::1
valli$ sudo date
/tmp/test-pam failed: exit code 9
[sudo] password for jdoe:
sudo: PAM authentication error: System error
valli$
Первая строка, добавленная к строке pam.d/sudo
по умолчанию, вызывает pam_exec
и, в случае успеха, пропускает следующую запись. Вторая строка просто безоговорочно запрещает доступ.
В /tmp/test-pam
я вызываю last
, чтобы получить IP-адрес, связанный с TTY, с которого был вызван pam. ${PAM_TTY#/dev/}
удаляет /dev/
из начала значения, потому что last
не распознает полный путь к устройству. Флаг -i
заставляет last
показывать либо IP-адрес, либо заполнитель 0.0.0.0
, если IP-адрес отсутствует; по умолчанию он показывает информационную строку, которую гораздо сложнее проверить. Вот почему я использовал last
вместо who
или w
; у них нет аналогичного варианта. Опция -p now
не является строго необходимой, так как мы увидим, что awk
проверяет только первую строку вывода, но ограничивает last
отображением только тех пользователей, которые в данный момент вошли в систему.
Команда awk
просто проверяет первую строку,и если третье поле не 0.0.0.0
, оно завершается с ошибкой. Поскольку это последняя команда в /tmp/test-pam
, код выхода awk становится кодом выхода для скрипта.
В моей системе ни один из тестов, которые вы пробовали в deny-ssh-user.sh
, не работал. Если вы поместите env > /tmp/test-pam.log
в начало вашего сценария, вы увидите, что среда была удалена, поэтому ни одна из ваших переменных SSH _FOO не будет установлена. А $PPID может указывать на любое количество процессов. Например, запустите perl -e 'system("sudo cat /etc/passwd")'
и убедитесь, что $PPID относится к процессу perl
.
Это Arch Linux, ядро 4.16.11-1-ARCH
, если это имеет значение. Я не думаю, что это должно, хотя.