Redis 6.0 权限控制基于 Bitmap 实现

Redis 6.0在4月30日就要和你们正式见面了,如今redis.io上已经提供了RC版本。在以前的博客中,已经介绍过权限控制新功能的一些用法,主要来源于做者Antirez在Redis Day上的一些演示。Antirez在最后提到,ACL的主要实现是基于Bitmap,所以对性能影响是能够忽略不计的。当时大体猜测了一下实现的思路,那么如今离发布已经很近了,做者也对ACL Logging进行了一些补充,不妨一块儿来看一下。redis

user结构

server.h中定义了对应的user结构保存用户的ACL信息,包括:数组

  • 用户名
  • flag,主要是一些特殊状态,例如用户的启用与禁用、总体控制(全部命令可用与否、全部键可访问与否)、免密码等
  • 可用命令(allowed_commands),一个长整型数。每一位表明命令,若是用户容许使用这个命令则置位1
  • 可用子命令(allowed_subcommands),一个指针数组,值也为指针,数组与可用命令一一对应,值为一个SDS数组,SDS数组中存放的是这个命令可用的子命令
  • 用户密码
  • 可用的key patterns。若是这个字段为NULL,用户将不能使用任何Key,除非flag中指明特殊状态如ALLKEYS
typedef struct user {
    sds name;
    uint64_t flags;
    uint64_t allowed_commands[USER_COMMAND_BITS_COUNT/64];
    sds **allowed_subcommands;
    list *passwords;
    list *patterns;
} user;
复制代码

补充一下一些新鲜的字段描述,allowed_commands其实是一个(默认)长度为1024的位图,它的index对应各个命令的ID,在历史版本中命令结构redisCommand是经过名字(name)来查找的,id为这个版本中新增的属性,专门用于ACL功能。bash

struct redisCommand {
    ...
    int id;
};
复制代码

user这个结构对应的是client结构的"user"字段,熟悉Redis的同窗应该对client也有所了解,就再也不赘述了。数据结构

ACL操做选读

ACL的命令不少,整体而言都是围绕着user对象展开的,所以从中挑选了几个函数来看一下具体是如何操做user对象。函数

一个须要铺垫的通用方法就是ACLGetUserCommandBit,ACL操做中都会涉及到获取用户的命令位图,ACLGetUserCommandBit()接收一个user结构和命令ID,根据ID定位出命令在allowed_commands中的位置,经过位运算返回用户是否有该命令权限性能

int ACLGetUserCommandBit(user *u, unsigned long id) {
    uint64_t word, bit;
    if (ACLGetCommandBitCoordinates(id,&word,&bit) == C_ERR) return 0;
    return (u->allowed_commands[word] & bit) != 0;
}
复制代码

当用户进行Redis操做时,例如set操做,操做的命令会保存在client结构的*cmd字段中,*cmd字段就是一个redisCommand结构的指针,redisCommand结构包含了命令的id,所以在使用时经过ACLGetUserCommandBit(u, cmd->id)传入。ui

建立用户

建立用户分为两步,首先须要建立一个user,经过调用ACLCreateUser(const char *name, size_t namelen)实现,返回的是一个user对象的指针。在建立时,会在server.h定义的Users中查找是否有同名用户,也是本次功能新增的,由于旧版本中只有"default"用户。此时这个用户拥有名称,flag被初始化为禁用用户,其他的属性均为Null或空list等。this

而后,经过调用ACLSetUser(user *u, const char *op, ssize_t oplen),调整传入用户u的对应属性,调整内容放在名为op操做的参数中。这个函数很是长,主要是针对各类不一样的“操做” switch case处理,节选部分以下:spa

int ACLSetUser(user *u, const char *op, ssize_t oplen) {
    if (oplen == -1) oplen = strlen(op);
    /* Part1 - 处理用户状态(flag)操做 */
    // 控制用户启用状态
    if (!strcasecmp(op,"on")) {
        u->flags |= USER_FLAG_ENABLED;
        u->flags &= ~USER_FLAG_DISABLED;
    } else if (!strcasecmp(op,"off")) {
        u->flags |= USER_FLAG_DISABLED;
        u->flags &= ~USER_FLAG_ENABLED;
    // 控制全局键、命令等可用与否
    } else if (!strcasecmp(op,"allkeys") ||
               !strcasecmp(op,"~*"))
    {
        u->flags |= USER_FLAG_ALLKEYS;
        listEmpty(u->patterns);
    }
    ...


    /* Part2 - 操做用户密码增删改查 */
    // > 和 < 等控制密码的改动删除等
    else if (op[0] == '>' || op[0] == '#') {
        sds newpass;
        if (op[0] == '>') {
            newpass = ACLHashPassword((unsigned char*)op+1,oplen-1);
        }


    /* Part3 - 操做用户可用命令的范围 */
    else if (op[0] == '+' && op[1] != '@') {
        if (strchr(op,'|') == NULL) {
            if (ACLLookupCommand(op+1) == NULL) {
                errno = ENOENT;
                return C_ERR;
            }
            unsigned long id = ACLGetCommandID(op+1);
            // 根据传入的id参数设置对应allowed_commands位图的值
            ACLSetUserCommandBit(u,id,1);
            // 新调整的命令的子命令数组会被重置
            ACLResetSubcommandsForCommand(u,id);
        }
    }
复制代码

补充一下具体调用例子,其实Redis的默认用户就是按照这套流程建立的:初始化名为“default”的空白无权限用户,而后为这个用户设置上全部权限:指针

DefaultUser = ACLCreateUser("default",7);
ACLSetUser(DefaultUser,"+@all",-1);
ACLSetUser(DefaultUser,"~*",-1);
ACLSetUser(DefaultUser,"on",-1);
ACLSetUser(DefaultUser,"nopass",-1);
复制代码

拦截不可用命令/键

命令/键拦截操做很是简单:

  • 判断命令/键是否可用
    • 若是不可用,ACL Log处理以及返回错误

ACL判断

咱们先看一下“不可用”的判断逻辑,而后再回到命令执行流程中看判断方法的调用。

判断函数一样很是长,展现完后会进行总结:

int ACLCheckCommandPerm(client *c, int *keyidxptr) {
    user *u = c->user;
    uint64_t id = c->cmd->id;
    // 命令相关的全局flag的检查,若知足则跳事后续部分
    if (!(u->flags & USER_FLAG_ALLCOMMANDS) &&
        c->cmd->proc != authCommand)
    {
        // 即便当前命令没有在allowed_commands中,还要检查子命令是否可用
        // 以避免出现仅开放了部分子命令权限的状况
        if (ACLGetUserCommandBit(u,id) == 0) {
            ...
            // 遍历子命令
            long subid = 0;
            while (1) {
                if (u->allowed_subcommands[id][subid] == NULL)
                    return ACL_DENIED_CMD;
                if (!strcasecmp(c->argv[1]->ptr,
                                u->allowed_subcommands[id][subid]))
                    break; // 子命令可用,跳出循环
                subid++;
            }
        }
    }

    // 键相关的全局flag检查,若知足则跳事后续部分
    if (!(c->user->flags & USER_FLAG_ALLKEYS) &&
        (c->cmd->getkeys_proc || c->cmd->firstkey))
    {
        int numkeys;
        // 先拿到当前要进行操做的Key
        int *keyidx = getKeysFromCommand(c->cmd,c->argv,c->argc,&numkeys);
        for (int j = 0; j < numkeys; j++) {
            listIter li;
            listNode *ln;
            listRewind(u->patterns,&li);

            // 检查当前user全部的关于Key的匹配Pattern
            // 若是有任意命中则跳出,不然断定不可用
            int match = 0;
            while((ln = listNext(&li))) {
                sds pattern = listNodeValue(ln);
                size_t plen = sdslen(pattern);
                int idx = keyidx[j];
                if (stringmatchlen(pattern,plen,c->argv[idx]->ptr,
                                   sdslen(c->argv[idx]->ptr),0))
                {
                    match = 1;
                    break;
                }
            }
            if (!match) {
                if (keyidxptr) *keyidxptr = keyidx[j];
                getKeysFreeResult(keyidx);
                return ACL_DENIED_KEY;
            }
        }
        getKeysFreeResult(keyidx);
    }
    return ACL_OK;
}
复制代码

那么为了方便喜欢跳过代码的同窗看结论:

  • ACL限制围绕user的各个字段进行
  • 全局的flag优先级最高,例如设置为全部键可用,全部命令可用,会跳事后续的可用命令遍历和可用键Pattern匹配
  • 即便在allowed_commands位图中没有被置位,命令也可能可用,由于它是个子命令,并且命令只开放了部分子命令的使用权限
  • 键经过遍历全部定义了的Pattern检查,若是有匹配上说明可用
  • 先判断操做是否可用,再判断键(包括全局flag也在操做以后)是否可用,两种判断分别对应不一样返回整数值:ACL_DENIED_CMDACL_DENIED_KEY

命令执行流程中的调用

判断逻辑以后到什么时候调用这套判断。咱们先来复习一下Redis如何执行命令:

  • 用户操做
  • 客户端RESP协议(Redis 6.0中有RESP3新协议记得关注)压缩发送给服务端
  • 服务端解读消息,存放至client对象的对应字段中,例如argcargv等存放命令和参数等内容
  • 执行前检查(各类执行条件)
  • 执行命令
  • 执行后处理(慢查询日志、AOF等)

目前执行命令的方法是在server.c中的processCommand(client *c),传入client对象,执行,返回执行成功与否。咱们节选其中关于ACL的部分以下:

int processCommand(client *c) {
    ...
    int acl_keypos;
    int acl_retval = ACLCheckCommandPerm(c,&acl_keypos);
    if (acl_retval != ACL_OK) {
        addACLLogEntry(c,acl_retval,acl_keypos,NULL);
        flagTransaction(c);
        if (acl_retval == ACL_DENIED_CMD)
            addReplyErrorFormat(c,
                "-NOPERM this user has no permissions to run "
                "the '%s' command or its subcommand", c->cmd->name);
        else
            addReplyErrorFormat(c,
                "-NOPERM this user has no permissions to access "
                "one of the keys used as arguments");
        return C_OK;
    }
    ...
复制代码

在命令解析以后,真正执行以前,经过调用ACLCheckCommandPerm获取判断结果,若是断定不经过,进行如下操做:

  • 记录ACL不经过的日志,这个是做者在RC1以后新增的功能,还在Twitch上进行了直播开发,有兴趣的同窗能够在Youtube上看到录播
  • 若是当前处于事务(MULTI)过程当中,将client的flag置为CLIENT_DIRTY_EXEC
  • 根据命令仍是键不可用,返回给客户端不一样的信息

所以此次ACL功能影响的是执行命令先后的操做。

其余功能对ACL的调用

经过搜索能够发现一共有3处调用了ACLCheckCommandPerm方法:

/home/duck/study/redis/src/multi.c:
  179  
  180          int acl_keypos;
  181:         int acl_retval = ACLCheckCommandPerm(c,&acl_keypos);
  182          if (acl_retval != ACL_OK) {
  183              addACLLogEntry(c,acl_retval,acl_keypos,NULL);

/home/duck/study/redis/src/scripting.c:
  608      /* Check the ACLs. */
  609      int acl_keypos;
  610:     int acl_retval = ACLCheckCommandPerm(c,&acl_keypos);
  611      if (acl_retval != ACL_OK) {
  612          addACLLogEntry(c,acl_retval,acl_keypos,NULL);

/home/duck/study/redis/src/server.c:
 3394       * ACLs. */
 3395      int acl_keypos;
 3396:     int acl_retval = ACLCheckCommandPerm(c,&acl_keypos);
 3397      if (acl_retval != ACL_OK) {
 3398          addACLLogEntry(c,acl_retval,acl_keypos,NULL);
复制代码

形式都是大同小异,了解一下便可。总结一下须要断定ACL的位置:

  • 正常命令执行流程中
  • MULTI事务执行过程当中
  • Lua脚本

总结

补充一张图来描述新增的ACL功能相关的结构:

图中部分的表达可能与实际的数据结构有所差别,主要缘由是代码理解和C语言的语法掌握不到位所致。

阅读代码的过程当中留意到,对命令的限制是经过Bitmap来实现的,而对Key的限制是经过特定Pattern来实现的。当对Key的限制Pattern数量特别多时,是否会由于匹配Pattern而对性能形成影响,例如超屡次的stringmatchlen()执行。固然这一块内容彷佛确实没有想到什么提高很是大的判断方式,后续也会继续关注ACL的相关改进。

博客:https://blog.2014bduck.com/archives/343
备注:毕业不久多积累一点老是好的orz,若是解读得不正确或者不恰当欢迎邮件骚扰2014bduck@gmail.com
复制代码
相关文章
相关标签/搜索