0%

redis源码学习|ACL

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 简单的语法

看一下当前用户

1
2
127.0.0.1:6379> acl list
1) "user default on nopass ~* &* +@all"
  • default 用户名
  • on 表示用户是激活的,如果是 off那么这个用户无法通过 AUTH 命令
  • nopass 表示这个用户没有密码
  • ~* 表示可以访问的Key(正则匹配)
  • &* 6.2 加入的,表示channel的权限
  • +@ 表示用户的权限,“+”表示授权权限,有权限操作或访问,“-”表示还是没有权限; @为权限分类,可以通过 ACL CAT 查询支持的分类。+@all 表示所有权限,nocommands 表示不给与任何命令的操作权限

用户权限分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"keyspace"
"read"
"write"
"set"
"sortedset"
"list"
"hash"
"string"
"bitmap"
"hyperloglog"
"geo"
"stream"
"pubsub"
"admin"
"fast"
"slow"
"blocking"
"dangerous"
"connection"
"transaction"
"scripting"

至于怎么操作,不是这次的重点,直接去看官方文档就好了 redis acl

ACL源码

开始上源码了,本次源码基于redis的 7.0.11版本,不同的版本之间可能有差异。

ACL相关的数据结构

ACL鉴权对应的是一个User,先看一下User和相关的selector的定义

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
typedef struct {
//用户名
sds name; /* The username as an SDS string. */

//用户的flag,用来做各种比对
uint32_t flags; /* See USER_FLAG_* */

//这个用户的密码,一个用户可以有多个密码,所以是用链表保存的
//这个链表的node是一个明文密码经过 `SHA256` 计算过后的 字符串
list *passwords; /* A list of SDS valid passwords for this user. */

//验证用户权限的选择器,该用户的权限保存在这个链表中
list *selectors; /* A list of selectors this user validates commands
against. This list will always contain at least
one selector for backwards compatibility. */
//缓存ACL命令的字符串
robj *acl_string; /* cached string represent of ACLs */
} user;


//user中 `selectors` 链表中的结构
typedef struct {
//这个 selectors 的flag
uint32_t flags; /* See SELECTOR_FLAG_* */

/* The bit in allowed_commands is set if this user has the right to
* execute this command.
* If the bit for a given command is NOT set and the command has
* allowed first-args, Redis will also check allowed_firstargs in order to
* understand if the command can be executed. */

//使用 bit 记录允许的命令
uint64_t allowed_commands[USER_COMMAND_BITS_COUNT/64];

/* allowed_firstargs is used by ACL rules to block access to a command unless a
* specific argv[1] is given.
*
* For each command ID (corresponding to the command bit set in allowed_commands),
* This array points to an array of SDS strings, terminated by a NULL pointer,
* with all the first-args that are allowed for this command. When no first-arg
* matching is used, the field is just set to NULL to avoid allocating
* USER_COMMAND_BITS_COUNT pointers. */

sds **allowed_firstargs;

//redis key 匹配规则的字符串链表
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. */

//redis channels 规则的字符串 链表
list *channels; /* A list of allowed Pub/Sub channel patterns. If this
field is NULL the user cannot mention any channel in a
`PUBLISH` or [P][UNSUBSCRIBE] command, unless the flag
ALLCHANNELS is set in the user. */
} aclSelector;


/* Structure used for handling key patterns with different key
* based permissions. */
//`aclSelector` 中 patterns 链表中的结构
typedef struct {
int flags; /* The CMD_KEYS_* flags for this key pattern */
sds pattern; /* The pattern to match keys against */
} keyPattern;

user源码

aclSelector源码

aclSelector 结构先有个大概的了解即可,后面在权限校验的那部分会有详细的分析

ACL的初始化

ACL的初始化在ACLInit函数中完成

1
2
3
4
5
6
7
void ACLInit(void) {
Users = raxNew();
UsersToLoad = listCreate();
listSetMatchMethod(UsersToLoad, ACLListMatchLoadedUser);
ACLLog = listCreate();
DefaultUser = ACLCreateDefaultUser();
}

ACLInit

ACLInit这个函数在 main 函数中被调用,也就是在redis启动的时候,就会初始化ACL的数据结构

所有的用户都存在Users这个全局变量中,这个变量是rax类型的

UsersToLoad 记录了从配置文件中加载的用户

ACLLog ACL操作相关的log

DefaultUser 特殊的用户,default用户

会使用 ACLCreateDefaultUser 这个函数初始化default这个用户

1
2
3
4
5
6
7
8
9
user *ACLCreateDefaultUser(void) {
user *new = ACLCreateUser("default",7);
ACLSetUser(new,"+@all",-1);
ACLSetUser(new,"~*",-1);
ACLSetUser(new,"&*",-1);
ACLSetUser(new,"on",-1);
ACLSetUser(new,"nopass",-1);
return new;
}

ACLCreateDefaultUser

创建一个用户是在ACLCreateUser 这个函数中完成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
user *ACLCreateUser(const char *name, size_t namelen) {
//先判断一下这个鱼护是否存在,如果存在了直接return
if (raxFind(Users,(unsigned char*)name,namelen) != raxNotFound) return NULL;
user *u = zmalloc(sizeof(*u));
u->name = sdsnewlen(name,namelen);
//新创建的用户默认都是不能使用的
u->flags = USER_FLAG_DISABLED;
u->passwords = listCreate();
u->acl_string = NULL;
listSetMatchMethod(u->passwords,ACLListMatchSds);
listSetFreeMethod(u->passwords,ACLListFreeSds);
listSetDupMethod(u->passwords,ACLListDupSds);

u->selectors = listCreate();
listSetFreeMethod(u->selectors,ACLListFreeSelector);
listSetDupMethod(u->selectors,ACLListDuplicateSelector);

/* Add the initial root selector */
aclSelector *s = ACLCreateSelector(SELECTOR_FLAG_ROOT);
listAddNodeHead(u->selectors, s);

raxInsert(Users,(unsigned char*)name,namelen,u,NULL);
return u;
}

ACLCreateUser

到这,default 用户的初始化就完成了

后面会从配置文件中加载其他用户的数据,完成其他用户的初始化

main 函数中调用 ACLLoadUsersAtStartup 完成从配置文件中加载用户

在此之前会先使用ACLAppendUserForLoading 函数完成从 redis.conf 中加载用户。因为redis中的用户既可以配置在redis.conf中,也可以配置在一个单独的ACL文件中(两者只能选择其中一个,不能同时启用)。这个函数是从redis.conf中加载用户

最终把从配置文件中加载的用户都放到UsersToLoad 这个链表中

ACLAppendUserForLoading

完成加载后,会调用ACLLoadUsersAtStartup 完成用户信息的初始化

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
void ACLLoadUsersAtStartup(void) {
//先判断一下,如果配置了ACL文件的同时,又在redis.conf 中配置了用户,那么redis启动会失败
//ACL的配置只能在一个里面配置
if (server.acl_filename[0] != '\0' && listLength(UsersToLoad) != 0) {
serverLog(LL_WARNING,
"Configuring Redis with users defined in redis.conf and at "
"the same setting an ACL file path is invalid. This setup "
"is very likely to lead to configuration errors and security "
"holes, please define either an ACL file or declare users "
"directly in your redis.conf, but not both.");
exit(1);
}

//处理从redis配置文件中加载的用户
if (ACLLoadConfiguredUsers() == C_ERR) {
serverLog(LL_WARNING,
"Critical error while loading ACLs. Exiting.");
exit(1);
}

//处理从acl文件中加载的用户
if (server.acl_filename[0] != '\0') {
sds errors = ACLLoadFromFile(server.acl_filename);
if (errors) {
serverLog(LL_WARNING,
"Aborting Redis startup because of ACL errors: %s", errors);
sdsfree(errors);
exit(1);
}
}
}

ACLLoadUsersAtStartup

从配置文件中加载的逻辑,需要完成用户加载和用户信息的初始化(用户的密码,flag,selectors规则等)

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
int ACLLoadConfiguredUsers(void) {
listIter li;
listNode *ln;
listRewind(UsersToLoad,&li);
//遍历UsersToLoad这个链表 完成信息初始化
while ((ln = listNext(&li)) != NULL) {
sds *aclrules = listNodeValue(ln);
sds username = aclrules[0];

if (ACLStringHasSpaces(aclrules[0],sdslen(aclrules[0]))) {
serverLog(LL_WARNING,"Spaces not allowed in ACL usernames");
return C_ERR;
}

user *u = ACLCreateUser(username,sdslen(username));
//做用户去重,一个用户只允许有一个
if (!u) {
/* Only valid duplicate user is the default one. */
serverAssert(!strcmp(username, "default"));
u = ACLGetUserByName("default",7);
ACLSetUser(u,"reset",-1);
}

/* Load every rule defined for this user. */

//遍历得到的规则,把这些规则赋给user,完成user信息的初始化
for (int j = 1; aclrules[j]; j++) {
if (ACLSetUser(u,aclrules[j],sdslen(aclrules[j])) != C_OK) {
const char *errmsg = ACLSetUserStringError();
serverLog(LL_WARNING,"Error loading ACL rule '%s' for "
"the user named '%s': %s",
aclrules[j],aclrules[0],errmsg);
return C_ERR;
}
}

/* Having a disabled user in the configuration may be an error,
* warn about it without returning any error to the caller. */
//完成信息初始后,判断一下这个用户是不是还是不可用的
if (u->flags & USER_FLAG_DISABLED) {
serverLog(LL_NOTICE, "The user '%s' is disabled (there is no "
"'on' modifier in the user description). Make "
"sure this is not a configuration error.",
aclrules[0]);
}
}
return C_OK;
}

ACLLoadConfiguredUsers

从ACL文件中加载user代码比较多,下面会把一些不重要的(比如文件加载等等)删掉

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
84
85
86
87
88
89
90
91
92
93
94
95
sds ACLLoadFromFile(const char *filename) {
//这里会先把全局的 Users 保存到 old_users指针,
//然后重新 初始化一个 User,后面加载如果出错了,可以回滚
rax *old_users = Users;
Users = raxNew();

//遍历文件中所有的行,开始加载user
for (int i = 0; i < totlines; i++)
............ 省略读文件部分...............
//创建一个User
user *u = ACLCreateUser(argv[1],sdslen(argv[1]));

//如果用户已经存在了,跳过
if (!u) {
errors = sdscatprintf(errors,"WARNING: Duplicate user '%s' found on line %d. ", argv[1], linenum);
sdsfreesplitres(argv,argc);
continue;
}

/* Finally process the options and validate they can
* be cleanly applied to the user. If any option fails
* to apply, the other values won't be applied since
* all the pending changes will get dropped. */
int merged_argc;

//这个函数的作用是把读到的字符串,提取成具体的user属性的数组
sds *acl_args = ACLMergeSelectorArguments(argv + 2, argc - 2, &merged_argc, NULL);
if (!acl_args) {
errors = sdscatprintf(errors,
"%s:%d: Unmatched parenthesis in selector definition.",
server.acl_filename, linenum);
}

int syntax_error = 0;
for (int j = 0; j < merged_argc; j++) {
acl_args[j] = sdstrim(acl_args[j],"\t\r\n");
//遍历上面得到的手势,给创建的user设置属性
if (ACLSetUser(u,acl_args[j],sdslen(acl_args[j])) != C_OK) {
const char *errmsg = ACLSetUserStringError();
if (errno == ENOENT) {
/* For missing commands, we print out more information since
* it shouldn't contain any sensitive information. */
errors = sdscatprintf(errors,
"%s:%d: Error in applying operation '%s': %s. ",
server.acl_filename, linenum, acl_args[j], errmsg);
} else if (syntax_error == 0) {
/* For all other errors, only print out the first error encountered
* since it might affect future operations. */
errors = sdscatprintf(errors,
"%s:%d: %s. ",
server.acl_filename, linenum, errmsg);
syntax_error = 1;
}
}
}

for (int i = 0; i < merged_argc; i++) sdsfree(acl_args[i]);
zfree(acl_args);

/* Apply the rule to the new users set only if so far there
* are no errors, otherwise it's useless since we are going
* to discard the new users set anyway. */
if (sdslen(errors) != 0) {
sdsfreesplitres(argv,argc);
continue;
}

sdsfreesplitres(argv,argc);
}

sdsfreesplitres(lines,totlines);

/* Check if we found errors and react accordingly. */
if (sdslen(errors) == 0) {
//判断一下,是否加载了新的`default` user,如果有的话把旧的default user释放掉,用新加载的
user *new_default = ACLGetUserByName("default",7);
if (!new_default) {
new_default = ACLCreateDefaultUser();
}

ACLCopyUser(DefaultUser,new_default);
ACLFreeUser(new_default);
raxInsert(Users,(unsigned char*)"default",7,DefaultUser,NULL);
raxRemove(old_users,(unsigned char*)"default",7,NULL);
ACLFreeUsersSet(old_users);
sdsfree(errors);
return NULL;
} else {
//如果出错了,数据回滚
ACLFreeUsersSet(Users);
Users = old_users;
errors = sdscat(errors,"WARNING: ACL errors detected, no change to the previously active ACL rules was performed");
return errors;
}
}

ACLLoadFromFile

不管是从 配置文件中加载用户还是从ACL文件中加载用户,最终都会调用ACLSetUser 这个函数来完成 user 属性的初始化。使用redis的 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
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
int ACLSetUser(user *u, const char *op, ssize_t oplen) {
//第一步,先把以前的属性字符串删掉
if (u->acl_string) {
decrRefCount(u->acl_string);
u->acl_string = NULL;
}

if (oplen == -1) oplen = strlen(op);
if (oplen == 0) return C_OK; /* Empty string is a no-operation. */
if (!strcasecmp(op,"on")) {
//设置 user 是 开启的
u->flags |= USER_FLAG_ENABLED;
u->flags &= ~USER_FLAG_DISABLED;
} else if (!strcasecmp(op,"off")) {
//设置 user 是关闭的
u->flags |= USER_FLAG_DISABLED;
u->flags &= ~USER_FLAG_ENABLED;
} else if (!strcasecmp(op,"skip-sanitize-payload")) {
u->flags |= USER_FLAG_SANITIZE_PAYLOAD_SKIP;
u->flags &= ~USER_FLAG_SANITIZE_PAYLOAD;
} else if (!strcasecmp(op,"sanitize-payload")) {
u->flags &= ~USER_FLAG_SANITIZE_PAYLOAD_SKIP;
u->flags |= USER_FLAG_SANITIZE_PAYLOAD;
} else if (!strcasecmp(op,"nopass")) {
u->flags |= USER_FLAG_NOPASS;
listEmpty(u->passwords);
} else if (!strcasecmp(op,"resetpass")) {
u->flags &= ~USER_FLAG_NOPASS;
listEmpty(u->passwords);
} else if (op[0] == '>' || op[0] == '#') {
//设置密码
sds newpass;
if (op[0] == '>') {
//如果是明文,先转成sha256字符串
newpass = ACLHashPassword((unsigned char*)op+1,oplen-1);
} else {
//判断一下输入的值是不是有效的hash值
if (ACLCheckPasswordHash((unsigned char*)op+1,oplen-1) == C_ERR) {
errno = EBADMSG;
return C_ERR;
}
newpass = sdsnewlen(op+1,oplen-1);
}

listNode *ln = listSearchKey(u->passwords,newpass);
//如果之前有这个密码,不用添加了,避免同一个密码添加多次
if (ln == NULL)
listAddNodeTail(u->passwords,newpass);
else
sdsfree(newpass);
u->flags &= ~USER_FLAG_NOPASS;
} else if (op[0] == '<' || op[0] == '!') {
//删除密码,同样的,如果是明文,先转成sha256字符串
sds delpass;
if (op[0] == '<') {
delpass = ACLHashPassword((unsigned char*)op+1,oplen-1);
} else {
//同样,判断这个字符串是不是有效的hash值
if (ACLCheckPasswordHash((unsigned char*)op+1,oplen-1) == C_ERR) {
errno = EBADMSG;
return C_ERR;
}
delpass = sdsnewlen(op+1,oplen-1);
}
listNode *ln = listSearchKey(u->passwords,delpass);
sdsfree(delpass);
if (ln) {
listDelNode(u->passwords,ln);
} else {
errno = ENODEV;
return C_ERR;
}
} else if (op[0] == '(' && op[oplen - 1] == ')') {
//处理 `()` 内的 selector
aclSelector *selector = aclCreateSelectorFromOpSet(op, oplen);
if (!selector) {
/* No errorno set, propagate it from interior error. */
return C_ERR;
}
listAddNodeTail(u->selectors, selector);
return C_OK;
} else if (!strcasecmp(op,"clearselectors")) {
//清理掉所有的 selector
listIter li;
listNode *ln;
listRewind(u->selectors,&li);
/* There has to be a root selector */
serverAssert(listNext(&li));
while((ln = listNext(&li))) {
listDelNode(u->selectors, ln);
}
return C_OK;
} else if (!strcasecmp(op,"reset")) {
//重置这个用户
serverAssert(ACLSetUser(u,"resetpass",-1) == C_OK);
serverAssert(ACLSetUser(u,"resetkeys",-1) == C_OK);
serverAssert(ACLSetUser(u,"resetchannels",-1) == C_OK);
if (server.acl_pubsub_default & SELECTOR_FLAG_ALLCHANNELS)
serverAssert(ACLSetUser(u,"allchannels",-1) == C_OK);
serverAssert(ACLSetUser(u,"off",-1) == C_OK);
serverAssert(ACLSetUser(u,"sanitize-payload",-1) == C_OK);
serverAssert(ACLSetUser(u,"clearselectors",-1) == C_OK);
serverAssert(ACLSetUser(u,"-@all",-1) == C_OK);
} else {
//处理用户的 selector 设置, 比如 `+@, -@, &, ~*` 等等命令
aclSelector *selector = ACLUserGetRootSelector(u);
if (ACLSetSelector(selector, op, oplen) == C_ERR) {
return C_ERR;
}
}
return C_OK;
}

aclCreateSelectorFromOpSet 这个函数最终会调用 ACLSetSelector 来把字符串转换成具体的规则

ACLSetSelector

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
int ACLSetSelector(aclSelector *selector, const char* op, size_t oplen) {
if (!strcasecmp(op,"allkeys") || !strcasecmp(op,"~*")) {
//允许所有key
selector->flags |= SELECTOR_FLAG_ALLKEYS;
listEmpty(selector->patterns);
} else if (!strcasecmp(op,"resetkeys")) {
//去掉所有key权限
selector->flags &= ~SELECTOR_FLAG_ALLKEYS;
listEmpty(selector->patterns);
} else if (!strcasecmp(op,"allchannels") || !strcasecmp(op,"&*")) {
//channel 和key一样,允许所有和去掉所有
selector->flags |= SELECTOR_FLAG_ALLCHANNELS;
listEmpty(selector->channels);
} else if (!strcasecmp(op,"resetchannels")) {
selector->flags &= ~SELECTOR_FLAG_ALLCHANNELS;
listEmpty(selector->channels);
} else if (!strcasecmp(op,"allcommands") || !strcasecmp(op,"+@all")) {
//允许所有的权限, 下面是去掉所有权限
memset(selector->allowed_commands,255,sizeof(selector->allowed_commands));
selector->flags |= SELECTOR_FLAG_ALLCOMMANDS;
ACLResetFirstArgs(selector);
} else if (!strcasecmp(op,"nocommands") || !strcasecmp(op,"-@all")) {
memset(selector->allowed_commands,0,sizeof(selector->allowed_commands));
selector->flags &= ~SELECTOR_FLAG_ALLCOMMANDS;
ACLResetFirstArgs(selector);
} else if (op[0] == '~' || op[0] == '%') {
if (selector->flags & SELECTOR_FLAG_ALLKEYS) {
//如果是允许所有key了, 再加限制key的命令,会返回错误
errno = EEXIST;
return C_ERR;
}
int flags = 0;
size_t offset = 1;
if (op[0] == '%') {
//如果是%开头,需要判断是读还是写
for (; offset < oplen; offset++) {
if (toupper(op[offset]) == 'R' && !(flags & ACL_READ_PERMISSION)) {
flags |= ACL_READ_PERMISSION;
} else if (toupper(op[offset]) == 'W' && !(flags & ACL_WRITE_PERMISSION)) {
flags |= ACL_WRITE_PERMISSION;
} else if (op[offset] == '~') {
offset++;
break;
} else {
errno = EINVAL;
return C_ERR;
}
}
} else {
flags = ACL_ALL_PERMISSION;
}

if (ACLStringHasSpaces(op+offset,oplen-offset)) {
errno = EINVAL;
return C_ERR;
}
keyPattern *newpat = ACLKeyPatternCreate(sdsnewlen(op+offset,oplen-offset), flags);
listNode *ln = listSearchKey(selector->patterns,newpat);
//避免重复
if (ln == NULL) {
listAddNodeTail(selector->patterns,newpat);
} else {
//如果有重复的. 合并两个pattern的flag
((keyPattern *)listNodeValue(ln))->flags |= flags;
ACLKeyPatternFree(newpat);
}
selector->flags &= ~SELECTOR_FLAG_ALLKEYS;
} else if (op[0] == '&') {
if (selector->flags & SELECTOR_FLAG_ALLCHANNELS) {
//同样,如果已经允许所有channel了,再去限制channel,也会报错
errno = EISDIR;
return C_ERR;
}
if (ACLStringHasSpaces(op+1,oplen-1)) {
errno = EINVAL;
return C_ERR;
}
sds newpat = sdsnewlen(op+1,oplen-1);
listNode *ln = listSearchKey(selector->channels,newpat);
//同样,也会对channel pattern 去重
if (ln == NULL)
listAddNodeTail(selector->channels,newpat);
else
sdsfree(newpat);
selector->flags &= ~SELECTOR_FLAG_ALLCHANNELS;
} else if (op[0] == '+' && op[1] != '@') {
//加命令权限 (没有在 ACL CAT分类里的)
//如果一个命令没有在分类里,可以通过前缀来做匹配,比如
//DB提供执行SELECT的能力,但可能仍然希望能够运行SELECT 0。
//ACL SETUSER myuser -select +select|0
if (strrchr(op,'|') == NULL) {
//如果op字符串中没有 `|` 不需要处理前缀, 直接处理即可
//需要先处理一下重命名的命令
struct redisCommand *cmd = ACLLookupCommand(op+1);
if (cmd == NULL) {
errno = ENOENT;
return C_ERR;
}
//处理禁用和允许的命令,包括子命令
ACLChangeSelectorPerm(selector,cmd,1);
} else {
//分割字符串,得到子命令
char *copy = zstrdup(op+1);
char *sub = strrchr(copy,'|');
sub[0] = '\0';
sub++;

struct redisCommand *cmd = ACLLookupCommand(copy);

/* Check if the command exists. We can't check the
* first-arg to see if it is valid. */
if (cmd == NULL) {
zfree(copy);
errno = ENOENT;
return C_ERR;
}

//不能直接作用于子命令
if (cmd->parent) {
zfree(copy);
errno = ECHILD;
return C_ERR;
}

//子命令不能是空的,比如 `HASH|` 这种是不合法的
if (strlen(sub) == 0) {
zfree(copy);
errno = EINVAL;
return C_ERR;
}

if (cmd->subcommands_dict) {
//这个命令有子命令,处理子命令
cmd = ACLLookupCommand(op+1);
if (cmd == NULL) {
zfree(copy);
errno = ENOENT;
return C_ERR;
}
ACLChangeSelectorPerm(selector,cmd,1);
} else {
/* If user is trying to use the ACL mech to block SELECT except SELECT 0 or
* block DEBUG except DEBUG OBJECT (DEBUG subcommands are not considered
* subcommands for now) we use the allowed_firstargs mechanism. */

/* Add the first-arg to the list of valid ones. */
serverLog(LL_WARNING, "Deprecation warning: Allowing a first arg of an otherwise "
"blocked command is a misuse of ACL and may get disabled "
"in the future (offender: +%s)", op+1);
ACLAddAllowedFirstArg(selector,cmd->id,sub);
}

zfree(copy);
}
} else if (op[0] == '-' && op[1] != '@') {
//去掉 命令权限 (没有在 ACL CAT分类里的)
struct redisCommand *cmd = ACLLookupCommand(op+1);
if (cmd == NULL) {
errno = ENOENT;
return C_ERR;
}
ACLChangeSelectorPerm(selector,cmd,0);
} else if ((op[0] == '+' || op[0] == '-') && op[1] == '@') {
//加或者减权限 (只处理在ACL CAT分类里的)
int bitval = op[0] == '+' ? 1 : 0;
if (ACLSetSelectorCommandBitsForCategory(selector,op+2,bitval) == C_ERR) {
errno = ENOENT;
return C_ERR;
}
} else {
errno = EINVAL;
return C_ERR;
}
return C_OK;
}

权限设置相关的差不多就是这些了,基本都是得到字符串后,处理字符串,然后处理对应的权限。代码量不小,其中一些细节没有详细展开分析,逻辑都不太复杂,后面有时间会考虑把这里的坑填上。

ACL规则存储

使用 ACL SETUSER 命令设置的规则,默认只会存在内存中,如果没有调用ACL SAVE命令进行存储,redis重启后会丢失所有 用户信息

使用ACL SAVE 命令的前提是 redis 配置了 ACL 文件,如果没有配置 ACL 文件,那么 使用 ACL SAVE命令会报错

使用 ACL SAVEACL LIST最终都会调用这个函数来生成 ACL 规则的字符串

ACLDescribeUser

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
robj *ACLDescribeUser(user *u) {
if (u->acl_string) {
incrRefCount(u->acl_string);
return u->acl_string;
}

sds res = sdsempty();

先处理flag
for (int j = 0; ACLUserFlags[j].flag; j++) {
if (u->flags & ACLUserFlags[j].flag) {
res = sdscat(res,ACLUserFlags[j].name);
res = sdscatlen(res," ",1);
}
}

//处理密码
listIter li;
listNode *ln;
listRewind(u->passwords,&li);
while((ln = listNext(&li))) {
sds thispass = listNodeValue(ln);
res = sdscatlen(res,"#",1);
res = sdscatsds(res,thispass);
res = sdscatlen(res," ",1);
}

//生成 selector的规则字符串
listRewind(u->selectors,&li);
while((ln = listNext(&li))) {
aclSelector *selector = (aclSelector *) listNodeValue(ln);
//通过 ACLDescribeSelector 生成 selector的规则字符串
//这里面的处理如果详细来拆分的话,还是有点多的,就不展开了
sds default_perm = ACLDescribeSelector(selector);
//这里需要注意一下, 如果是非root selector, 规则需要用 () 包裹
if (selector->flags & SELECTOR_FLAG_ROOT) {
res = sdscatfmt(res, "%s", default_perm);
} else {
res = sdscatfmt(res, " (%s)", default_perm);
}
sdsfree(default_perm);
}

u->acl_string = createObject(OBJ_STRING, res);
/* because we are returning it, have to increase count */
incrRefCount(u->acl_string);

return u->acl_string;
}

ACL鉴权

下面开始分析 鉴权相关的代码。

上面的基本是和权限设置相关的,有了权限后,要执行命令时,需要判断是否可以执行,这部分就是鉴权了。

先来最基本的 AUTH 命令

authCommand

AUTH 命令兼容以前的命令参数,会根据参数的个数判断命名的版本,如果参数长度是2,会使用以前的模式来处理。

如果参数长度是3,会按照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
33
34
35
void authCommand(client *c) {
//如果参数>3了这个命令是错误的,直接返回
if (c->argc > 3) {
addReplyErrorObject(c,shared.syntaxerr);
return;
}
redactClientCommandArgument(c, 1);

/* Handle the two different forms here. The form with two arguments
* will just use "default" as username. */
robj *username, *password;
if (c->argc == 2) {
//处理两种参数的情况,如果默认用户是没有密码的, 返回错谁
if (DefaultUser->flags & USER_FLAG_NOPASS) {
addReplyError(c,"AUTH <password> called without any password "
"configured for the default user. Are you sure "
"your configuration is correct?");
return;
}

username = shared.default_username;
password = c->argv[1];
} else {
//处理三种参数的情况
username = c->argv[1];
password = c->argv[2];
redactClientCommandArgument(c, 2);
}
//校验用户名和密码是否匹配
if (ACLAuthenticateUser(c,username,password) == C_OK) {
addReply(c,shared.ok);
} else {
addReplyError(c,"-WRONGPASS invalid username-password pair or user is disabled.");
}
}

AUTH 命令处理还是挺简单的,下面开始ACL对具体命令的判断

当一个命令进来后,redis要先判断命令是否能执行,从 server.c:3769 开始,调用ACLCheckAllPerm函数。

最终会调用到 ACLCheckAllUserCommandPerm 函数去做具体的校验逻辑

会遍历userSelectors 这个链表来做鉴权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
listRewind(u->selectors,&li);
while((ln = listNext(&li))) {
aclSelector *s = (aclSelector *) listNodeValue(ln);
int acl_retval = ACLSelectorCheckCmd(s, cmd, argv, argc, &local_idxptr, &cache);
if (acl_retval == ACL_OK) {
//如果通过校验了,可以结束循环,然后返回正确
cleanupACLKeyResultCache(&cache);
return ACL_OK;
}
if (acl_retval > relevant_error ||
(acl_retval == relevant_error && local_idxptr > last_idx))
{
relevant_error = acl_retval;
last_idx = local_idxptr;
}
}

做鉴权判断的是这个函数ACLSelectorCheckCmd

ACLSelectorCheckCmd

鉴权的优先级

  1. 判断命令类型,比如GET,SET,HGET 等等,判断是否有这个类型命令的权限
  2. 判断key名,通过对比key的名字,判断这个key是否有权限
  3. 判断是否是channel命令,然后判断权限
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
static int ACLSelectorCheckCmd(aclSelector *selector, struct redisCommand *cmd, robj **argv, int argc, int *keyidxptr, aclKeyResultCache *cache) {
uint64_t id = cmd->id;
int ret;
//如果 flags 不是全部 ALLCOMMANDS 并且 这个命令不是不需要验证的命令(这个命令需要auth后才能使用)
//CMD_NO_AUTH命令包括 `auth`,`hello`,`quit`,`reset`
if (!(selector->flags & SELECTOR_FLAG_ALLCOMMANDS) && !(cmd->flags & CMD_NO_AUTH)) {
//如果命令bit里没有找到,需要再去匹配一下前缀,防止漏判
if (ACLGetSelectorCommandBit(selector,id) == 0) {
/* Check if the first argument matches. */
if (argc < 2 ||
selector->allowed_firstargs == NULL ||
selector->allowed_firstargs[id] == NULL)
{
return ACL_DENIED_CMD;
}

long subid = 0;
//遍历,比较前缀
while (1) {
if (selector->allowed_firstargs[id][subid] == NULL)
return ACL_DENIED_CMD;
int idx = cmd->parent ? 2 : 1;
if (!strcasecmp(argv[idx]->ptr,selector->allowed_firstargs[id][subid]))
break; /* First argument match found. Stop here. */
subid++;
}
}
}

//如果不是允许所有的key 并且这个命令里 包含了key
//排除类似 select 0 等等没有key操作的命令
if (!(selector->flags & SELECTOR_FLAG_ALLKEYS) && doesCommandHaveKeys(cmd)) {
//这里有一个从函数外传进来的,用来缓存key 的结构, 如果没有初始化过,需要初始化一下
if (!(cache->keys_init)) {
cache->keys = (getKeysResult) GETKEYS_RESULT_INIT;
//获取到这条命令中所有的key
getKeysFromCommandWithSpecs(cmd, argv, argc, GET_KEYSPEC_DEFAULT, &(cache->keys));
cache->keys_init = 1;
}
getKeysResult *result = &(cache->keys);
keyReference *resultidx = result->keys;
for (int j = 0; j < result->numkeys; j++) {
int idx = resultidx[j].pos;
//通过遍历所有的key,判断是否符合规则
ret = ACLSelectorCheckKey(selector, argv[idx]->ptr, sdslen(argv[idx]->ptr), resultidx[j].flags);
//遇到一个不符合的,直接就可以退出,并返回错误
if (ret != ACL_OK) {
if (keyidxptr) *keyidxptr = resultidx[j].pos;
return ret;
}
}
}

//检查channel的权限,只需要检查 pub 和 sub这两个权限即可
const int channel_flags = CMD_CHANNEL_PUBLISH | CMD_CHANNEL_SUBSCRIBE;
if (!(selector->flags & SELECTOR_FLAG_ALLCHANNELS) && doesCommandHaveChannelsWithFlags(cmd, channel_flags)) {
getKeysResult channels = (getKeysResult) GETKEYS_RESULT_INIT;
getChannelsFromCommand(cmd, argv, argc, &channels);
keyReference *channelref = channels.keys;
for (int j = 0; j < channels.numkeys; j++) {
int idx = channelref[j].pos;
if (!(channelref[j].flags & channel_flags)) continue;
int is_pattern = channelref[j].flags & CMD_CHANNEL_PATTERN;
//和匹配key类似,也是遍历匹配,遇到不符合的就结束并返回错误
int ret = ACLCheckChannelAgainstList(selector->channels, argv[idx]->ptr, sdslen(argv[idx]->ptr), is_pattern);
if (ret != ACL_OK) {
if (keyidxptr) *keyidxptr = channelref[j].pos;
getKeysFreeResult(&channels);
return ret;
}
}
getKeysFreeResult(&channels);
}
return ACL_OK;
}

ACL鉴权的基本就是这些了

ACL相关的一些知识

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
33
34
35
36
37
38
39
40
41
42
//命令分类的定义
struct ACLCategoryItem {
const char *name;
uint64_t flag;
} ACLCommandCategories[] = { /* See redis.conf for details on each category. */
{"keyspace", ACL_CATEGORY_KEYSPACE},
{"read", ACL_CATEGORY_READ},
{"write", ACL_CATEGORY_WRITE},
{"set", ACL_CATEGORY_SET},
{"sortedset", ACL_CATEGORY_SORTEDSET},
{"list", ACL_CATEGORY_LIST},
{"hash", ACL_CATEGORY_HASH},
{"string", ACL_CATEGORY_STRING},
{"bitmap", ACL_CATEGORY_BITMAP},
{"hyperloglog", ACL_CATEGORY_HYPERLOGLOG},
{"geo", ACL_CATEGORY_GEO},
{"stream", ACL_CATEGORY_STREAM},
{"pubsub", ACL_CATEGORY_PUBSUB},
{"admin", ACL_CATEGORY_ADMIN},
{"fast", ACL_CATEGORY_FAST},
{"slow", ACL_CATEGORY_SLOW},
{"blocking", ACL_CATEGORY_BLOCKING},
{"dangerous", ACL_CATEGORY_DANGEROUS},
{"connection", ACL_CATEGORY_CONNECTION},
{"transaction", ACL_CATEGORY_TRANSACTION},
{"scripting", ACL_CATEGORY_SCRIPTING},
{NULL,0} /* Terminator. */
};

//userflag的定义
struct ACLUserFlag {
const char *name;
uint64_t flag;
} ACLUserFlags[] = {
/* Note: the order here dictates the emitted order at ACLDescribeUser */
{"on", USER_FLAG_ENABLED},
{"off", USER_FLAG_DISABLED},
{"nopass", USER_FLAG_NOPASS},
{"skip-sanitize-payload", USER_FLAG_SANITIZE_PAYLOAD_SKIP},
{"sanitize-payload", USER_FLAG_SANITIZE_PAYLOAD},
{NULL,0} /* Terminator. */
};

还有一个在权限校验中用到的东西 cmd->id 这个是每个redis命令的id,具体是通过这个函数得到的

ACLGetCommandID

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
//获取一个命令的id
unsigned long ACLGetCommandID(sds cmdname) {
sds lowername = sdsdup(cmdname);
sdstolower(lowername);
if (commandId == NULL) commandId = raxNew();
void *id = raxFind(commandId,(unsigned char*)lowername,sdslen(lowername));
if (id != raxNotFound) {
sdsfree(lowername);
//如果找到了,直接return id就可以了
return (unsigned long)id;
}

//如果没有找到, 把这个命令插入的rax表中,并得到id
raxInsert(commandId,(unsigned char*)lowername,strlen(lowername),
(void*)nextid,NULL);
sdsfree(lowername);
unsigned long thisid = nextid;
//id自增, nextid是在acl中定义的
nextid++;

//如果id==1023了, 再自增一次,防止id是1024
//因为这个值要给aclSelector中的allowed_commands做索引,
//allowed_commands 是一个长度为16的数组,至于为什么不能是1024这个值,后面会看到
if (nextid == USER_COMMAND_BITS_COUNT-1) nextid++;
return thisid;
}

//nextid的定义
static unsigned long nextid = 0; /* Next command id that has not been assigned */

//aclSelector 中的 allowed_commands定义
uint64_t allowed_commands[USER_COMMAND_BITS_COUNT/64];

ACLSetSelectorCommandBit

根据 cmd.id 设置 selector

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
void ACLSetSelectorCommandBit(aclSelector *selector, unsigned long id, int value) {
uint64_t word, bit;
//调用 ACLGetCommandBitCoordinates 计算得到 word 和bit
if (ACLGetCommandBitCoordinates(id,&word,&bit) == C_ERR) return;
if (value) {
//如果是允许的话,设置这个bit
selector->allowed_commands[word] |= bit;
} else {
//如果是不予许, 取消这个位, 再把flag设置一下
selector->allowed_commands[word] &= ~bit;
selector->flags &= ~SELECTOR_FLAG_ALLCOMMANDS;
}
}

int ACLGetCommandBitCoordinates(uint64_t id, uint64_t *word, uint64_t *bit) {
//如果id >= 1024了,return 错误
if (id >= USER_COMMAND_BITS_COUNT) return C_ERR;
//sizeof(uint64_t) 计算得到8
//word 得到但是 数组的索引
//到这应该就明白为什么 在 `ACLGetCommandID` id 不能是1024了吧
*word = id / sizeof(uint64_t) / 8;
//bit 是通过位运算得到的, 是id%64 得到的一个[0,63]的值,然后再左移得到 bit的值
*bit = 1ULL << (id % (sizeof(uint64_t) * 8));
return C_OK;
}

这样一番操作下来,selector.allowed_commands 得到一个长度是64的 uint64_t 的数组,数组内的每一个整数的每一个位,记录了一个命令开启或者关闭(对应位的0或1)

这里有个非常巧妙的计算过程。

一般我们在使用位运算记录的时候,都是1,2,4,8 等等这些2的倍数来做,因为要保证不会出现位冲突。比如2的二进制是 10, 3的二进制是 11 在同时使用 1011 进行 与或运算的时候,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 类型的数组

commands初始化

在这里,redis的所有命令都会被初始化

就拿 ACL 这个命令来说吧 ACL命令初始化

ACL命令的 acl_categories 被设置为了0, subcommands 被设置为了 ACL_Subcommands

其中 ACL_Subcommands 是一个命令的数组,在这里被初始化

有子命令的会把对应的子命令赋值对 subcommands 中,没有子命令的就直接是空指针了

暂时能想到的就是这些了,后面有新的再补充