Featured image of post redis源码学习|ACL

redis源码学习|ACL

深入了解Redis 6.0的新特性:ACL(访问控制列表)。本文详细探讨了Redis ACL的作用、命令和权限设置,以及如何在源码层面实现ACL的初始化、加载和鉴权。从基本的AUTH命令到复杂的权限设置,每一部分都配有清晰的代码示例和解释。文章还涵盖了ACL规则的存储和命令初始化过程,为想要在Redis中实现细粒度访问控制的开发者提供了宝贵的资源。

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;

user源码

aclSelector源码

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

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}

ACLCreateDefaultUser

创建一个用户是在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}

ACLCreateUser

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

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

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

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

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

ACLAppendUserForLoading

完成加载后,会调用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}

ACLLoadUsersAtStartup

从配置文件中加载的逻辑,需要完成用户加载和用户信息的初始化(用户的密码,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}

ACLLoadConfiguredUsers

从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}

ACLLoadFromFile

不管是从 配置文件中加载用户还是从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 来把字符串转换成具体的规则

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 SAVEACL LIST最终都会调用这个函数来生成 ACL 规则的字符串

ACLDescribeUser

 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 命令

authCommand

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 函数去做具体的校验逻辑

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

 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

ACLSelectorCheckCmd

鉴权的优先级

  1. 判断命令类型,比如GET,SET,HGET 等等,判断是否有这个类型命令的权限
  2. 判断key名,通过对比key的名字,判断这个key是否有权限
  3. 判断是否是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相关的一些知识

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,具体是通过这个函数得到的

ACLGetCommandID

 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];

ACLSetSelectorCommandBit

根据 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 在同时使用 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 中,没有子命令的就直接是空指针了

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

发表了56篇文章 · 总计128.44k字
本博客已稳定运行
© QX
使用 Hugo 构建
主题 StackJimmy 设计