用 Pundit / Cancancan 做動態權限管理
Rails 有兩個 gem 在處理權限管理,分別是 Pundit 跟 Cancancan,自己的公司是使用 Cancancan,朋友的公司是使用 pundit,都剛好是用 DB 的資料來做權限控管,想要自己試看看用起來手感如何
如果想要看 code 的話可以參考我的 repo
其中又把兩種不同的方式分成兩個 branch 來做
feature/pundit-permission-system
是使用 pundit 做的, feature/cancan-permission-system
是使用 cancancan 做的
Outline
Pundit
Pundit 的基本使用方式參考 官方repo
DB 設計
在這個設計裡面,一個 User 歸屬於一個 UserGroup
UserGroup 裡面的 admin 為 boolean,決定在這個 group 裡面的使用者有沒有 admin 權限
PermissionResource 對應到每個不同的要做權限控管的資源,如果是對於 Book model 的權限控管,在這裡的 name 就是 Book
default priority 則是紀錄這個資源預設對大家是可讀還是可寫還是 disable
UserGroup 跟 PermissionResource 之間則是多對多關聯,中間的 group_permission 紀錄了某 UserGroup 對於某 resource 的存取權,紀錄在 priority 欄位裡面
code 說明
model
在 create permission resource 時,default_priority 只有 disable / read / write 三種
然後每增加一個 resource 就會 trigger 自動幫所有 user_group 都加上 default priority
1 | # app/models/permission_resource.rb |
GroupPermission 這邊加了一個 scope,要撈 enable 的 permission 會撈 priority > 0 的,也就是非 diable 的
這裡有做一個 before create 的 hook,要搭配前面的機制,在 create 的時候 priority 自動加上 permission_reosurce 自己設定的 default priority
1 | # app/models/group_permission.rb |
policy
原本的使用方式是,根據每個不同的 model 做出對應的 policy 做權限管理 ex PostPolicy
對應到 Post
model
但現在如果要動態產生,就會把所有 policy 做的事情集中到 ApplicationPolicy
來做
其中的重點在 permissions
這個 method,他會撈這個 user 所有的 enabled_permissions,也就是他歸屬的 user_group 所有的非 diable 的 group_permission
再透過 group_permission 撈出,permission_reosurce 的 name,看對這個 resource 有沒有 read 或者 write 權限,這邊的一個假設是只要不是 disable 都有 read 權限
另外因為 pundit 不像 cancancan 有一些 mapping,這邊的 mapping 就是自己寫: ex writable => [:create, :new, :update, :edit, :destroy]
1 | # app/policies/application_policy.rb |
有點討厭的是如果要對 Book 這個 resource 管控,還是要把這個 policy 寫出來,不然可能要考慮 metaprogramming 的方式
1 | # app/policies/book_policy.rb |
controller
在目前比較簡單的示範中,我們假設 controller 的命名也都符合 model 的命名,所以在每個 controller 的 action 都先進行權限確認
1 | class AuthedController < ApplicationController |
如果權限不符合,預設 pundit 會 raise Pundit::NotAuthorizedError
這個 error,所以可以把他抓起來
1 | class ApplicationController < ActionController::Base |
view
決定按鈕要不要出現可以搭配 policy helper 使用
1 | <% if policy(@books).writable? %> |
Cancancan
Cancancan 的基本使用方式參考 官方repo
DB 設計
在這個設計裡面,User 跟 Team 是多對多,Team 跟 PermissionGroup 是多對多,PermissionGroup 跟 Permission 是多對多,然後一個 ApplicationResource has_many permissions
ApplicationResource 代表一個資源,他的 actions 裡面代表可以對這個資源做哪些操作,通常對應到 controller 裡面的 action, ex. [:index, :new]
而因為 cancancan 這個 gem 又已經對 controller 的 action 做了下面這樣的 mapping:
1 | read: [:index, :show] |
所以 application resource 紀錄的 action 也通常是這四種
Permission 則代表對這個 resource 可以操作的一種權限,比方說對 Book 這個 resource read 跟 read + write 可以分成兩種不同的權限
比較特別的是 permission 裡面的 allowed_actions 用 bitmask 做紀錄,而他對應到的屬性是根據 belongs_to 的 application resource 的 actions 決定,所以 如果 permission A 跟 permission B 對應到的 application ressource 不同,就算他們的 allowed_actions 都是 1,那他們代表的意義也可能不同
最後 team 跟 permission_group 的多對多,其實是可以只留下 team 或者只留下 permission_group,他們之間的區隔有點模糊,可能根據實際應用的例子可以考慮留下其中一個 model 即可,但這樣做有個彈性是,之後每個 user 也可以自己擁有 permission_group,而不屬於 team 底下
code 說明
model
我們需要以 User 為起點,拿到這個用戶的權限,他背後串連著多個 model
首先 ApplicationResource
裡面實作 fetch_all_resources
這個 class method
1 | # app/models/application_resource.rb |
上面的結果會拿到像這樣子的資料結構,說明每個 resource 有哪些權限可以使用:
1 | { |
Permission
則有一個 class_method fetch_all_permissions
,利用上面的資料結構,要把所有的 permission 代表的權限表示出來,中間有一些處理 Bitmask 的操作不是很重要:
1 | # app/models/permission.rb |
上面的結果會拿到像這樣子的資料結構
1 | # hash[permission.id] = { |
最後在 User 這個 model 就可以利用上面的產物,還有這個 user 屬於哪個 team,這個 team 有哪些 permission_group 去拿到屬於這個 user 的 permission
1 | # app/models/user.rb |
最後的產物會是像這樣:
1 | [ |
ability
跟 pundit 不同於需要定義不同的 policy class 去定義權限,原本 cancancan 就是把權限集中在 Ability 這個 class 上面
因此就可以在這個 class 上面定義對某一 user 的權限,我們透過 user 的 all_abilities
method 去拿到這個 user 的所有權限
1 | class Ability |
controller
cancancan 在 controller 有個很好用的 helper method authorize_resource
,在做每個 action 之前就會幫你做權限的檢查
1 | # app/controllers/books_controller.rb |
view
相對於 pundit 在 view 裡面呼叫 policy 確認權限, cancancan 則是用 can?
這個 helper method
1 | # app/views/books/index.html.erb |
Conclusion
因為 DB 設計的部分兩邊是可以共用的,所以不對這方面多做評論
在 gem 的使用限制方面,Pundit 需要對不同的 Resource 都做出相對應的 policy,但 Cancancan 不用,如果要做像這樣動態的定義,Cancancan 比較方便
另外 Cancancan 預設就有對一些 Restful 的 action 做一些 簡單的 mapping,讓 code 顯得不那麼囉唆,而且對於 Cancancan 的定義 permission 的方式使用正面表列(ex. can(['read','write'], 'Book')
),我會覺得比較單純一些
References
- 讀書會的朋友劭方的分享,感謝他
- 公司專案的 code