测试特定UID是否对一个目录具有写入权限?

52

我们可以通过当前进程的uid测试一个目录是否可写:

if [ -w $directory ] ; then echo 'Eureka!' ; fi

但有谁能建议一种方法来测试某个目录是否可被其他UID写入?

我的情况是我正在管理一个MySQL服务器实例,我想暂时更改慢查询日志文件的位置。我可以通过执行 MySQL 命令 SET GLOBAL slow_query_log_file='$new_log_filename' 以及禁用和启用查询日志记录使 mysqld 开始使用该文件来完成此操作。

但我希望我的脚本检查 mysqld 进程的 UID 是否具有创建新日志文件的权限。因此,我想做类似于以下伪代码的事情:

$ if [ -w-as-mysql-uid `basename $new_log_filename` ] ; then echo 'Eureka!' ; fi

当然,这只是一个虚构的测试前提。

澄清:我希望找到一种不依赖su的解决方案,因为我不能假定我的脚本用户具有su权限。


相关的 Ruby 问题:http://stackoverflow.com/questions/13369667/programmatically-test-whether-an-arbitrary-user-has-access-to-an-arbitrary-file - Todd A. Jacobs
有趣的是,OS X上的Server Admin.app具有“权限检查器”,用户可以将其拖放到其中,然后选择任何文件,它将显示该用户可以和不能执行的所有文件操作。不过我不知道它是如何工作的。你会觉得这很容易。 - Phil Frost
您真的需要这样一个通用的解决方案吗?为什么不要求目录由mysql拥有并具有所有者写入权限,然后只需检查特定情况? - Barmar
@Bamar,我正在尝试开发一个工具,将对MySQL社区有普遍用途,而不是每个人都以文字用户“mysql”运行MySQL(即使他们应该这样做)。 - Bill Karwin
8个回答

32

这是一个冗长而拐弯抹角的检查方法。

USER=johndoe
DIR=/path/to/somewhere

# Use -L to get information about the target of a symlink,
# not the link itself, as pointed out in the comments
INFO=( $(stat -L -c "%a %G %U" "$DIR") )
PERM=${INFO[0]}
GROUP=${INFO[1]}
OWNER=${INFO[2]}

ACCESS=no
if (( ($PERM & 0002) != 0 )); then
    # Everyone has write access
    ACCESS=yes
elif (( ($PERM & 0020) != 0 )); then
    # Some group has write access.
    # Is user in that group?
    gs=( $(groups $USER) )
    for g in "${gs[@]}"; do
        if [[ $GROUP == $g ]]; then
            ACCESS=yes
            break
        fi
    done
elif (( ($PERM & 0200) != 0 )); then
    # The owner has write access.
    # Does the user own the file?
    [[ $USER == $OWNER ]] && ACCESS=yes
fi

3
警告关于符号链接,因为它们的权限可能是lrwxrwxrwx,但目标文件的权限却不同。 - F. Hauri - Give Up GitHub
3
我发现我必须改变stat选项的顺序,将stat -Lc "fmt" 改为 stat -cL - tripleee
5
我遇到了一个错误提示:unexpected token `&': conditional binary operator expected,具体信息可以在这个链接找到。 - tripleee
1
最后,stat -c '%a' 对我来说没有打印前导零,所以我不得不将其更改为'0%a',以便算术运算正确工作。 - tripleee
迟来的是参考链接:https://meta.stackoverflow.com/questions/260245/when-should-i-make-edits-to-code,但它对于在*答案*中编辑代码比我记得的要宽松得多。 - tripleee
显示剩余7条评论

10

那可以进行测试:

if read -a dirVals < <(stat -Lc "%U %G %A" $directory) && (
    ( [ "$dirVals" == "$wantedUser" ] && [ "${dirVals[2]:2:1}" == "w" ] ) ||
    ( [ "${dirVals[2]:8:1}" == "w" ] ) ||
    ( [ "${dirVals[2]:5:1}" == "w" ] && (
        gMember=($(groups $wantedUser)) &&
        [[ "${gMember[*]:2}" =~ ^(.* |)${dirVals[1]}( .*|)$ ]]
    ) ) )
  then
    echo 'Happy new year!!!'
  fi

解释:

只有一个测试(if),没有循环和分支。

+注意:我使用的是stat -Lc而不是stat -c,因此这也适用于符号链接!

所以条件是if,

  • 我可以成功读取$directory的状态,并将其赋值给dirVals
  • 并且(
    • (所有者匹配标志用户可写存在)
    • 或者标志other Writeable存在
    • 或者(标志GroupWriteabe存在
      • 我可以将$wantedUser的成员列表成功分配给gMember并且
      • 通过合并$gMember的第2到最后一个字段构建的字符串将与beginOfSting-Or-something-followed-by-a-space匹配,紧接着目标组(${dirVals[1]}),紧接着a-space-followed-by-something-Or-endOfString。)
    )

然后echo Happy new year!

由于组的测试暗示了第二个fork(我喜欢尽可能减少这样的调用),因此这是要执行的最后一个测试。

旧:

简单:

su - mysql -c "test -w '$directory'" && echo yes
yes
或:
if su - mysql -s /bin/sh -c "test -w '$directory'" ; then 
    echo 'Eureka!'
  fi

注意:为了让$directory能够发挥作用,务必首先用双引号将其括起来!


+1 是的,这个方法可以行得通,所以这是一个正确的答案,但我希望避免使用 su,因为我不能假设用户具有 su 权限。 - Bill Karwin
所以看一下 stat -c "%U %G %a" $directory 或者 stat -c "%u %g %A" $directory 或者任何变体... 新年快乐! - F. Hauri - Give Up GitHub
ACL是什么情况?而且,$wantedGroup不是必须要遍历$wantedUser所在的所有组吗? - Phil Frost
1
@PhilFrost 抱歉,我因为新年压力大而感到紧张...有更好的工作。 - F. Hauri - Give Up GitHub
+1,虽然你应该按照用户、组、其他的顺序进行检查,而不是用户、其他、组,因为一个文件比“组”权限更多的“其他”权限(非常罕见,我知道)会失败。例如:touch foo; chmod 044; [ -w foo ] || echo "I can't write to a file where I don't have permissions but others have" - Carlos Campderrós
+1, 谢谢。对我有效。但有一个问题:除了当前用户之外,它在我的/tmp(1777)上失败了,没有进一步检查,只是想说明一下。 - mviereck

8
你可以使用sudo在你的脚本中执行测试。例如:
sudo -u mysql -H sh -c "if [ -w $directory ] ; then echo 'Eureka' ; fi"

为此,执行脚本的用户当然需要具备sudo特权。
如果您需要 UID 而不是用户名,也可以使用:
sudo -u \#42 -H sh -c "if [ -w $directory ] ; then echo 'Eureka' ; fi"

在这种情况下,42mysql用户的uid。如果需要,请替换为您自己的值。
更新(以支持非sudo权限用户) 要让bash脚本在没有sudo权限的情况下更改用户,需要执行suid(“切换用户ID”)的能力。正如此答案所指出的那样,这是一项安全限制,需要通过黑客工具进行解决。请参阅这篇博客以获取一个“如何”绕过它的示例(我没有测试/尝试过,因此无法确认其成功率)。
如果可能的话,我的建议是编写一个C脚本,并给予其suid权限(尝试chmod 4755 file-name)。然后,您可以从C脚本中调用setuid(#)来设置当前用户的ID,然后继续从C应用程序执行代码,或者让它执行一个运行所需/想要的任何命令的单独的bash脚本。虽然这也是一种很有技巧性的方法,但就非sudo替代方案而言,这可能是最简单的方法之一(我个人认为)。

+1 是的,这个方法可以行得通,所以这是一个正确的答案,但我希望避免使用 su,因为我不能假设用户具有 su 权限。 - Bill Karwin
-H, --set-home 是什么意思? - papo
我正在做类似的事情 if ! sudo -u [App_xx] --shell [ -r "$path" -a -w "$path" ]; then ... 然后我可以作为root采取行动/修复权限。 - papo

7
因为我必须对@chepner的答案进行一些更改才能使其正常工作,所以我在这里发布了我的临时脚本,以便轻松复制和粘贴。这只是一个小的重构,并且我已经投票支持了chepner的答案。如果被接受的答案更新了这些修复程序,我将删除我的答案。我已经在那个答案上留下了评论,指出我遇到的问题。
我想摆脱Bashisms,这就是为什么我根本不使用数组的原因。((算术求值))仍然是一个仅限于Bash的特性,所以我还是被困在Bash中。
for f; do
    set -- $(stat -Lc "0%a %G %U" "$f")
    (("$1" & 0002)) && continue
    if (("$1" & 0020)); then
        case " "$(groups "$USER")" " in *" "$2" "*) continue ;; esac
    elif (("$1" & 0200)); then
        [ "$3" = "$USER" ] && continue
    fi
    echo "$0: Wrong permissions" "$@" "$f" >&2
done

没有注释的话,这段代码甚至相当紧凑。

1
+1 感谢测试和解决方案!请不要删除。 - Bill Karwin

3

有一个有趣的可能性(但不再是bash了)是创建一个带有suid标志的C程序,属于mysql所有。

步骤1。

创建这个美妙的C源文件,并将其命名为caniwrite.c(抱歉,我一直很糟糕地选择名称):

#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc,char* argv[]) {
   int i;
   for(i=1;i<argc;++i) {
      if(eaccess(argv[i],W_OK)) {
         return EXIT_FAILURE;
      }
   }
   return EXIT_SUCCESS;
}

步骤二:

编译:

gcc -Wall -ocaniwrite caniwrite.c

步骤三。

将其移动到您喜欢的任何文件夹中,/usr/local/bin/ 是一个不错的选择,更改其所有权并设置suid标志:(以root用户身份执行此操作)

# mv -nv caniwrite /usr/local/bin
# chown mysql:mysql /usr/local/bin/caniwrite
# chmod +s /usr/local/bin/caniwrite

完成!

只需按照以下方式调用:

if caniwrite folder1; then
    echo "folder1 is writable"
else
    echo "folder1 is not writable"
fi

事实上,你可以使用任意数量的参数调用caniwrite方法。如果所有目录(或文件)都是可写的,则返回值为true,否则返回false。


LOL谢谢建议,但我将我的工具编写为bash脚本的目的是避免部署C代码! :-) - Bill Karwin
@BillKarwin 除此之外,我相信没有其他的bash解决方案可以解决这个问题。虽然有一些可行的hack方法,但是在100%的情况下都不会起作用(考虑到ACL和管理组的各种可能性)。现在,我提供的解决方案并不需要大量的C代码部署!它只是一个对eaccess调用的简单包装器。而且还很有趣 ;-) - gniourf_gniourf

2

alias wbyu='_(){ local -i FND=0; if [[ $# -eq 2 ]]; then for each in $(groups "$1" | awk "{\$1=\"\";\$2=\"\"; print \$0}"); do (($(find "${2}" \( -perm /220 -o -group "$each" -a -perm /g+w \) 2>/dev/null | wc -l))) && FND=1; done; else echo "使用方法: wbyu <用户> <文件|目录>"; fi; (($FND)) && echo "发现可写目录!"; }; _'

我将它放入一个别名中,它需要两个参数,第一个是用户,第二个是要检查的目录。它查找任何人都可以写入的权限,并循环遍历指定用户的组,以检查目录是否在用户组中并且可写 - 如果其中任意一个命中,它就会设置一个找到标志并在最后打印“发现可写目录!”。

换句话说:

FND=0
USER=user1
DIR=/tmp/test
for each in $(groups "$USER" | awk '{$1="";$2=""; print $0}'); do 
(($(find "$DIR" \( -perm /220 -o -group "$each" -a -perm /g+w \)\ 
   2>/dev/null | wc -l))) && FND=1 
done
(($FND)) && echo 'Eureka!'

谢谢,这非常有创意! - Bill Karwin
这个别名似乎完全是多余的;只需调用函数 wbyu 并且不使用别名即可。 - tripleee

2
我编写了一个名为can_user_write_to_file的函数,如果传递给它的用户是文件/目录的所有者或属于具有对该文件/目录写访问权限的组,则函数将返回1。否则,该方法返回0
## Method which returns 1 if the user can write to the file or
## directory.
##
## $1 :: user name
## $2 :: file
function can_user_write_to_file() {
  if [[ $# -lt 2 || ! -r $2 ]]; then
    echo 0
    return
  fi

  local user_id=$(id -u ${1} 2>/dev/null)
  local file_owner_id=$(stat -c "%u" $2)
  if [[ ${user_id} == ${file_owner_id} ]]; then
    echo 1
    return
  fi

  local file_access=$(stat -c "%a" $2)
  local file_group_access=${file_access:1:1}
  local file_group_name=$(stat -c "%G" $2)
  local user_group_list=$(groups $1 2>/dev/null)

  if [ ${file_group_access} -ge 6 ]; then
    for el in ${user_group_list-nop}; do
      if [[ "${el}" == ${file_group_name} ]]; then
        echo 1
        return
      fi
    done
  fi

  echo 0
}

为了测试它,我写了一个小测试函数:

function test_can_user_write_to_file() {
  echo "The file is: $(ls -l $2)"
  echo "User is:" $(groups $1 2>/dev/null)
  echo "User" $1 "can write to" $2 ":" $(can_user_write_to_file $1 $2)
  echo ""
}

test_can_user_write_to_file root /etc/fstab
test_can_user_write_to_file invaliduser /etc/motd
test_can_user_write_to_file torstein /home/torstein/.xsession
test_can_user_write_to_file torstein /tmp/file-with-only-group-write-access

至少从这些测试中,该方法在考虑文件所有权和组写访问方面是按预期工作的 :-)


2

为什么不尝试一些简单的操作,比如在相关文件夹上进行一个mkdir操作。这样更加可靠...

    mkdir your_directory/
    [[ $? -ne 0 ]] && echo "fatal" || echo "winner winner chicken dinner.."

或者?
    # -- run the following commands as the_User_ID
    sudo su - the_User_ID << BASH

    mkdir your_directory/
    [[ $? -ne 0 ]] && echo "fatal" || echo "winner winner chicken dinner.."

    BASH

创意解决方案,但我不需要知道当前进程的UID是否可以写入目录,我需要知道不同特定UID是否可以写入该目录。 - Bill Karwin
您在顶部的问题似乎没有暗示(在我看来),对此感到抱歉。我提供了一种以不同用户身份运行的方法。当然,有几种方法可以做到这一点,例如让分叉进程报告错误。 - Mike Q
谢谢,但需要sudo权限的解决方案并不是我要找的解决方案。在许多环境中,这是不允许的。 - Bill Karwin
$? 的显式检查是多余的,尽管这是一种常见的反模式。任何类似于 command; [[ $? -ne 0 ]] && failure || success 的东西都可以更好地、更符合惯用法地写成 command && success || failure(假设 success 本身总是成功的)。 - tripleee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接