zk原始碼閱讀9:資料模型ACL之基礎資料結構以及驗證

weixin_33727510發表於2017-06-21

摘要

DataTree涉及到ACL,本節先講解ACL相關內容
講ACL的參考資料並不是很多,書上也沒有講原理實現,這裡自己整理一下
本文主要講解

ACL簡介
ACL資料結構
  perms
  Id(id,schema)
  內建許可權,ACL列表,Id,schema
ACL的建立,修改
ACL的驗證
  ACL建立修改的驗證(create,setACL)
  ACL申請許可權的驗證(各種操作)

簡介

ZooKeeper使用ACL來控制訪問其znode(ZooKeeper的資料樹的資料節點)。ACL的實現方式非常類似於UNIX檔案的訪問許可權:它採用訪問許可權位 允許/禁止 對節點的各種操作以及能進行操作的範圍。不同於UNIX許可權的是,ZooKeeper的節點不侷限於 使用者(檔案的擁有者),組和其他人(其它)這三個標準範圍。ZooKeeper不具有znode的擁有者的概念。相反,ACL指定id集以及與之對應的許可權。

還要注意的是一條ACL僅針對於一個特定的節點。尤其不適用於子節點。
例如,如果/app 只對IP:172.16.16.1可讀 而 / APP/status 是對任何人可讀的,ACL不是遞迴的。

ZooKeeper支援可插拔的身份驗證方案。 id使用如下形式 scheme:id,其中 scheme 是 id 所對應一個認證方案。例如,IP:172.16.16.1,id為主機的地址 172.16.16.1。

當客戶端連線到ZooKeeper驗證自己時,ZooKeeper將有關該客戶端的所有Id與客戶連線關聯。客戶端試圖訪問一個節點時,這些ID與該znodes的ACL驗證。 ACL是由(scheme:expression, perms)對構成。其中expression的格式指定為scheme。例如,(IP:19.22.0.0/16,READ)表示對所有起始IP為19.22的客戶端具有讀許可權。

一個ZooKeeper的節點(znode)儲存兩部分內容:資料和狀態,狀態中包含ACL資訊。建立一個znode會產生一個ACL列表。

那麼,ACL具體是什麼呢,怎麼實現的?

ACL資料結構

程式碼裡面涉及ACL機制的類有

org.apache.zookeeper.data.ACL
  包含許可權perms與Id(見下)

org.apache.zookeeper.data.Id
  包含驗證模式schema和提供的驗證內容id
  
org.apache.zookeeper.ZooDefs
  提供內建的OpCode
  許可權Perms
  ACL列表定義Ids

先用一張圖說明ACL與Id這兩個類的依賴關係


4871751-889465cda47bfda6.png
ACL與Id依賴關係

也可以說,每個ACL包括:

  驗證模式(scheme)
  具體內容(Id)(當scheme=“digest”時,Id為使用者名稱密碼,例如“root:J0sTy9BCUKubtK1y8pkbL7qoxSw=”)
  許可權(perms)

下面分開進行介紹這兩個結構(perms,Id),也可以說是三個結構(perms,id,schema),這裡根據類的定義來,還是當成兩個資料結構來講,ACL資料結構如下

public class ACL implements Record {
  private int perms;
  private Id id;
}

許可權perms

目前,節點的許可權(perms)有以下幾種,在org.apache.zookeeper.ZooDefs.Perms 中定義

        int READ = 1 << 0;//允許對本節點GetChildren和GetData操作
        int WRITE = 1 << 1;//允許對本節點SetData操作
        int CREATE = 1 << 2;//允許對子節點Create操作
        int DELETE = 1 << 3;//允許對子節點Delete操作
        int ADMIN = 1 << 4;//允許對本節點setAcl操作
        int ALL = READ | WRITE | CREATE | DELETE | ADMIN;//這個是組合許可權

ACL許可權用一個int型數字perms表示
perms的5個二進位制位分別表示setacl、delete、create、write、read
比如0x1f=adcwr,0x1=----r,0x15=a-c-r。
除了ALL以外,其他都是最細粒度的許可權,可以用|,&來自己定義perms的組合許可權

Id

包含驗證模式schema以及提供驗證的內容id
目前zk提供了兩個內建的Id,在org.apache.zookeeper.ZooDefs.Ids中定義

/**
         * This Id represents anyone.
         */
        public final Id ANYONE_ID_UNSAFE = new Id("world", "anyone");//固定使用者為anyone,為所有Client端開放許可權

        /**
         * This Id is only usable to set ACLs. It will get substituted with the
         * Id's the client authenticated with.
         */
        public final Id AUTH_IDS = new Id("auth", "");//不使用任何id,代表任何已確認使用者。

以及在org.apache.zookeeper.server.auth.DigestAuthenticationProvider#handleAuthentication 定義的

new Id("super", "")//在這種scheme情況下,對應的id擁有超級許可權,可以做任何事情(cdrwa)

除了內建Id以外,還有內建的schema提供認證模式,但是沒有對應的預設id(因為都是動態提供的)

schema

除了上面內建Id定義的world和auth,super這三個schema,還有無固定id的內建的schema即驗證模式
驗證模式以及驗證方法通過AuthenticationProvider實現

4871751-8f6827c930f68e2a.png
AuthenticationProvider的三種實現

最終在ProviderRegistry中進行註冊

digest:Client端由使用者名稱和密碼驗證,譬如user:password,digest的密碼生成方式是Sha1摘要的base64形式
ip:Client端由IP地址驗證,譬如172.2.0.0/24
Sasl: 這個類定義了,但是並沒有註冊,我也並不清楚這個認證方式

下面對Id做一個總結

schema id 意義 備註
auth "" 不使用任何id,代表任何已確認使用者 自帶Id
world anyone 固定使用者,為所有Client端開放許可權 自帶Id
super "" 擁有超級許可權,可以做任何事情(cdrwa) 自帶Id
ip 無固定值,有固定格式(ip expression) IP驗證方式 自帶schema
digest 無固定值,有固定格式(digest expression) 使用者名稱和密碼驗證,再生成摘要 自帶schema
sasl 無固定值,有固定格式(sasl expression) sasl驗證方式,這個我並不是很懂 自帶schema

ACL的建立與修改

只有兩類API會改變Znode的ACL列表:一個是create(),一個是setACL()。
這兩個方法都要求傳入一個List。Server接到這兩種更新請求後,會判斷指定的每一個ACL中,scheme對應的AuthenticationProvider是否存在。
如果存在,呼叫其isValid(String)方法判斷對應的id表示式是否合法
具體參見PrepRequestProcessor.fixupACL()方法。

ACL的驗證

ACL建立修改時的驗證

只在create和setACL操作中涉及ACL的建立與修改,具體參見PrepRequestProcessor.fixupACL()

private boolean fixupACL(List<Id> authInfo, List<ACL> acl) {
        if (skipACL) {
            return true;
        }
        if (acl == null || acl.size() == 0) {
            return false;
        }

        Iterator<ACL> it = acl.iterator();
        LinkedList<ACL> toAdd = null;
        while (it.hasNext()) {
            ACL a = it.next();
            Id id = a.getId();
            if (id.getScheme().equals("world") && id.getId().equals("anyone")) {//如果是固定使用者,為所有Client端開放許可權
                // wide open
            } else if (id.getScheme().equals("auth")) {
                // This is the "auth" id, so we have to expand it to the
                // authenticated ids of the requestor
                it.remove();//如果是auth,把這個從acl的List中刪掉
                if (toAdd == null) {
                    toAdd = new LinkedList<ACL>();
                }
                boolean authIdValid = false;
                for (Id cid : authInfo) {
                    /*
                    一般情況下,預設的Id只有IP這一種(org.apache.zookeeper.server.NIOServerCnxn.NIOServerCnxn),裡面呼叫了
                    authInfo.add(new Id("ip", addr.getHostAddress()));
                     */
                    AuthenticationProvider ap =
                        ProviderRegistry.getProvider(cid.getScheme());
                    if (ap == null) {
                        LOG.error("Missing AuthenticationProvider for "
                                + cid.getScheme());
                    } else if (ap.isAuthenticated()) {//如果驗證過了,三種實現中,IP返回false,其他兩種返回true
                        authIdValid = true;
                        toAdd.add(new ACL(a.getPerms(), cid));
                    }
                }
                if (!authIdValid) {
                    return false;
                }
            } else {//其他認證模式的話,如ip,digest,sasl
                AuthenticationProvider ap = ProviderRegistry.getProvider(id
                        .getScheme());
                if (ap == null) {
                    return false;
                }
                if (!ap.isValid(id.getId())) {//如果id的格式不valid
                    return false;
                }
            }
        }
        if (toAdd != null) {
            for (ACL a : toAdd) {
                acl.add(a);
            }
        }
        return acl.size() > 0;//確保有一種方式認證通過了
    }

簡而言之,這個函式就是看設定的ACL值是否合理,基本過程如下

1.如果acl列表有("world","anyone"),那麼一定認證通過
2.上述情況外,如果是Id的schema是"auth",那麼要看請求攜帶的authInfo是否是isAuthenticated的,是的話認證通過
3.上述情況外,一般就是“ip”,"digest","sasl",呼叫對應認證提供器的isValid方法校驗id內容格式是否valid,是的話認證通過

例項分析

下面分析一個用"auth",""這個Id建立節點出現的異常

背景

String path1 = zk.create("/test21", "asd".getBytes(),
                    ZooDefs.Ids.CREATOR_ALL_ACL,
                    CreateMode.EPHEMERAL);

其中CREATOR_ALL_ACL定義在org.apache.zookeeper.ZooDefs.Ids#CREATOR_ALL_ACL中

public final ArrayList<ACL> CREATOR_ALL_ACL = new ArrayList<ACL>(
                Collections.singletonList(new ACL(Perms.ALL, AUTH_IDS)));//用到了"auth",""

出現了異常

org.apache.zookeeper.KeeperException$InvalidACLException: KeeperErrorCode = InvalidACL for /test21

原理分析

NIOServerCnxn#NIOServerCnxn只給request的authInfo加了"ip"這個schema
PrepRequestProcessor#fixupACL中,處理邏輯如下

4871751-f549ce7ab899dddb.png
異常分析

也就是說("auth","")應該和sasl或者digest這種schema配合起來使用才行,這裡就深究如何使用"auth",""了

申請許可權時的驗證(以create為例)

應該會在後面處理鏈的時候講,這裡帶過一下
比如在parentNode中進行createNode操作,參見
org.apache.zookeeper.server.PrepRequestProcessor#pRequest2Txn 中 case OpCode.create
呼叫了

checkACL(zks, parentRecord.acl, ZooDefs.Perms.CREATE,
                        request.authInfo);//驗證是否有create許可權

checkACL函式如下

/**
     *
     * @param zks
     * @param acl 對應節點或者父節點擁有的許可權
     * @param perm 目前操作需要的許可權
     * @param ids 目前請求提供的許可權
     * @throws KeeperException.NoAuthException
     */
    static void checkACL(ZooKeeperServer zks, List<ACL> acl, int perm,
            List<Id> ids) throws KeeperException.NoAuthException {
        if (skipACL) {//如果跳過ACL
            return;
        }
        if (acl == null || acl.size() == 0) {//如果沒有要求的ACL
            return;
        }
        for (Id authId : ids) {
            if (authId.getScheme().equals("super")) {//如果提供的ACL有超級許可權
                return;
            }
        }
        for (ACL a : acl) {
            Id id = a.getId();
            if ((a.getPerms() & perm) != 0) {//如果對應的節點擁有perm許可權
                if (id.getScheme().equals("world")
                        && id.getId().equals("anyone")) {
                    return;//如果請求提供了超級許可權
                }
                AuthenticationProvider ap = ProviderRegistry.getProvider(id
                        .getScheme());//根據策略模式獲取對應的認證提供器
                if (ap != null) {
                    for (Id authId : ids) {//用認證器一個個 認證 請求提供的Id
                        if (authId.getScheme().equals(id.getScheme())
                                && ap.matches(authId.getId(), id.getId())) {//模式相同並且匹配通過
                            //這要有一個匹配通過就行
                            return;
                        }
                    }
                }
            }
            //如果對應的節點都沒有要求的perm許可權,那就驗證失敗,和請求提供什麼許可權無關
        }
        throw new KeeperException.NoAuthException();
    }

簡而言之,就是當前節點acl包含當前操作許可權perm,並且當前節點acl能夠認證通過請求提供的ids許可權(有一個認證通過就行)

思考

注意上面描述的Id與id的區別

在Id這個類中,有id這個屬性,注意大小寫

ACL與Id的關係

ACL包含perms與Id
Id包含Id和schema

("super","")和("world","anyone")許可權比較

從上面的checkACL函式來講,先遇到("super","")就return
實際上,("super","")並沒有分配許可權,就像是程式開的後門,遇到了這個Id,就通行。

而("world","anyone"),還進行了perm的分配,有對應的ACL,參照ZooDefs.Ids#ANYONE_ID_UNSAFE
在上面checkACL函式中,該Id還受限於

if ((a.getPerms() & perm) != 0)

也就是說,原來的節點許可權不包含當前需要的perm許可權時,("world","anyone")也沒用
所以結論就是*** ("super","")許可權更大***

許可權的建立修改,以及申請許可權的驗證

在對節點進行create和setACL時涉及許可權的建立和修改,主要驗證acl列表的合理性
在org.apache.zookeeper.server.PrepRequestProcessor#fixupACL判斷

在對節點進行操作時,需要驗證當前請求以及相關節點是否有對應的許可權
在org.apache.zookeeper.server.PrepRequestProcessor#checkACL判斷

問題

建立最後將ACL資訊儲存在znode狀態中,這是怎麼實現的?
這個在後面請求鏈中再看

(auth,"")這個Id到底該怎麼配合digest或者sasl使用,沒有深究

吐槽

Id對應常量的地方有點亂,比如super定義在DigestAuthenticationProvider中

AuthenticationProvider 三個實現類的#isValid都沒有註釋
要自己看才知道對應schema該寫的id(即驗證內容)的格式應該是怎麼樣的

(auth,"")這個Id,使用說明太少了,沒看到demo也沒見到合理的資料

refer

主要參照 http://www.cnblogs.com/linuxbug/p/5023677.html
認證提供與註冊的相關介紹
一些定義
http://aliapp.blog.51cto.com/8192229/1327674
《從paxos到zookeeper》第7章

相關文章