Redis 在6.0 中加入了ACL(Access Control List) 的支持,在此前的版本中,Redis是没有用户的概念的,没有办法很好的控制权限,在6.0中加入了用户的概念,可以给每个用户分配不同的权限来控制权限。
ACL作用
- ACL的作用就是对不同用户的权限做限制,限制不同用户的权限。比如限制某些用户使用删除命令,限制某些用户只能使用读命令等等。在 6.0 之前,redis没有用户的概念,是不能做具体用户的限制的,如果有的用户执行了
FLUSHALL
命令,那么整个库的数据都没有了,这样是有风险的。
ACL命令
redis还是使用原来的 AUTH
命令来进行认证,这个命令做了对低版本的兼容
- 6.0 及以后版本
AUTH <username> <password>
- 6.0 之前版本
AUTH <password>
在使用 AUTH <password>
这个命令时,使用的是redis的默认用户,redis ACL 中初始化了一个默认用户default
这样就实现了对低版本的兼容。在 6.0
以后的版本中使用 AUTH <password>
这个命令时,实际上是对default用户做了认证。
ACL是使用 DSL(domain specific language)定义的,该 DSL 描述了给定用户能够执行的操作。此类规则始终从左到右从第一个到最后一个实施,因为有时规则的顺序对于理解用户的实际能力很重要。
ACL 简单的语法
看一下当前用户
1127.0.0.1:6379> acl list
21) "user default on nopass ~* &* +@all"
default
用户名on
表示用户是激活的,如果是off
那么这个用户无法通过AUTH
命令nopass
表示这个用户没有密码~*
表示可以访问的Key(正则匹配)&*
6.2 加入的,表示channel的权限+@
表示用户的权限,“+”表示授权权限,有权限操作或访问,“-”表示还是没有权限; @为权限分类,可以通过ACL CAT
查询支持的分类。+@all 表示所有权限,nocommands 表示不给与任何命令的操作权限
用户权限分类
1 "keyspace"
2 "read"
3 "write"
4 "set"
5 "sortedset"
6 "list"
7 "hash"
8 "string"
9 "bitmap"
10 "hyperloglog"
11 "geo"
12 "stream"
13 "pubsub"
14 "admin"
15 "fast"
16 "slow"
17 "blocking"
18 "dangerous"
19 "connection"
20 "transaction"
21 "scripting"
至于怎么操作,不是这次的重点,直接去看官方文档就好了 redis acl
ACL源码
开始上源码了,本次源码基于redis的 7.0.11版本,不同的版本之间可能有差异。
ACL相关的数据结构
ACL鉴权对应的是一个User
,先看一下User
和相关的selector
的定义
1typedef struct {
2 //用户名
3 sds name; /* The username as an SDS string. */
4
5 //用户的flag,用来做各种比对
6 uint32_t flags; /* See USER_FLAG_* */
7
8 //这个用户的密码,一个用户可以有多个密码,所以是用链表保存的
9 //这个链表的node是一个明文密码经过 `SHA256` 计算过后的 字符串
10 list *passwords; /* A list of SDS valid passwords for this user. */
11
12 //验证用户权限的选择器,该用户的权限保存在这个链表中
13 list *selectors; /* A list of selectors this user validates commands
14 against. This list will always contain at least
15 one selector for backwards compatibility. */
16 //缓存ACL命令的字符串
17 robj *acl_string; /* cached string represent of ACLs */
18} user;
19
20
21//user中 `selectors` 链表中的结构
22typedef struct {
23 //这个 selectors 的flag
24 uint32_t flags; /* See SELECTOR_FLAG_* */
25
26 /* The bit in allowed_commands is set if this user has the right to
27 * execute this command.
28 * If the bit for a given command is NOT set and the command has
29 * allowed first-args, Redis will also check allowed_firstargs in order to
30 * understand if the command can be executed. */
31
32 //使用 bit 记录允许的命令
33 uint64_t allowed_commands[USER_COMMAND_BITS_COUNT/64];
34
35 /* allowed_firstargs is used by ACL rules to block access to a command unless a
36 * specific argv[1] is given.
37 *
38 * For each command ID (corresponding to the command bit set in allowed_commands),
39 * This array points to an array of SDS strings, terminated by a NULL pointer,
40 * with all the first-args that are allowed for this command. When no first-arg
41 * matching is used, the field is just set to NULL to avoid allocating
42 * USER_COMMAND_BITS_COUNT pointers. */
43
44 sds **allowed_firstargs;
45
46 //redis key 匹配规则的字符串链表
47 list *patterns; /* A list of allowed key patterns. If this field is NULL
48 the user cannot mention any key in a command, unless
49 the flag ALLKEYS is set in the user. */
50
51 //redis channels 规则的字符串 链表
52 list *channels; /* A list of allowed Pub/Sub channel patterns. If this
53 field is NULL the user cannot mention any channel in a
54 `PUBLISH` or [P][UNSUBSCRIBE] command, unless the flag
55 ALLCHANNELS is set in the user. */
56} aclSelector;
57
58
59/* Structure used for handling key patterns with different key
60 * based permissions. */
61//`aclSelector` 中 patterns 链表中的结构
62typedef struct {
63 int flags; /* The CMD_KEYS_* flags for this key pattern */
64 sds pattern; /* The pattern to match keys against */
65} keyPattern;
aclSelector
结构先有个大概的了解即可,后面在权限校验的那部分会有详细的分析
ACL的初始化
ACL的初始化在ACLInit
函数中完成
1void ACLInit(void) {
2 Users = raxNew();
3 UsersToLoad = listCreate();
4 listSetMatchMethod(UsersToLoad, ACLListMatchLoadedUser);
5 ACLLog = listCreate();
6 DefaultUser = ACLCreateDefaultUser();
7}
ACLInit
这个函数在 main
函数中被调用,也就是在redis启动的时候,就会初始化ACL的数据结构
所有的用户都存在Users
这个全局变量中,这个变量是rax
类型的
UsersToLoad
记录了从配置文件中加载的用户
ACLLog
ACL操作相关的log
DefaultUser
特殊的用户,default
用户
会使用 ACLCreateDefaultUser
这个函数初始化default这个用户
1user *ACLCreateDefaultUser(void) {
2 user *new = ACLCreateUser("default",7);
3 ACLSetUser(new,"+@all",-1);
4 ACLSetUser(new,"~*",-1);
5 ACLSetUser(new,"&*",-1);
6 ACLSetUser(new,"on",-1);
7 ACLSetUser(new,"nopass",-1);
8 return new;
9}
创建一个用户是在ACLCreateUser
这个函数中完成的
1user *ACLCreateUser(const char *name, size_t namelen) {
2 //先判断一下这个鱼护是否存在,如果存在了直接return
3 if (raxFind(Users,(unsigned char*)name,namelen) != raxNotFound) return NULL;
4 user *u = zmalloc(sizeof(*u));
5 u->name = sdsnewlen(name,namelen);
6 //新创建的用户默认都是不能使用的
7 u->flags = USER_FLAG_DISABLED;
8 u->passwords = listCreate();
9 u->acl_string = NULL;
10 listSetMatchMethod(u->passwords,ACLListMatchSds);
11 listSetFreeMethod(u->passwords,ACLListFreeSds);
12 listSetDupMethod(u->passwords,ACLListDupSds);
13
14 u->selectors = listCreate();
15 listSetFreeMethod(u->selectors,ACLListFreeSelector);
16 listSetDupMethod(u->selectors,ACLListDuplicateSelector);
17
18 /* Add the initial root selector */
19 aclSelector *s = ACLCreateSelector(SELECTOR_FLAG_ROOT);
20 listAddNodeHead(u->selectors, s);
21
22 raxInsert(Users,(unsigned char*)name,namelen,u,NULL);
23 return u;
24}
到这,default
用户的初始化就完成了
后面会从配置文件中加载其他用户的数据,完成其他用户的初始化
在main
函数中调用 ACLLoadUsersAtStartup
完成从配置文件中加载用户
在此之前会先使用ACLAppendUserForLoading
函数完成从 redis.conf
中加载用户。因为redis中的用户既可以配置在redis.conf中,也可以配置在一个单独的ACL文件中(两者只能选择其中一个,不能同时启用)。这个函数是从redis.conf
中加载用户
最终把从配置文件中加载的用户都放到UsersToLoad
这个链表中
完成加载后,会调用ACLLoadUsersAtStartup
完成用户信息的初始化
1void ACLLoadUsersAtStartup(void) {
2 //先判断一下,如果配置了ACL文件的同时,又在redis.conf 中配置了用户,那么redis启动会失败
3 //ACL的配置只能在一个里面配置
4 if (server.acl_filename[0] != '\0' && listLength(UsersToLoad) != 0) {
5 serverLog(LL_WARNING,
6 "Configuring Redis with users defined in redis.conf and at "
7 "the same setting an ACL file path is invalid. This setup "
8 "is very likely to lead to configuration errors and security "
9 "holes, please define either an ACL file or declare users "
10 "directly in your redis.conf, but not both.");
11 exit(1);
12 }
13
14 //处理从redis配置文件中加载的用户
15 if (ACLLoadConfiguredUsers() == C_ERR) {
16 serverLog(LL_WARNING,
17 "Critical error while loading ACLs. Exiting.");
18 exit(1);
19 }
20
21 //处理从acl文件中加载的用户
22 if (server.acl_filename[0] != '\0') {
23 sds errors = ACLLoadFromFile(server.acl_filename);
24 if (errors) {
25 serverLog(LL_WARNING,
26 "Aborting Redis startup because of ACL errors: %s", errors);
27 sdsfree(errors);
28 exit(1);
29 }
30 }
31}
从配置文件中加载的逻辑,需要完成用户加载和用户信息的初始化(用户的密码,flag,selectors规则等)
1int ACLLoadConfiguredUsers(void) {
2 listIter li;
3 listNode *ln;
4 listRewind(UsersToLoad,&li);
5 //遍历UsersToLoad这个链表 完成信息初始化
6 while ((ln = listNext(&li)) != NULL) {
7 sds *aclrules = listNodeValue(ln);
8 sds username = aclrules[0];
9
10 if (ACLStringHasSpaces(aclrules[0],sdslen(aclrules[0]))) {
11 serverLog(LL_WARNING,"Spaces not allowed in ACL usernames");
12 return C_ERR;
13 }
14
15 user *u = ACLCreateUser(username,sdslen(username));
16 //做用户去重,一个用户只允许有一个
17 if (!u) {
18 /* Only valid duplicate user is the default one. */
19 serverAssert(!strcmp(username, "default"));
20 u = ACLGetUserByName("default",7);
21 ACLSetUser(u,"reset",-1);
22 }
23
24 /* Load every rule defined for this user. */
25
26 //遍历得到的规则,把这些规则赋给user,完成user信息的初始化
27 for (int j = 1; aclrules[j]; j++) {
28 if (ACLSetUser(u,aclrules[j],sdslen(aclrules[j])) != C_OK) {
29 const char *errmsg = ACLSetUserStringError();
30 serverLog(LL_WARNING,"Error loading ACL rule '%s' for "
31 "the user named '%s': %s",
32 aclrules[j],aclrules[0],errmsg);
33 return C_ERR;
34 }
35 }
36
37 /* Having a disabled user in the configuration may be an error,
38 * warn about it without returning any error to the caller. */
39 //完成信息初始后,判断一下这个用户是不是还是不可用的
40 if (u->flags & USER_FLAG_DISABLED) {
41 serverLog(LL_NOTICE, "The user '%s' is disabled (there is no "
42 "'on' modifier in the user description). Make "
43 "sure this is not a configuration error.",
44 aclrules[0]);
45 }
46 }
47 return C_OK;
48}
从ACL文件中加载user代码比较多,下面会把一些不重要的(比如文件加载等等)删掉
1sds ACLLoadFromFile(const char *filename) {
2 //这里会先把全局的 Users 保存到 old_users指针,
3 //然后重新 初始化一个 User,后面加载如果出错了,可以回滚
4 rax *old_users = Users;
5 Users = raxNew();
6
7 //遍历文件中所有的行,开始加载user
8 for (int i = 0; i < totlines; i++)
9 ............ 省略读文件部分...............
10 //创建一个User
11 user *u = ACLCreateUser(argv[1],sdslen(argv[1]));
12
13 //如果用户已经存在了,跳过
14 if (!u) {
15 errors = sdscatprintf(errors,"WARNING: Duplicate user '%s' found on line %d. ", argv[1], linenum);
16 sdsfreesplitres(argv,argc);
17 continue;
18 }
19
20 /* Finally process the options and validate they can
21 * be cleanly applied to the user. If any option fails
22 * to apply, the other values won't be applied since
23 * all the pending changes will get dropped. */
24 int merged_argc;
25
26 //这个函数的作用是把读到的字符串,提取成具体的user属性的数组
27 sds *acl_args = ACLMergeSelectorArguments(argv + 2, argc - 2, &merged_argc, NULL);
28 if (!acl_args) {
29 errors = sdscatprintf(errors,
30 "%s:%d: Unmatched parenthesis in selector definition.",
31 server.acl_filename, linenum);
32 }
33
34 int syntax_error = 0;
35 for (int j = 0; j < merged_argc; j++) {
36 acl_args[j] = sdstrim(acl_args[j],"\t\r\n");
37 //遍历上面得到的手势,给创建的user设置属性
38 if (ACLSetUser(u,acl_args[j],sdslen(acl_args[j])) != C_OK) {
39 const char *errmsg = ACLSetUserStringError();
40 if (errno == ENOENT) {
41 /* For missing commands, we print out more information since
42 * it shouldn't contain any sensitive information. */
43 errors = sdscatprintf(errors,
44 "%s:%d: Error in applying operation '%s': %s. ",
45 server.acl_filename, linenum, acl_args[j], errmsg);
46 } else if (syntax_error == 0) {
47 /* For all other errors, only print out the first error encountered
48 * since it might affect future operations. */
49 errors = sdscatprintf(errors,
50 "%s:%d: %s. ",
51 server.acl_filename, linenum, errmsg);
52 syntax_error = 1;
53 }
54 }
55 }
56
57 for (int i = 0; i < merged_argc; i++) sdsfree(acl_args[i]);
58 zfree(acl_args);
59
60 /* Apply the rule to the new users set only if so far there
61 * are no errors, otherwise it's useless since we are going
62 * to discard the new users set anyway. */
63 if (sdslen(errors) != 0) {
64 sdsfreesplitres(argv,argc);
65 continue;
66 }
67
68 sdsfreesplitres(argv,argc);
69 }
70
71 sdsfreesplitres(lines,totlines);
72
73 /* Check if we found errors and react accordingly. */
74 if (sdslen(errors) == 0) {
75 //判断一下,是否加载了新的`default` user,如果有的话把旧的default user释放掉,用新加载的
76 user *new_default = ACLGetUserByName("default",7);
77 if (!new_default) {
78 new_default = ACLCreateDefaultUser();
79 }
80
81 ACLCopyUser(DefaultUser,new_default);
82 ACLFreeUser(new_default);
83 raxInsert(Users,(unsigned char*)"default",7,DefaultUser,NULL);
84 raxRemove(old_users,(unsigned char*)"default",7,NULL);
85 ACLFreeUsersSet(old_users);
86 sdsfree(errors);
87 return NULL;
88 } else {
89 //如果出错了,数据回滚
90 ACLFreeUsersSet(Users);
91 Users = old_users;
92 errors = sdscat(errors,"WARNING: ACL errors detected, no change to the previously active ACL rules was performed");
93 return errors;
94 }
95}
不管是从 配置文件中加载用户还是从ACL文件中加载用户,最终都会调用ACLSetUser
这个函数来完成 user
属性的初始化。使用redis的 ACL
命令来管理用户时也是通过这个函数来处理的。
1int ACLSetUser(user *u, const char *op, ssize_t oplen) {
2 //第一步,先把以前的属性字符串删掉
3 if (u->acl_string) {
4 decrRefCount(u->acl_string);
5 u->acl_string = NULL;
6 }
7
8 if (oplen == -1) oplen = strlen(op);
9 if (oplen == 0) return C_OK; /* Empty string is a no-operation. */
10 if (!strcasecmp(op,"on")) {
11 //设置 user 是 开启的
12 u->flags |= USER_FLAG_ENABLED;
13 u->flags &= ~USER_FLAG_DISABLED;
14 } else if (!strcasecmp(op,"off")) {
15 //设置 user 是关闭的
16 u->flags |= USER_FLAG_DISABLED;
17 u->flags &= ~USER_FLAG_ENABLED;
18 } else if (!strcasecmp(op,"skip-sanitize-payload")) {
19 u->flags |= USER_FLAG_SANITIZE_PAYLOAD_SKIP;
20 u->flags &= ~USER_FLAG_SANITIZE_PAYLOAD;
21 } else if (!strcasecmp(op,"sanitize-payload")) {
22 u->flags &= ~USER_FLAG_SANITIZE_PAYLOAD_SKIP;
23 u->flags |= USER_FLAG_SANITIZE_PAYLOAD;
24 } else if (!strcasecmp(op,"nopass")) {
25 u->flags |= USER_FLAG_NOPASS;
26 listEmpty(u->passwords);
27 } else if (!strcasecmp(op,"resetpass")) {
28 u->flags &= ~USER_FLAG_NOPASS;
29 listEmpty(u->passwords);
30 } else if (op[0] == '>' || op[0] == '#') {
31 //设置密码
32 sds newpass;
33 if (op[0] == '>') {
34 //如果是明文,先转成sha256字符串
35 newpass = ACLHashPassword((unsigned char*)op+1,oplen-1);
36 } else {
37 //判断一下输入的值是不是有效的hash值
38 if (ACLCheckPasswordHash((unsigned char*)op+1,oplen-1) == C_ERR) {
39 errno = EBADMSG;
40 return C_ERR;
41 }
42 newpass = sdsnewlen(op+1,oplen-1);
43 }
44
45 listNode *ln = listSearchKey(u->passwords,newpass);
46 //如果之前有这个密码,不用添加了,避免同一个密码添加多次
47 if (ln == NULL)
48 listAddNodeTail(u->passwords,newpass);
49 else
50 sdsfree(newpass);
51 u->flags &= ~USER_FLAG_NOPASS;
52 } else if (op[0] == '<' || op[0] == '!') {
53 //删除密码,同样的,如果是明文,先转成sha256字符串
54 sds delpass;
55 if (op[0] == '<') {
56 delpass = ACLHashPassword((unsigned char*)op+1,oplen-1);
57 } else {
58 //同样,判断这个字符串是不是有效的hash值
59 if (ACLCheckPasswordHash((unsigned char*)op+1,oplen-1) == C_ERR) {
60 errno = EBADMSG;
61 return C_ERR;
62 }
63 delpass = sdsnewlen(op+1,oplen-1);
64 }
65 listNode *ln = listSearchKey(u->passwords,delpass);
66 sdsfree(delpass);
67 if (ln) {
68 listDelNode(u->passwords,ln);
69 } else {
70 errno = ENODEV;
71 return C_ERR;
72 }
73 } else if (op[0] == '(' && op[oplen - 1] == ')') {
74 //处理 `()` 内的 selector
75 aclSelector *selector = aclCreateSelectorFromOpSet(op, oplen);
76 if (!selector) {
77 /* No errorno set, propagate it from interior error. */
78 return C_ERR;
79 }
80 listAddNodeTail(u->selectors, selector);
81 return C_OK;
82 } else if (!strcasecmp(op,"clearselectors")) {
83 //清理掉所有的 selector
84 listIter li;
85 listNode *ln;
86 listRewind(u->selectors,&li);
87 /* There has to be a root selector */
88 serverAssert(listNext(&li));
89 while((ln = listNext(&li))) {
90 listDelNode(u->selectors, ln);
91 }
92 return C_OK;
93 } else if (!strcasecmp(op,"reset")) {
94 //重置这个用户
95 serverAssert(ACLSetUser(u,"resetpass",-1) == C_OK);
96 serverAssert(ACLSetUser(u,"resetkeys",-1) == C_OK);
97 serverAssert(ACLSetUser(u,"resetchannels",-1) == C_OK);
98 if (server.acl_pubsub_default & SELECTOR_FLAG_ALLCHANNELS)
99 serverAssert(ACLSetUser(u,"allchannels",-1) == C_OK);
100 serverAssert(ACLSetUser(u,"off",-1) == C_OK);
101 serverAssert(ACLSetUser(u,"sanitize-payload",-1) == C_OK);
102 serverAssert(ACLSetUser(u,"clearselectors",-1) == C_OK);
103 serverAssert(ACLSetUser(u,"-@all",-1) == C_OK);
104 } else {
105 //处理用户的 selector 设置, 比如 `+@, -@, &, ~*` 等等命令
106 aclSelector *selector = ACLUserGetRootSelector(u);
107 if (ACLSetSelector(selector, op, oplen) == C_ERR) {
108 return C_ERR;
109 }
110 }
111 return C_OK;
112}
aclCreateSelectorFromOpSet
这个函数最终会调用 ACLSetSelector
来把字符串转换成具体的规则
1int ACLSetSelector(aclSelector *selector, const char* op, size_t oplen) {
2 if (!strcasecmp(op,"allkeys") || !strcasecmp(op,"~*")) {
3 //允许所有key
4 selector->flags |= SELECTOR_FLAG_ALLKEYS;
5 listEmpty(selector->patterns);
6 } else if (!strcasecmp(op,"resetkeys")) {
7 //去掉所有key权限
8 selector->flags &= ~SELECTOR_FLAG_ALLKEYS;
9 listEmpty(selector->patterns);
10 } else if (!strcasecmp(op,"allchannels") || !strcasecmp(op,"&*")) {
11 //channel 和key一样,允许所有和去掉所有
12 selector->flags |= SELECTOR_FLAG_ALLCHANNELS;
13 listEmpty(selector->channels);
14 } else if (!strcasecmp(op,"resetchannels")) {
15 selector->flags &= ~SELECTOR_FLAG_ALLCHANNELS;
16 listEmpty(selector->channels);
17 } else if (!strcasecmp(op,"allcommands") || !strcasecmp(op,"+@all")) {
18 //允许所有的权限, 下面是去掉所有权限
19 memset(selector->allowed_commands,255,sizeof(selector->allowed_commands));
20 selector->flags |= SELECTOR_FLAG_ALLCOMMANDS;
21 ACLResetFirstArgs(selector);
22 } else if (!strcasecmp(op,"nocommands") || !strcasecmp(op,"-@all")) {
23 memset(selector->allowed_commands,0,sizeof(selector->allowed_commands));
24 selector->flags &= ~SELECTOR_FLAG_ALLCOMMANDS;
25 ACLResetFirstArgs(selector);
26 } else if (op[0] == '~' || op[0] == '%') {
27 if (selector->flags & SELECTOR_FLAG_ALLKEYS) {
28 //如果是允许所有key了, 再加限制key的命令,会返回错误
29 errno = EEXIST;
30 return C_ERR;
31 }
32 int flags = 0;
33 size_t offset = 1;
34 if (op[0] == '%') {
35 //如果是%开头,需要判断是读还是写
36 for (; offset < oplen; offset++) {
37 if (toupper(op[offset]) == 'R' && !(flags & ACL_READ_PERMISSION)) {
38 flags |= ACL_READ_PERMISSION;
39 } else if (toupper(op[offset]) == 'W' && !(flags & ACL_WRITE_PERMISSION)) {
40 flags |= ACL_WRITE_PERMISSION;
41 } else if (op[offset] == '~') {
42 offset++;
43 break;
44 } else {
45 errno = EINVAL;
46 return C_ERR;
47 }
48 }
49 } else {
50 flags = ACL_ALL_PERMISSION;
51 }
52
53 if (ACLStringHasSpaces(op+offset,oplen-offset)) {
54 errno = EINVAL;
55 return C_ERR;
56 }
57 keyPattern *newpat = ACLKeyPatternCreate(sdsnewlen(op+offset,oplen-offset), flags);
58 listNode *ln = listSearchKey(selector->patterns,newpat);
59 //避免重复
60 if (ln == NULL) {
61 listAddNodeTail(selector->patterns,newpat);
62 } else {
63 //如果有重复的. 合并两个pattern的flag
64 ((keyPattern *)listNodeValue(ln))->flags |= flags;
65 ACLKeyPatternFree(newpat);
66 }
67 selector->flags &= ~SELECTOR_FLAG_ALLKEYS;
68 } else if (op[0] == '&') {
69 if (selector->flags & SELECTOR_FLAG_ALLCHANNELS) {
70 //同样,如果已经允许所有channel了,再去限制channel,也会报错
71 errno = EISDIR;
72 return C_ERR;
73 }
74 if (ACLStringHasSpaces(op+1,oplen-1)) {
75 errno = EINVAL;
76 return C_ERR;
77 }
78 sds newpat = sdsnewlen(op+1,oplen-1);
79 listNode *ln = listSearchKey(selector->channels,newpat);
80 //同样,也会对channel pattern 去重
81 if (ln == NULL)
82 listAddNodeTail(selector->channels,newpat);
83 else
84 sdsfree(newpat);
85 selector->flags &= ~SELECTOR_FLAG_ALLCHANNELS;
86 } else if (op[0] == '+' && op[1] != '@') {
87 //加命令权限 (没有在 ACL CAT分类里的)
88 //如果一个命令没有在分类里,可以通过前缀来做匹配,比如
89 //DB提供执行SELECT的能力,但可能仍然希望能够运行SELECT 0。
90 //ACL SETUSER myuser -select +select|0
91 if (strrchr(op,'|') == NULL) {
92 //如果op字符串中没有 `|` 不需要处理前缀, 直接处理即可
93 //需要先处理一下重命名的命令
94 struct redisCommand *cmd = ACLLookupCommand(op+1);
95 if (cmd == NULL) {
96 errno = ENOENT;
97 return C_ERR;
98 }
99 //处理禁用和允许的命令,包括子命令
100 ACLChangeSelectorPerm(selector,cmd,1);
101 } else {
102 //分割字符串,得到子命令
103 char *copy = zstrdup(op+1);
104 char *sub = strrchr(copy,'|');
105 sub[0] = '\0';
106 sub++;
107
108 struct redisCommand *cmd = ACLLookupCommand(copy);
109
110 /* Check if the command exists. We can't check the
111 * first-arg to see if it is valid. */
112 if (cmd == NULL) {
113 zfree(copy);
114 errno = ENOENT;
115 return C_ERR;
116 }
117
118 //不能直接作用于子命令
119 if (cmd->parent) {
120 zfree(copy);
121 errno = ECHILD;
122 return C_ERR;
123 }
124
125 //子命令不能是空的,比如 `HASH|` 这种是不合法的
126 if (strlen(sub) == 0) {
127 zfree(copy);
128 errno = EINVAL;
129 return C_ERR;
130 }
131
132 if (cmd->subcommands_dict) {
133 //这个命令有子命令,处理子命令
134 cmd = ACLLookupCommand(op+1);
135 if (cmd == NULL) {
136 zfree(copy);
137 errno = ENOENT;
138 return C_ERR;
139 }
140 ACLChangeSelectorPerm(selector,cmd,1);
141 } else {
142 /* If user is trying to use the ACL mech to block SELECT except SELECT 0 or
143 * block DEBUG except DEBUG OBJECT (DEBUG subcommands are not considered
144 * subcommands for now) we use the allowed_firstargs mechanism. */
145
146 /* Add the first-arg to the list of valid ones. */
147 serverLog(LL_WARNING, "Deprecation warning: Allowing a first arg of an otherwise "
148 "blocked command is a misuse of ACL and may get disabled "
149 "in the future (offender: +%s)", op+1);
150 ACLAddAllowedFirstArg(selector,cmd->id,sub);
151 }
152
153 zfree(copy);
154 }
155 } else if (op[0] == '-' && op[1] != '@') {
156 //去掉 命令权限 (没有在 ACL CAT分类里的)
157 struct redisCommand *cmd = ACLLookupCommand(op+1);
158 if (cmd == NULL) {
159 errno = ENOENT;
160 return C_ERR;
161 }
162 ACLChangeSelectorPerm(selector,cmd,0);
163 } else if ((op[0] == '+' || op[0] == '-') && op[1] == '@') {
164 //加或者减权限 (只处理在ACL CAT分类里的)
165 int bitval = op[0] == '+' ? 1 : 0;
166 if (ACLSetSelectorCommandBitsForCategory(selector,op+2,bitval) == C_ERR) {
167 errno = ENOENT;
168 return C_ERR;
169 }
170 } else {
171 errno = EINVAL;
172 return C_ERR;
173 }
174 return C_OK;
175}
权限设置相关的差不多就是这些了,基本都是得到字符串后,处理字符串,然后处理对应的权限。代码量不小,其中一些细节没有详细展开分析,逻辑都不太复杂,后面有时间会考虑把这里的坑填上。
ACL规则存储
使用 ACL SETUSER
命令设置的规则,默认只会存在内存中,如果没有调用ACL SAVE
命令进行存储,redis重启后会丢失所有 用户信息
使用ACL SAVE
命令的前提是 redis 配置了 ACL 文件,如果没有配置 ACL 文件,那么 使用 ACL SAVE
命令会报错
使用 ACL SAVE
和 ACL LIST
最终都会调用这个函数来生成 ACL 规则的字符串
1robj *ACLDescribeUser(user *u) {
2 if (u->acl_string) {
3 incrRefCount(u->acl_string);
4 return u->acl_string;
5 }
6
7 sds res = sdsempty();
8
9 先处理flag
10 for (int j = 0; ACLUserFlags[j].flag; j++) {
11 if (u->flags & ACLUserFlags[j].flag) {
12 res = sdscat(res,ACLUserFlags[j].name);
13 res = sdscatlen(res," ",1);
14 }
15 }
16
17 //处理密码
18 listIter li;
19 listNode *ln;
20 listRewind(u->passwords,&li);
21 while((ln = listNext(&li))) {
22 sds thispass = listNodeValue(ln);
23 res = sdscatlen(res,"#",1);
24 res = sdscatsds(res,thispass);
25 res = sdscatlen(res," ",1);
26 }
27
28 //生成 selector的规则字符串
29 listRewind(u->selectors,&li);
30 while((ln = listNext(&li))) {
31 aclSelector *selector = (aclSelector *) listNodeValue(ln);
32 //通过 ACLDescribeSelector 生成 selector的规则字符串
33 //这里面的处理如果详细来拆分的话,还是有点多的,就不展开了
34 sds default_perm = ACLDescribeSelector(selector);
35 //这里需要注意一下, 如果是非root selector, 规则需要用 () 包裹
36 if (selector->flags & SELECTOR_FLAG_ROOT) {
37 res = sdscatfmt(res, "%s", default_perm);
38 } else {
39 res = sdscatfmt(res, " (%s)", default_perm);
40 }
41 sdsfree(default_perm);
42 }
43
44 u->acl_string = createObject(OBJ_STRING, res);
45 /* because we are returning it, have to increase count */
46 incrRefCount(u->acl_string);
47
48 return u->acl_string;
49}
ACL鉴权
下面开始分析 鉴权相关的代码。
上面的基本是和权限设置相关的,有了权限后,要执行命令时,需要判断是否可以执行,这部分就是鉴权了。
先来最基本的 AUTH
命令
AUTH
命令兼容以前的命令参数,会根据参数的个数判断命名的版本,如果参数长度是2,会使用以前的模式来处理。
如果参数长度是3,会按照ACL的模式来处理
1void authCommand(client *c) {
2 //如果参数>3了这个命令是错误的,直接返回
3 if (c->argc > 3) {
4 addReplyErrorObject(c,shared.syntaxerr);
5 return;
6 }
7 redactClientCommandArgument(c, 1);
8
9 /* Handle the two different forms here. The form with two arguments
10 * will just use "default" as username. */
11 robj *username, *password;
12 if (c->argc == 2) {
13 //处理两种参数的情况,如果默认用户是没有密码的, 返回错谁
14 if (DefaultUser->flags & USER_FLAG_NOPASS) {
15 addReplyError(c,"AUTH <password> called without any password "
16 "configured for the default user. Are you sure "
17 "your configuration is correct?");
18 return;
19 }
20
21 username = shared.default_username;
22 password = c->argv[1];
23 } else {
24 //处理三种参数的情况
25 username = c->argv[1];
26 password = c->argv[2];
27 redactClientCommandArgument(c, 2);
28 }
29 //校验用户名和密码是否匹配
30 if (ACLAuthenticateUser(c,username,password) == C_OK) {
31 addReply(c,shared.ok);
32 } else {
33 addReplyError(c,"-WRONGPASS invalid username-password pair or user is disabled.");
34 }
35}
AUTH
命令处理还是挺简单的,下面开始ACL对具体命令的判断
当一个命令进来后,redis要先判断命令是否能执行,从 server.c:3769 开始,调用ACLCheckAllPerm
函数。
最终会调用到 ACLCheckAllUserCommandPerm 函数去做具体的校验逻辑
会遍历user
的 Selectors
这个链表来做鉴权
1 listRewind(u->selectors,&li);
2 while((ln = listNext(&li))) {
3 aclSelector *s = (aclSelector *) listNodeValue(ln);
4 int acl_retval = ACLSelectorCheckCmd(s, cmd, argv, argc, &local_idxptr, &cache);
5 if (acl_retval == ACL_OK) {
6 //如果通过校验了,可以结束循环,然后返回正确
7 cleanupACLKeyResultCache(&cache);
8 return ACL_OK;
9 }
10 if (acl_retval > relevant_error ||
11 (acl_retval == relevant_error && local_idxptr > last_idx))
12 {
13 relevant_error = acl_retval;
14 last_idx = local_idxptr;
15 }
16 }
做鉴权判断的是这个函数ACLSelectorCheckCmd
鉴权的优先级
- 判断命令类型,比如
GET
,SET
,HGET
等等,判断是否有这个类型命令的权限 - 判断key名,通过对比key的名字,判断这个key是否有权限
- 判断是否是channel命令,然后判断权限
1static int ACLSelectorCheckCmd(aclSelector *selector, struct redisCommand *cmd, robj **argv, int argc, int *keyidxptr, aclKeyResultCache *cache) {
2 uint64_t id = cmd->id;
3 int ret;
4 //如果 flags 不是全部 ALLCOMMANDS 并且 这个命令不是不需要验证的命令(这个命令需要auth后才能使用)
5 //CMD_NO_AUTH命令包括 `auth`,`hello`,`quit`,`reset`
6 if (!(selector->flags & SELECTOR_FLAG_ALLCOMMANDS) && !(cmd->flags & CMD_NO_AUTH)) {
7 //如果命令bit里没有找到,需要再去匹配一下前缀,防止漏判
8 if (ACLGetSelectorCommandBit(selector,id) == 0) {
9 /* Check if the first argument matches. */
10 if (argc < 2 ||
11 selector->allowed_firstargs == NULL ||
12 selector->allowed_firstargs[id] == NULL)
13 {
14 return ACL_DENIED_CMD;
15 }
16
17 long subid = 0;
18 //遍历,比较前缀
19 while (1) {
20 if (selector->allowed_firstargs[id][subid] == NULL)
21 return ACL_DENIED_CMD;
22 int idx = cmd->parent ? 2 : 1;
23 if (!strcasecmp(argv[idx]->ptr,selector->allowed_firstargs[id][subid]))
24 break; /* First argument match found. Stop here. */
25 subid++;
26 }
27 }
28 }
29
30 //如果不是允许所有的key 并且这个命令里 包含了key
31 //排除类似 select 0 等等没有key操作的命令
32 if (!(selector->flags & SELECTOR_FLAG_ALLKEYS) && doesCommandHaveKeys(cmd)) {
33 //这里有一个从函数外传进来的,用来缓存key 的结构, 如果没有初始化过,需要初始化一下
34 if (!(cache->keys_init)) {
35 cache->keys = (getKeysResult) GETKEYS_RESULT_INIT;
36 //获取到这条命令中所有的key
37 getKeysFromCommandWithSpecs(cmd, argv, argc, GET_KEYSPEC_DEFAULT, &(cache->keys));
38 cache->keys_init = 1;
39 }
40 getKeysResult *result = &(cache->keys);
41 keyReference *resultidx = result->keys;
42 for (int j = 0; j < result->numkeys; j++) {
43 int idx = resultidx[j].pos;
44 //通过遍历所有的key,判断是否符合规则
45 ret = ACLSelectorCheckKey(selector, argv[idx]->ptr, sdslen(argv[idx]->ptr), resultidx[j].flags);
46 //遇到一个不符合的,直接就可以退出,并返回错误
47 if (ret != ACL_OK) {
48 if (keyidxptr) *keyidxptr = resultidx[j].pos;
49 return ret;
50 }
51 }
52 }
53
54 //检查channel的权限,只需要检查 pub 和 sub这两个权限即可
55 const int channel_flags = CMD_CHANNEL_PUBLISH | CMD_CHANNEL_SUBSCRIBE;
56 if (!(selector->flags & SELECTOR_FLAG_ALLCHANNELS) && doesCommandHaveChannelsWithFlags(cmd, channel_flags)) {
57 getKeysResult channels = (getKeysResult) GETKEYS_RESULT_INIT;
58 getChannelsFromCommand(cmd, argv, argc, &channels);
59 keyReference *channelref = channels.keys;
60 for (int j = 0; j < channels.numkeys; j++) {
61 int idx = channelref[j].pos;
62 if (!(channelref[j].flags & channel_flags)) continue;
63 int is_pattern = channelref[j].flags & CMD_CHANNEL_PATTERN;
64 //和匹配key类似,也是遍历匹配,遇到不符合的就结束并返回错误
65 int ret = ACLCheckChannelAgainstList(selector->channels, argv[idx]->ptr, sdslen(argv[idx]->ptr), is_pattern);
66 if (ret != ACL_OK) {
67 if (keyidxptr) *keyidxptr = channelref[j].pos;
68 getKeysFreeResult(&channels);
69 return ret;
70 }
71 }
72 getKeysFreeResult(&channels);
73 }
74 return ACL_OK;
75}
ACL鉴权的基本就是这些了
ACL相关的一些知识
1//命令分类的定义
2struct ACLCategoryItem {
3 const char *name;
4 uint64_t flag;
5} ACLCommandCategories[] = { /* See redis.conf for details on each category. */
6 {"keyspace", ACL_CATEGORY_KEYSPACE},
7 {"read", ACL_CATEGORY_READ},
8 {"write", ACL_CATEGORY_WRITE},
9 {"set", ACL_CATEGORY_SET},
10 {"sortedset", ACL_CATEGORY_SORTEDSET},
11 {"list", ACL_CATEGORY_LIST},
12 {"hash", ACL_CATEGORY_HASH},
13 {"string", ACL_CATEGORY_STRING},
14 {"bitmap", ACL_CATEGORY_BITMAP},
15 {"hyperloglog", ACL_CATEGORY_HYPERLOGLOG},
16 {"geo", ACL_CATEGORY_GEO},
17 {"stream", ACL_CATEGORY_STREAM},
18 {"pubsub", ACL_CATEGORY_PUBSUB},
19 {"admin", ACL_CATEGORY_ADMIN},
20 {"fast", ACL_CATEGORY_FAST},
21 {"slow", ACL_CATEGORY_SLOW},
22 {"blocking", ACL_CATEGORY_BLOCKING},
23 {"dangerous", ACL_CATEGORY_DANGEROUS},
24 {"connection", ACL_CATEGORY_CONNECTION},
25 {"transaction", ACL_CATEGORY_TRANSACTION},
26 {"scripting", ACL_CATEGORY_SCRIPTING},
27 {NULL,0} /* Terminator. */
28};
29
30//userflag的定义
31struct ACLUserFlag {
32 const char *name;
33 uint64_t flag;
34} ACLUserFlags[] = {
35 /* Note: the order here dictates the emitted order at ACLDescribeUser */
36 {"on", USER_FLAG_ENABLED},
37 {"off", USER_FLAG_DISABLED},
38 {"nopass", USER_FLAG_NOPASS},
39 {"skip-sanitize-payload", USER_FLAG_SANITIZE_PAYLOAD_SKIP},
40 {"sanitize-payload", USER_FLAG_SANITIZE_PAYLOAD},
41 {NULL,0} /* Terminator. */
42};
还有一个在权限校验中用到的东西 cmd->id
这个是每个redis命令的id,具体是通过这个函数得到的
1//获取一个命令的id
2unsigned long ACLGetCommandID(sds cmdname) {
3 sds lowername = sdsdup(cmdname);
4 sdstolower(lowername);
5 if (commandId == NULL) commandId = raxNew();
6 void *id = raxFind(commandId,(unsigned char*)lowername,sdslen(lowername));
7 if (id != raxNotFound) {
8 sdsfree(lowername);
9 //如果找到了,直接return id就可以了
10 return (unsigned long)id;
11 }
12
13 //如果没有找到, 把这个命令插入的rax表中,并得到id
14 raxInsert(commandId,(unsigned char*)lowername,strlen(lowername),
15 (void*)nextid,NULL);
16 sdsfree(lowername);
17 unsigned long thisid = nextid;
18 //id自增, nextid是在acl中定义的
19 nextid++;
20
21 //如果id==1023了, 再自增一次,防止id是1024
22 //因为这个值要给aclSelector中的allowed_commands做索引,
23 //allowed_commands 是一个长度为16的数组,至于为什么不能是1024这个值,后面会看到
24 if (nextid == USER_COMMAND_BITS_COUNT-1) nextid++;
25 return thisid;
26}
27
28//nextid的定义
29static unsigned long nextid = 0; /* Next command id that has not been assigned */
30
31//aclSelector 中的 allowed_commands定义
32uint64_t allowed_commands[USER_COMMAND_BITS_COUNT/64];
根据 cmd.id
设置 selector
1void ACLSetSelectorCommandBit(aclSelector *selector, unsigned long id, int value) {
2 uint64_t word, bit;
3 //调用 ACLGetCommandBitCoordinates 计算得到 word 和bit
4 if (ACLGetCommandBitCoordinates(id,&word,&bit) == C_ERR) return;
5 if (value) {
6 //如果是允许的话,设置这个bit
7 selector->allowed_commands[word] |= bit;
8 } else {
9 //如果是不予许, 取消这个位, 再把flag设置一下
10 selector->allowed_commands[word] &= ~bit;
11 selector->flags &= ~SELECTOR_FLAG_ALLCOMMANDS;
12 }
13}
14
15int ACLGetCommandBitCoordinates(uint64_t id, uint64_t *word, uint64_t *bit) {
16 //如果id >= 1024了,return 错误
17 if (id >= USER_COMMAND_BITS_COUNT) return C_ERR;
18 //sizeof(uint64_t) 计算得到8
19 //word 得到但是 数组的索引
20 //到这应该就明白为什么 在 `ACLGetCommandID` id 不能是1024了吧
21 *word = id / sizeof(uint64_t) / 8;
22 //bit 是通过位运算得到的, 是id%64 得到的一个[0,63]的值,然后再左移得到 bit的值
23 *bit = 1ULL << (id % (sizeof(uint64_t) * 8));
24 return C_OK;
25}
这样一番操作下来,selector.allowed_commands
得到一个长度是64的 uint64_t
的数组,数组内的每一个整数的每一个位,记录了一个命令开启或者关闭(对应位的0或1)
这里有个非常巧妙的计算过程。
一般我们在使用位运算记录的时候,都是1,2,4,8 等等这些2的倍数来做,因为要保证不会出现位冲突。比如2的二进制是 10
, 3的二进制是 11
在同时使用 10
和 11
进行 与或运算的时候,2 和 3 第一位的 1
会相互影响,使结果不准确。
但是如果直接用id的值左移来计算,uint64_t bit = 1ULL << id
假设有100个cmd,id的值就是100,那就需要一个整数是100位的长度。但是现在C语言的最大整数是64位长度。显然是不够用的。所以这里在计算的时候,会把命令先分组,让命令落在[0,63]这个范围内,然后再对组内的数字进行左移计算。这样,使用长度是64的 uint64_t
的数组就能完成 1024个命令的bit记录了。
命令初始化
在ACL中,有 命令分类 (acl_categories)
和 子命令(subcommands)
这两个概念,这个东西需要在命令初始化时就赋值。
acl_categories
是一个 uint64_t
类型的,对应的值是 ACL_CATEGORY_
的宏定义,在 这里 被定义
subcommands
是一个 redisCommand
类型的指针,其实就是 redisCommand
类型的数组
在这里,redis的所有命令都会被初始化
就拿 ACL
这个命令来说吧 ACL命令初始化
ACL命令的 acl_categories
被设置为了0
, subcommands
被设置为了 ACL_Subcommands
其中 ACL_Subcommands
是一个命令的数组,在这里被初始化
有子命令的会把对应的子命令赋值对 subcommands
中,没有子命令的就直接是空指针了
暂时能想到的就是这些了,后面有新的再补充