PingCode Wiki 許可權設計之ACL

阿杰發表於2022-11-24

2021年 Wiki 加入了很多強硬的特性,其中包括協同編輯 、頁面許可權表情符號 等,這些功能給使用者帶來了更好的體驗。作為 Wiki 使用者兼開發者,今日來聊聊年終上線的頁面許可權,同時總結一下開發階段涉及到的技術、遇到的問題以及解決方案,關於許可權本人之前已經寫過一篇 Worktile 許可權的文章了,Worktile 許可權著重講了 RBAC(基於角色的許可權控制方案)的設計與實現,本文基於 Wiki 頁面許可權選擇的另一個主流許可權設計的方案:ACL

本文大致分為三部分:

  1. ACL 介紹
  2. 介紹我們的許可權以及為什麼選擇它
  3. 設計實現

一、ACL 介紹

1. 什麼是 ACL?

ACL:Access Control List,許可權控制列表,是對檔案以及目錄的許可權控制方案。大名鼎鼎的 Linux 許可權系統,它就是 ACL 的典型案例,本人在開發過程中也受到了 Linux 許可權設計的一些啟發。

2. ACL的使用場景

使用場景也可以換個問法:為什麼要使用 ACL ?關於這個問題我們還以 Linux 作為案例:Linux 本身只提供了Owner(所有者)、Group(使用者組)、Others(其他成員),也就是說其他成員或使用者組是無法指定更細粒度的許可權。

為了更好的解釋,我們來舉個簡單的例子(場景):

有 4 個成員有 A、B、C、D,其中 A、B、C 是開發組G的成員,A成員建立了一個程式碼倉庫並把團隊開發的程式碼放置到該目錄中,其中這些程式碼主要是關於G組的,與其他成員無關,所以A把檔案目錄設定了許可權,許可權是組內可讀可寫,其他人沒有任何許可權。

現在來分析一下各個角色,A 是倉庫的 Owner,G 是 Group(含B、C),D 是與該檔案無關的成員,所以是 Others。現在入職了一個使用者E,因為E是新人,所以不想讓E去操作程式碼,只允許他檢視熟悉程式碼。

面對這種場景,試想一下如何給 E 成員設定對應許可權呢?答案是 oh no,因為 E 既不能按照 G 組許可權,也不能按照 Others 許可權,更不能是 Owner!所以面對這種雞肋的許可權,ACL 就作為了其補充,ACL 可以支援針對某一個使用者或某個使用者組做獨立的許可權,完美解決了類似場景。

二、PingCode Wiki 許可權架構

OK,瞭解了什麼是 ACL 以及使用場景,我們來聊一下 Wiki 的許可權架構,基本架構見下圖。
image.png

1. 基礎許可權 RBAC——角色對應的許可權

1.1 許可權配置

image.png

1.2 檢視許可權

image.png

2. 頁面許可權ACL——針對使用者和使用者組的許可權

頁面許可權同樣是作為 RBAC 的補充,更加細粒度的許可權劃分,即給某部分人或某部分使用者組分配對應許可權。

2.1 頁面許可權設定入口

右上角選單——「更多-許可權設定」
image.png

2.2 獨立許可權與共享許可權

獨立許可權——「許可權設定-知識庫成員」
image.png
共享許可權——「許可權設定-非知識庫成員」
image.png
從我們的需求(針對使用者和使用者組部分人群)來看是符合 ACL 解決方案的。

三、設計與實現

到這裡的同學相信已經對 Wiki 許可權功能有了大致的瞭解,下面開始詳細介紹一下開發中的設計細節,關鍵點、難點以及方案的選用取與舍。

1. 關係模型與資料庫設計

瞭解基本需求,本人根據以往的開發經驗簡單的定了一個初步方案,見下圖方案A,後來對照原型和設計圖調整了一些細節,產生了方案B,見下圖。
A方案
image.png
B 方案(最終方案)
image.png
兩種方案的區別:

A 方案是關係型資料庫的常見設計,以使用者/使用者組為主導對映模組和許可權,對應關係為:多對多 使用者/使用者組(user/group) ——> 資源(pageId)
B 方案,以模組為主導對映使用者/使用者組,一對多:資源(pageId)——> 使用者/使用者組(scopes)

考慮到許可權是全量儲存的,正是由於這種互動方式所以調整出了 B 方案,而且服務端資料庫是 mongodb,支援陣列的特性,非常的契合。

2. 對應的程式實現

資源或模組(pageId)——> 使用者/使用者組(scopes),實體對應關係
image.png

欄位型別描述
scopesArray<Schema>許可權設定詳細
permission_typeEnum許可權型別
principalsScopeDesignation[]指定詳細
typeScopeDesignationType指定型別
idUIDlID人/使用者組/部門的唯一標識
valuenumber許可權值

許可權值計算參考了Linux,採用的是位運算:1、2、4、8...

  • 擁有的許可權值累計取最大mask值:只讀1 + 編輯 2,最終資料庫存3。
  • 判斷是否擁有某個許可權值:許可權值 & 預期的許可權值 = 存在/預期的許可權值。

    3. 約定 API

    還是由於儲存互動決定了新增、修改、刪除是一個 API。

  • 儲存(新增/修改/移除): {put}/api/wiki/spaces/:spaceId/pages/:pageId/permission$ 
  • 獲取: {get} /api/wiki/spaces/:spaceId/pages/:pageId/permission/:scopeType$ 

    4. 涉及的複雜場景

    許可權涉及的複雜場景非常的多,以下場景討論和決定經歷了很多波折,某些功能中途重做了兩次,迭代向後推遲了一週,兩次會議,共持續大約兩個半小時,由於文章篇幅,只列舉部分場景作為交流。

    4.1 許可權繼承(複雜場景)

    第一個難處理的點就是許可權繼承,確實很繞,相信很多產品都遇到過類似場景(具體是哪些這裡不贅述了),本人從技術和產品的角度都參考過 Linux 的處理,但由於系統級別和應用還是有些許不同,做了一些調整,下面羅列的是我們做過的方案。
    第一版:子頁面完全繼承父頁面且不可更改

  • 父頁面不可見,子頁面不可見
  • 父頁面只讀,子頁面編輯,最終子頁面是隻讀
  • 父頁面編輯,子頁面只讀,最終子頁面是編輯

不滿足的場景和邏輯的衝突點:

  • 父頁面允許檢視,預期某些子頁面允許檢視,某些子頁面不可檢視
  • 父頁面允許檢視但不許編輯,預期某些子頁面開放編輯許可權
  • 父頁面允許編輯,但某些子頁面不允許編輯

第二版:子頁面允許獨立許可權,預設繼承父許可權
頁面繼承邏輯:子頁面一旦設定,中斷與父頁面的繼承關係,中斷後許可權型別可以改

  • 父級頁面設定許可權後,子級頁面不設定許可權,預設繼承父級許可權;
  • 子級頁面設定許可權後,不再繼承父級許可權,形成自己單獨的頁面許可權

4.2 中介軟體邏輯

中介軟體涉及的場景也比較多,比如單頁面詳情檢視,拖拽移動以及跨知識庫移動,複製,刪除等等,核心的驗證邏輯如下圖。
image.png

4.3 頁面樹展示及許可權計算(難點)

4.3.1 展示邏輯

會議上,討論頁面樹的展示的可選方案如下:

  • 展示全部層級頁面,根據許可權可見內容(存在標題私密性問題,層級過多後,會看到多個不可見的標題,拋棄
  • 只展示有許可權的頁面,把子頁面提出來(層級結構打亂了,拋棄
  • 落地方案:只展示有許可權的頁面,當父頁面不可見時,子頁面可見時,把父頁面展示出來,如果沒有可見的子頁面時父頁面也不可見(方案二)---父頁面不可見的標題用灰色展示(點選效果保留),點選後右側無許可權頁面
  • 直接隱藏父與子(已實現方案一,僅能透過通知檢視,入口少,改變繼承關係後,不太適用,拋棄

最終確定的是一個折中方案,如果子頁面有許可權,那麼會把父頁面也展示出來,保證樹結構的完整性,若當前頁面和子頁面都沒有許可權,那麼就過濾掉。如下圖
image.png

4.3.2 許可權計算(技術難點)

難點:

  1. 頁面樹涉及到繼承
  2. 全量的計算(弄不好需要遞迴處理),所以程式計算複雜度要考慮
  3. 過濾邏輯(子有檢視許可權,父一定展示)

解決手段

  1. 資料庫儲存 parent 和 parent_ids 欄位,parent_ids 按照樹的層級拍平排序,比如樹的層級是 A->B->C,parent_ids 儲存的是 [A,B,C]。
  2. 把列表資料分割成兩組,一組是所有的頁面,另一組是所有作為了父頁面的,具體怎麼查詢只需要驗證parent_id 是否存在即可,屬於資料庫操作。
  3. 把 List 資料結構轉換成 Map 或 Set,減少迭代次數來降低複雜度。

具體處理邏輯
準備工作:

  1. 從資料庫中取出當前知識庫的頁面列表資料和頁面對應的 ACL 許可權列表
  2. 將頁面列表拍平轉換為Map(為了下面減少複雜度)
  3. 將許可權列表拍平轉換為 Map(目的同上)
  4. 取出 RBAC 的知識庫許可權
  5. 用來處理子頁面可見,父頁面不可見,而需要展示的父頁面集合 Set(在下面會詳細講解具體用處)
  6. 最終要 頁面列表(含許可權值)

如下圖(為了方便後續的講解,特標註序號)
image.png
處理流程,如下圖:
image.png

  1. 迭代頁面列表
  2. 取出一個頁面,找對應許可權
  3. 如果找到了設定的對應許可權,那麼合併 RBAC 許可權放入最終的結果集,沒有找到執行下面邏輯a. 透過當前頁面的 parent_ids 按倒序迭代(上面解決手段已講解 parent_ids 的排列順序)
    b. 如果沒有父級或所有父級均沒有許可權,則繼續最外層迭代,反之取出最近有許可權的父級頁面,並且將父級頁面與當前頁面之間的頁面放入 ⑥ (Set)中,這塊大家可能有疑問,舉個例子:假設有3個頁面,A、B、C,C 的父級是 B,B 的父級是 A,當前迭代的是 C,C 沒有許可權,B也沒有許可權,A有許可權,那麼最終將 A、B 推入 Set 中,C 由於繼承邏輯 也擁有許可權。
  4. 將 Set 中的父級頁面補充到最終的結果集
  5. 繼續迭代,直至完畢

最後發現 ⑥ 是為了補充缺失的一部分資料:子頁面可見,父頁面不可見,但要展示的父頁面
大家可能從上面環節還有一個疑問就是為什麼要合併 RBAC 知識庫許可權,請接著看。

4.4 許可權相關特殊處理

上面談到了已經驗證出頁面許可權,但還是合併了知識庫許可權,是因為產品邏輯中有以下規定:

  1. 知識庫沒有編輯許可權,頁面許可權有,那麼最終有編輯許可權,所以要覆蓋知識庫的編輯許可權。
  2. 擁有隻讀許可權的成員不能執行的操作:
  • 不能建立子頁面
  • 不能刪除當前頁面
  • 不能移動當前頁面
  • 不能移動至只讀的頁面下(對目標頁面來說實際上是新增頁面)
  • 不能複製頁面
  • 不能複製至只讀頁面下
  • 不能釋出頁面
  • 不能重新命名頁面
  • 不能進入編輯頁面

注意的是切勿漏掉場景。

4.4.1 個人對後續的設想

針對於這些特殊處理,個人感覺終究不是解決方案,而且後續新加或移除許可權,都要兼顧多處,技術角度維護起來困難,也容易漏掉,所以設想頁面也加入更多許可權點,讓使用者來主動勾選所需要和禁止的許可權,技術上就可以統一一套邏輯。

五、寫在最後

上面只是列舉了一些通用和些許複雜的場景,實際開發中還有很多的細節,所以開發期間很忙,經歷了頭腦風暴,也有走誤區的及時調整和反思,這些都是寶貴的經驗。

最後談談開發至今的一些感悟:

  • 團隊之間及時溝通,充分理解需求,建立統一認知,避免把煙囪做成水井的糗事,也減少互相甩鍋的情況,對團隊有良好的作用,值得一提的是Wiki團隊秉承著這一優良習慣,與 Wiki 這款產品的價值觀相契合 。
  • 技術人員要積極考慮需求,優先站在使用者和產品角度思考,其次再考慮技術,切勿過於執著技術,卻忽略了使用者和產品的初衷。
  • 技術人員對每一行程式碼負責,別把技術債留給自己或別人,程式碼終將會回饋團隊和你自身。
  • 要做好細節,作為還是技術都要做好細節,“剛”到可能是細節決定的成敗。

到這裡結束了,非常感謝每個看到這裡的同學 ,有任何疑問歡迎討論交流。

歡迎關注 Wiki,關注 PingCode 研發中心


最後,推薦我們的智慧化研發管理工具 PingCode 給大家。

PingCode官網

關於PingCode

PingCode是由國內老牌SaaS廠商Worktile 打造的智慧化研發管理工具,圍繞企業研發管理需求推出了Agile(敏捷開發)、Testhub(測試管理)、Wiki(知識庫)、Plan(專案集)、Goals(目標管理)、Flow(自動化管理)、Access (目錄管理)七大子產品以及應用市場,實現了對專案、任務、需求、缺陷、迭代規劃、測試、目標管理等研發管理全流程的覆蓋以及程式碼託管工具、CI/CD流水線、自動化測試等眾多主流開發工具的打通。

自正式釋出以來,以酷狗音樂、商湯科技、電銀資訊、51社保、萬國資料、金鷹卡通、用友、國汽智控、智齒客服、易快報等知名企業為代表,已經有超過13個行業的眾多企業選擇PingCode落地研發管理。

相關文章