Redis ACL从使用到实现

简介

Redis ACL是6.0版本中推出的新功能。是一项可以限制用户连接可执行命令和键访问操作的功能。
客户端在连接服务器之后,客户端需要提供用户名和密码用于验证身份,如果验证成功,客户端连接会关联特定用户以及用户相应的权限。Redis可以配置新的客户端连接自动使用默认用户(default)进行验证(默认选项)。
此外,ACL功能对旧版客户端和应用都是向后兼容的,对于旧版配置用户密码的方式(requirepass)也是支持的。该命令设置的是default用户的密码,即auth password 等价于 auth default password。没有指定用户的客户端连接使用的都是default,可以通过控制defalut用户权限来兼容老版本。

ACL使用场景

  1. 你希望限制用户访问命令和键以提高安全性。不在信任列表中的用户没有权限访问,而在信任列表中的用户拥有完成工作的最小访问权限。例如一些客户端只可以执行只读的命令。
  2. 你希望提供运维安全。避免程序出错或者人为操作失误导致数据或者配置受到损坏。比如执行flush、keys等。在之前版本中都是通过命令重命令的方式来对使用者隐藏,但是这样可能会存在其他隐患。

ACL使用

ACL规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
on:启用用户
off:禁止用户
+:添加命令到用户允许执行命令列表
-:从用户允许执行命令列表删除命令
+@:允许用户执行定义在category类别中的命令
-@:删除用户拥有的category类别命令权限
+|subcommand:允许用户启用一个被禁止类别下的子命令。
allcommands:+@all 的别名.
nocommands:-@all 的别名。
~:添加一个键值模式的权限
allkeys:是 ~* 的别名
resetkeys:在键模式列表里面清空所有的键模式
>:添加密码到用户有效密码列表里
<:从用户有效密码列表中删除密码
#:添加 SHA-256 形式哈希值到用户有效密码列表里
!:从有效的密码列表中删除哈希值密码
nopass:删除所有与用户关联的密码
resetpass:清除用户所有密码
reset:用户将返回和它被默认创建时同样的状态。

ACL命令使用

1
2
3
4
5
6
7
8
9
10
11
12
/* ACL -- show and modify the configuration of ACL users.
* ACL HELP // 获取帮助信息
* ACL LOAD // 从外部ACL文件导入用户信息
* ACL LIST // 所有用户权限信息
* ACL USERS // 所有用户的列表
* ACL CAT [<category>] // 命令分类,及分类下的具体命令列表
* ACL SETUSER <username> ... acl rules ... // 添加用户、权限修改等操作
* ACL DELUSER <username> [...] // 删除用户
* ACL GETUSER <username> // 获取指定用户的信息
* ACL GENPASS // 生成一个密码
* ACL WHOAMI // 获取当前用户
*/

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建一个未启用的用户
127.0.0.1:6379> acl setuser demo
OK

// 查看所有用户
127.0.0.1:6379> acl list
1) "user alice on >1234 ~alice:* +@all"
2) "user default on nopass ~* +@all"
3) "user demo off -@all"

// 启用用户,设置密码为1234,并授予demo:* 键值对的所有权限
127.0.0.1:6379> acl setuser demo on >1234 ~demo:* +@all
OK
127.0.0.1:6379> acl list
1) "user alice on >1234 ~alice:* +@all"
2) "user default on nopass ~* +@all"
3) "user demo on >1234 ~demo:* +@all"

实现

Redis新增了一个user结构体。结构体具体定义如下。
需要注意的是:

  1. Redis使用allowed_commands数据来记录是否拥有命令的执行权限。每一种命令对应其中的一个字节(使用全局的一棵rax树保存命令,id获取函数为ACLGetCommandID)。
  2. 密码使用的是一个list列表。表示一个用户允许多个密码登录。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // 用户定义
    typedef struct user {
    // 用户名
    sds name; /* The username as an SDS string. */
    // 用户标记
    uint64_t flags; /* See USER_FLAG_* */

    /* The bit in allowed_commands is set if this user has the right to
    * execute this command. In commands having subcommands, if this bit is
    * set, then all the subcommands are also available.
    *
    * If the bit for a given command is NOT set and the command has
    * subcommands, Redis will also check allowed_subcommands in order to
    * understand if the command can be executed. */
    // 允许执行的命令数组
    uint64_t allowed_commands[USER_COMMAND_BITS_COUNT/64];

    /* This array points, for each command ID (corresponding to the command
    * bit set in allowed_commands), to an array of SDS strings, terminated by
    * a NULL pointer, with all the sub commands that can be executed for
    * this command. When no subcommands matching is used, the field is just
    * set to NULL to avoid allocating USER_COMMAND_BITS_COUNT pointers. */
    // 一个数组指针,表示在命令ID下允许执行的具体命令列表,以NULL结束。
    // 如果没有允许执行的命令,这个值为NULL,避免分配USER_COMMAND_BITS_COUNT空间。
    sds **allowed_subcommands;
    // 密码
    list *passwords; /* A list of SDS valid passwords for this user. */
    // 运行执行命令的匹配符列表。如果这个值为NULL表示不允许执行任何命令。除非这个用户的flag为ALLKEYS。
    list *patterns; /* A list of allowed key patterns. If this field is NULL
    the user cannot mention any key in a command, unless
    the flag ALLKEYS is set in the user. */
    } user;

acl命令具体实现在acl.c -> aclCommand(),下面我们将通过这个函数来分析acl命令的具体实现。

增加用户及权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
if (!strcasecmp(sub,"setuser") && c->argc >= 3) {
sds username = c->argv[2]->ptr;
/* Create a temporary user to validate and stage all changes against
* before applying to an existing user or creating a new user. If all
* arguments are valid the user parameters will all be applied together.
* If there are any errors then none of the changes will be applied. */
// 创建一个临时的用户去验证执行阶段,如果在某个阶段存在问题也不会影响原用户。
user *tempu = ACLCreateUnlinkedUser();

// 判断该用户是否已存在,如果存在。将权限同步给临时用户
user *u = ACLGetUserByName(username,sdslen(username));
if (u) ACLCopyUser(tempu, u);

for (int j = 3; j < c->argc; j++) {
// 根据传参执行授权操作
if (ACLSetUser(tempu,c->argv[j]->ptr,sdslen(c->argv[j]->ptr)) != C_OK) {
char *errmsg = ACLSetUserStringError();
addReplyErrorFormat(c,
"Error in ACL SETUSER modifier '%s': %s",
(char*)c->argv[j]->ptr, errmsg);

ACLFreeUser(tempu);
return;
}
}

/* Overwrite the user with the temporary user we modified above. */
if (!u) u = ACLCreateUser(username,sdslen(username));
serverAssert(u != NULL);
ACLCopyUser(u, tempu);
ACLFreeUser(tempu);
addReply(c,shared.ok);

acl setuser是Redis用来增加用户及权限的命令。我们可以看到在授权中,会先创建一个临时用户,先在临时用户上进行权限修改操作。如果在某阶段执行出错,前面执行成功的权限修改行为并不会影响到原用户。
调用的关键函数为:

  • ACLSetUser:按照ACL规则来进行具体的授权行为。
  • ACLCopyUser:将一个用户的所有权限参数拷贝给一个新用户。

其余功能实现就不介绍了。。

权限检查

在所有命令执行前,都会调用processCommand函数。在这个函数中,会进行一些命令执行前的常规检查,比如参数个数、实例状态、用户权限等。
该函数会调用acl.c -> ACLCheckCommandPerm()函数来检查用户是否拥有命令和键值对的执行权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/* Check if the command ready to be excuted in the client 'c', and already
* referenced by c->cmd, can be executed by this client according to the
* ACls associated to the client user c->user.
*
* If the user can execute the command ACL_OK is returned, otherwise
* ACL_DENIED_CMD or ACL_DENIED_KEY is returned: the first in case the
* command cannot be executed because the user is not allowed to run such
* command, the second if the command is denied because the user is trying
* to access keys that are not among the specified patterns. */
// 检查当前客户端是否拥有执行命令的权限
int ACLCheckCommandPerm(client *c) {
user *u = c->user;
uint64_t id = c->cmd->id;

/* If there is no associated user, the connection can run anything. */
if (u == NULL) return ACL_OK;

/* Check if the user can execute this command. */
// 检查是否拥有执行命令的权限
if (!(u->flags & USER_FLAG_ALLCOMMANDS) &&
c->cmd->proc != authCommand)
{
/* If the bit is not set we have to check further, in case the
* command is allowed just with that specific subcommand. */
if (ACLGetUserCommandBit(u,id) == 0) {
/* Check if the subcommand matches. */
if (c->argc < 2 ||
u->allowed_subcommands == NULL ||
u->allowed_subcommands[id] == NULL)
{
return ACL_DENIED_CMD;
}

long subid = 0;
while (1) {
if (u->allowed_subcommands[id][subid] == NULL)
return ACL_DENIED_CMD;
if (!strcasecmp(c->argv[1]->ptr,
u->allowed_subcommands[id][subid]))
break; /* Subcommand match found. Stop here. */
subid++;
}
}
}

/* Check if the user can execute commands explicitly touching the keys
* mentioned in the command arguments. */
// 检查是否拥有键值对操作的权限
if (!(c->user->flags & USER_FLAG_ALLKEYS) &&
(c->cmd->getkeys_proc || c->cmd->firstkey))
{
int numkeys;
int *keyidx = getKeysFromCommand(c->cmd,c->argv,c->argc,&numkeys);
for (int j = 0; j < numkeys; j++) {
listIter li;
listNode *ln;
listRewind(u->patterns,&li);

/* Test this key against every pattern. */
int match = 0;
while((ln = listNext(&li))) {
sds pattern = listNodeValue(ln);
size_t plen = sdslen(pattern);
int idx = keyidx[j];
if (stringmatchlen(pattern,plen,c->argv[idx]->ptr,
sdslen(c->argv[idx]->ptr),0))
{
match = 1;
break;
}
}
if (!match) {
getKeysFreeResult(keyidx);
return ACL_DENIED_KEY;
}
}
getKeysFreeResult(keyidx);
}

/* If we survived all the above checks, the user can execute the
* command. */
return ACL_OK;
}

https://www.infoq.cn/article/fe97cBWx6Hqk9qVvoW7r