17 ACL setfacl getfacl
ACL 介绍
ACL 在 Linux 内核 2.6 被支持,是用文件的
扩展属性 实现的,扩展属性名为 system.posix_acl_access
。
要想在 ext2、ext3、ext4 或 reiserfs 文件系统上创建 ACL,装配相应的文件系统时需要带
mount –o acl
选项。
ACL 的功能是传统访问权限的超集,优先于传统访问权限生效。每个 ACL 项目(Access Control Entry,即 ACE)指定一个 rwx(3 个二进制位)的权限。其中:
ACL_USER_OBJ
、ACL_GROUP_OBJ
和ACL_OTHER
(在没有指定其他项目时)相当于传统权限,这个时候 ACL 被称为最小 ACL。- 支持对特定用户(
ACL_USER
)或者特定组(ACL_GRUOP
)指定权限。 - 支持掩码。
ACL_MASK
记录了可由ACL_USER
、ACL_GROUP_OBJ
以及ACL_GROUP
型 ACE 所能授予的最高权限。但如果 ACL 含有标记类型为ACL_USER
或ACL_GROUP
的记录,那么就必须包含一条ACL_MASK
型的 ACE。如果包含了掩码项目,ACL 就成为扩展 ACL。 ACL_USER_OBJ
、ACL_GROUP_OBJ
、ACL_OTHER
和ACL_MASK
都只能出现一次。
ACL 权限匹配算法
ACL 的权限检查同样是具有短路特性的。
- 如果是特权进程,则放行。(实际实现的时候可能会把这一步放在最后作为 fallback,因为系统审计程序只应该记录进程真正需要使用到特权的情况。)
- 按照用户检查,如果进程有效用户 ID 和
ACL_USER_OBJ
匹配,则按照这一项检查权限。 - 否则,如果进程有效用户 ID 和某个
ACL_USER
匹配,则将它和ACL_MASK
求与作为权限结果。 - 否则,如果进程的有效组 ID 或者辅助组 ID 之一满足全部权限检查,则放行。
- 如果组 ID 匹配
ACL_GROUP_OBJ
,则将其和ACL_MASK
求与(如果没有ACL_MASK
则跳过这一步)。 - 如果组 ID 匹配
ACL_GRUOP
,则将其和ACL_MASK
求与(此时ACL_MASK
必定存在)。
- 如果组 ID 匹配
- 否则,按照
ACL_OTHER
检查权限(ACL_MASK
不会影响这一项)。
第 4 点的全部权限检查举例:如果有一个进程对应了两个组 A 和 B,且在 ACL 中,A 对文件有读权限,B 对文件有写权限,那么进程单独申请读权限或者单独申请写权限都是能够通过检查的,但是进程申请读写权限时会失败。
ACL 的文本格式
ACL 具有长文本格式和短文本格式。长文本格式是 ACE 用换行符连接,并且可以包含 #
开头的注释;短文本格式是 ACE 用逗号连接。
ACL 的组成是 tag:[tag-qualifier]:permissions
(标记类型、标记限定符、权限集合)。当 tag 为用户或者组时,不提供限定符则表示文件所属用户或者所属组,提供限定符(用户名或者组名)则表示给定的用户或者组。一些例子:
命令 setfacl
和 getfacl
Debian 需要额外安装 acl 包。
每次用 setfacl
操作 ACL 但是又没有指明 mask 规则时,mask 会被自动调整成 ACL_USER
、ACL_GROUP_OBJ
以及 ACL_GROUP
型 ACE 的并集。想要禁用这个行为,需要传入参数 -n
/ --no-mask
。
ACL_MASK
的意义以及传统权限与 ACL 的同步
很多应用不知道 ACL 的存在,会使用 chmod
等系统调用来修改权限,比如用 chmod
修改权限为 0700(开头的 0 表示是八进制数),表示除了文件主人之外都没有访问权限,这个时候如果没有 ACL_MASK
,就需要把所有特定用户和特定组的权限都改成 0 以阻止它们访问。但是这样做又有一个问题,将来用 chmod
改权限为 0777 的时候,以前设置的特定用户、特定组的规则都丢失了,难道还要给它们都设置成 0777 吗?
在有 ACL_MASK
的时候(回忆第一节的内容,一旦有特定用户规则或者特定组规则,这个掩码就一定存在):
- 对传统的组权限(就是 user/group/other 的 group)的修改会反映在
ACL_MASK
而不是ACL_GROUP_OBJ
上。 stat
系统调用返回的st_mode
中的 group 对应位置替换为ACL_MASK
的二进制表示,而不是 inode 中存储的组权限。如果用ls -l
去显示文件权限,权限串会以+
结尾,表示当前 ACL 是扩展 ACL,而且 group 权限显示的是ACL_MASK
(因为stat
返回的数据就是这样)。- 可以用
getxattr
去获得文件的system.posix_acl_access
扩展属性。(如果没有ACL_MASK
,那么 ACL 是最小 ACL,这个时候去访问会出现 no data available 的错误。)但是因为我不知道如何反序列化它,拿到数据之后也不知道怎么解读,可能得看内核源码去寻找答案。
传统权限与 ACL 的同步方式如下:
- 无
ACL_MASK
时 ,ACL 是最小 ACL,user 权限对应ACL_USER_OBJ
,group 权限对应ACL_GROUP_OBJ
,other 权限对应ACL_OTHER
,chmod
和setfacl
操作以上权限都只会修改 inode 文件权限,而扩展属性不存在、也不会被创建。 - 有
ACL_MASK
时,ACL 是扩展 ACL,inode 文件权限和文件扩展属性中的 ACL 信息是两份独立数据,但是chmod
和setfacl
在更新 user 和 other 权限时会相互同步。Group 权限比较特殊,它对应于ACL_MASK
,chmod
对组权限的修改会直接修改这个,而不影响 inode 模式(st_mode
)中的权限,由于操作对象是同一个所以不需要同步。setfacl
对ACL_GROUP_OBJ
的修改还是会同步到 inode 模式的组权限上。
有个小问题:假设文件的 ACL 是 user::rw-,group::---,mask::---,other::r--
,对该文件使用 chmod g+rw
,ACL 会变成 user::rw-,group::---,mask::rw-,other::r--
,由于不能真正改变组权限且 ACL 的优先级高于 inode 模式位,此时文件同组不同用户仍然无法访问文件。要访问文件只能用 setfacl
修改组权限,或者删除掩码(这个时候没有特定用户或特定组的规则,可以删除掩码)。
用以下操作证明文件的 ACL 是扩展 ACL 时,EA 中会 user 权限和 group 权限的冗余数据并保持同步,而不是直接透明修改 inode 模式位:
(py310) xxx /data/ $ diff <(./build/main < A.txt) <(./build/main < B.txt)
# 此时刚刚用 setfacl -m m::rwx 给 A.txt 和 B.txt 分别创建了 ACL_MASK 类型的 ACE
(py310) xxx /data/ $ getfacl A.txt
# file: A.txt
# owner: simon
# group: simon
user::rw-
group::r--
mask::rwx
other::r--
(py310) xxx /data/ $ setfacl -m u::rw A.txt
(py310) xxx /data/ $ diff <(./build/main < A.txt) <(./build/main < B.txt)
Binary files /dev/fd/63 and /dev/fd/62 differ
A.txt 的用户权限已经是 rw,现在用 setfacl
设置同样的权限,A、B 两个文件的扩展属性有差异,说明 setfacl
本身是存储了一份信息的,而不是透明映射到 inode 文件权限中去。
build/main 的代码如下,它直接以二进制形式输出标准输入流对应文件的扩展属性:
#include <stdio.h>
#include <sys/xattr.h>
int main() {
char buf[256] = {0};
ssize_t sz = getxattr("/proc/self/fd/0", "system.posix_acl_access", buf, sizeof(buf));
if (sz == -1) {
perror("getxattr");
return 1;
}
for (ssize_t i = 0; i < sz; ++i) {
putchar(buf[i]);
}
}
默认型 ACL(相当于 umask)
之前介绍的 ACL 都是访问型的(从 EA 名的命名方式可以看出来),其实还有一种 ACL,即默认型 ACL,对应的 EA 名为 system.posix_acl_default
。这种 ACL 只能针对目录设置,且目录下新创建的子目录会继承默认型 ACL,目录下新创建的子目录和子文件会将默认型 ACL 作为自己的访问型 ACL。默认型 ACL 的作用有点相当于进程的 umask,而且它存在时 umask 会被忽略。
如果目录有默认型 ACL,其下用 mkdir
或者 open
等系统调用创建的新的文件会和默认型 ACL 发生按位与操作:
ACL_USER_OBJ
对应 umask 的 user 部分。ACL_MASK
(若不含ACL_MASK
,则为ACL_GROUP_OBJ
)对应 umask 的 group 部分。ACL_OTHER
对应 umask 的 other 部分。
想要用 getfacl
和 setfacl
来查看 / 修改默认型 ACL,需要加 -d
参数;想要用 setfacl
删除默认型 ACL,需要加 -k
参数。
ACL 的数量限制
单个文件的 ACL 记录长度受制于 文件系统扩展属性的长度。在 ext2、ext3 以及 ext4 文件系统中,由于单个文件扩展属性的所有信息不能超过一个逻辑磁盘块大小,因此 ACL 的数量还是很有限的。有些文件系统比如 JFS 限制就要宽松很多。
TLPI 作者的观点是:ACL 数量非常有限,而且内容长了也不便于维护,最好用组的形式去管理权限,而少用特定用户的 ACL 项(ACE)。
ACL API
ACL 的 API 非常多,使用前需要包含 <sys/acl.h>
。如果还用到了 POSIX.1e 标准草案中的各种 Linux 扩展(acl(5) 手册页罗列了一系列 Linux 扩展),程序可能还需要包含 <acl/libacl.h>
,此时还需要加上 -lacl
选项以完成链接。为了能在 man pages 看到 acl 相关 API 的说明,需要先安装 libacl(在 Debian 上是 libacl1-dev)。
最核心的两个类型是 acl_t
和 acl_entry_t
,其中前者被实现为指针类型。
还有一些 ACL 相关的调用没有在图中出现。比如:
acl_calc_mask(&acl)
用来给acl_t
类型的acl
重新计算ACL_MASK
型记录(如果必要)为ACL_USER
、ACL_GROUP
以及ACL_GROUP_OBJ
型记录的权限并集。acl_valid(acl)
用来检查acl
的有效性。acl_delete_def_file(pathname)
用来删除目录的默认 ACL。acl_init(count)
创建一个空的 ACL 结构。acl_dup(acl)
为acl
创建副本。acl_free(handle)
释放 ACL 相关 API 分配的内存。