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
複製代碼
相關文章
相關標籤/搜索