簡介
權限存取設計是在開發 Application 中相當棘手的問題。
在網站開始建設的初期,通常這樣的問題並不會浮現,畢竟一般人的需求大半隻會有 user 和 admin 兩種角色。但是隨著網站演化,更多的業務需求浮現,第三種角色的出現,通常就會把原本乾淨的 code 弄得骯髒不堪。
多種角色的權限設計難題
當只有 user 和 admin 的情況下,你可以在 view 裏面單純的做出這樣的設計
<% if user.is_admin ? %>
<%= link_to("Admin Pannel", admin_panel_path ) %>
<% end %>
並且在 controller 裏面加上權限判斷
class Admin::ArticleController < ApplicationController
before_filter :require_is_admin
end
但一段時間之後,出現這樣的需求:
- 使用者可以被設定爲「editor」
- 擁有「editor」角色的使用者,可以進入 admin 後臺發表、編輯文章
- 擁有「edtior」角色的使用者,進入 admin 後臺內的活動範圍僅限縮在文章後臺內
- 擁有「edtior」角色的使用者,進入 admin 後臺內,不可以看到其他後臺選項。
身爲開發者的你,要如何在現有後臺內加入這樣的設計?
不用實際動手寫也知道,若如以往使用 if / else 的設計,Helper / Controller / View 鐵定變成一團血肉模糊。
抱怨不能解決問題,但世界上是否存在乾淨的解答?
答案就是:「Rule Engine」。
Rule-engine based authorization library: Cancan
「針對多種條件執行多種動作」,此類的需求,無論是使用 if / else,甚至是 case when,架構還是不免會一團混亂。與其承襲舊思路,不如啓用新想法 - 「Rule Engine」:預先設計撰寫一套邏輯規則引擎,而後程序針對預設的規則進行邏輯判斷後執行。
而「角色權限」的設計需求上,正特別適合用 Rule Engine 這樣的觀念去建構。Rails 中的授權庫(authorization library) cancan 正是以Rule Engine作爲基礎。
Cancan的特點1: 接口簡單,其把權限判定邏輯從 Helper / Controller / View 中, 移到 app/models/ability.rb,從而實現
- View 只需要判斷是否可以執行動作,而不必問是否有權限
<% if can? :update, @article %>
<%= link_to "Edit", edit_article_path(@article) %>
<% end %>
- Controller 無需手動判斷權限
class ArticlesController < ApplicationController
authorize_resource
def show
# @article is already authorized
end
end
實際上,權限判斷在view和controller通常是雷同的,分散處理常常故此失彼。所以,Cancan的第二個特點是: 權限中心化管理, 即將權限存取,則全交給 app/models/ability.rb 去判斷處理。
class Ability
include CanCan::Ability
def initialize(user)
if user.blank?
# not logged in
cannot :manage, :all
basic_read_only
elsif user.has_role?(:admin)
# admin
can :manage, :all
elsif user.has_role?(:member)
can :create, Topic
can :update, Topic do |topic|
(topic.user_id == user.id)
end
can :destroy, Topic do |topic|
(topic.user_id == user.id)
end
basic_read_only
else
# banned or unknown situation
cannot :manage, :all
basic_read_only
end
end
protected
def basic_read_only
can :read, Topic
can :list, Topic
can :search, Topic
end
end
使用Cancan 的限制:RESTful controller (resource)
初學者不太理解 cancan 兩個接口函數:load_and_authorize_resource
、authorize_resource
此外,cancan 也沒在Readme中說明其架構的限制,即:
- 必須爲 RESTful resource (cancan 直接假設了你一定使用 RESTful,畢竟這年頭誰還在寫 non-RESTful …?)
- resource 必須與 Controller 同名(@article 與 ArticlesController)
這兩條限制,也是Rails默認的配置,所以,使用過 cancan 的人,大概都「猜到」規則好像是這樣?
其實,其source code 裏面就寫的很清楚
load_and_authorize_resource
load_and_authorized_resource
做了兩件事:
def load_and_authorize_resource
load_resource
authorize_resource
end
load_resource
authorize_resource
load_resource
是load_resource_instance
的符號別名(:loard_resource
=> load_resource_instance
)
def load_resource_instance
if !parent? && new_actions.include?(@params[:action].to_sym)
build_resource
elsif id_param || @options[:singleton]
find_resource
end
end
okay,這段代碼的意思是如果你在 Controller 裏面下加入load_resource
,cancan
會自作聰明的幫你 自動 在每一個 action中加一個instance 。
class ArticlesController < ApplicationController
load_resource
def new
end
def show
# @article is already loaded
end
end
如果是 new 這個 action,效果會等於
def new
@article = Article.new
end
如果是 show 這個 action,效果會等於
def show
@article = Article.find(params[:id])
end
有好處也有壞處,好處是…你不需要自己打一行 code,壞處就是不熟 cancan 的人,找不到 @article 在哪裏會驚慌失措…
load_resource
還有一些其他進階用法,詳細介紹見controller_additions.rb
。
authorize_resource
authorize_resource
就是對 resource 判斷權限(根據
CanCan::Ability 裏的權限表)。
而這個 resource 必定是與同名的 instance。
如果是 ArticlesController 對應的必然是 @article。
但是你會想說這樣慘了?萬一我在 ArticlesController 裏面要用 @post 怎麼辦呢?
可以在 controller 裏面指定 資源實例的名字: authorize_resource
:post
class ArticlesController < ApplicationController
authorize_resource :post
def new
@post = Article.new
end
def show
@post = Article.find(params[:id])
end
end
Ability 代碼如下:
can :read, Post
can :create, Post
can :update, Post
resource 規則小結
所以 cancan 裏面的 resource 第一個會去喫 controller 的名稱當成 resource name,如果是 ArticlesController,instance 就會是 @article,而在 ability 裏面就會是 can :read, Article。這是在假設你已經使用同名設計 resource & controller 的情況下。
如果非同名。你可以做出指定:authorize_resource :post,雖然是 ArticlesController,但是這一組的 resource 名稱爲 post,所以 instance 就會是 @post,而在 ability 裏面就會是 can :read, Post。
一般開發者常會誤會的是
- ability 會綁到 model,實際上不是
- controller 名稱要與 @instance 名稱相同,實際上不一定
- @instance 要與 model 同名,實際上不用
- ability 喫的應該是 controller name,實際上不一定(喫的是 resource name,且可以被指定)。
Cancan 喫的是 resource,而且自作聰明的假設了大家「應該」都同名,而且 README example 也是使用「同名」,纔會造成了這麼多的誤解…
如果你有更多疑問,可以直接看 controller_resource.rb
的元代碼,
相信會讓你對整個架構更加的清楚… 下面介紹ability的設計。
角色判斷 current_ability
這是一段普通的 ability.rb 權限範例 code。
class Ability
include CanCan::Ability
def initialize(user)
if user.blank?
# not logged in
cannot :manage, :all
basic_read_only
elsif user.has_role?(:admin)
# admin
can :manage, :all
end
end
protected
def basic_read_only
can :read, Topic
can :list, Topic
can :search, Topic
end
end
一般開發者最有疑問的是def initialize(user)
這一段程序碼中的
user 到底是怎麼來的?怎麼會沒頭沒尾的天外飛來一個 user,然後對這個 user 進行角色判斷就可以動了?
這一段要追溯到lib/controller_additions.rb
中的這一段
current_ability。
cancan 裏面去判斷是否有權限的一直是 current_abibilty
,而 current_abibilty
initialize
的方式就是塞 current_user 進去。
def current_ability
@current_ability ||= ::Ability.new(current_user)
end
所以 initialize(user) 裏的 if user.blank? 其實就等於 if current_user.blank?(若沒登入)。
這樣去解讀程序碼,看起來就好理解很多了… 權限類別解說 :manage, :all, ..etc.
cancan 裏面用了一堆自定義縮寫,如 :manage、:read、:update、:all,讓人不是很瞭解在做什麼。
- :manage: 是指這個 controller 內所有的 action
- :read : 指 :index 和 :show
- :update: 指 :edit 和 :update
- :destroy: 指 :destroy
- :create: 指 :new 和 :crate
而 :all 是指所有 object (resource)
當然,不只是 CRUD 的 method 纔可以被列上去,如果你有其他非 RESTful 的 method 如 :search,也是可以寫上去..,只是要一條一條列上去,有點麻煩就是了。 組合技:alias_action
cancan 還提供了組合技,要是嫌原先的 :update, :read 這種組合包不夠用。還可以用 alias_action 自己另外再組。例如把 :update 和 :destroy 組成 :modify。
alias_action :update, :destroy, :to => :modify
can :modify, Comment
組合技: 定製method
要是你嫌每個角色都要一條一條把權限列上去,超麻煩。可以把一些共通的權限包成 method。用疊加 method 上去的方式列舉。比如把基礎權限都包成 basic_read_only、account_manager_only, etc…
def basic_read_only
can :read, Topic
can :list, Topic
can :search, Topic
end
針對物件狀態控管
在 User story 中,使用者固然 can :update, Topic,但還是讓人覺得覺得哪裏有點怪怪的?
是的。使用者應該只能編輯和修改屬於自己的文章,can :update, Topic 只有說使用者可以「修改文章」啊(等於可以修改所有文章) XD
所以 ability.rb 就要這樣設計了
can :update, Topic do |topic|
(topic.user_id == user.id)
end
can :destroy, Topic do |topic|
(topic.user_id == user.id)
end
可以玩的更加進階:
can :publish, Post do |post|
( post.draft? || post.submitted? ) && !post.published?
end
其他
cancan 還有其他進階主題可以繼續探討,讀者可以自行研究:
- Nested Resources
- Exception Handling
- Ensure Authorization
不過關於「難懂」和「難用」的部分,我想我應該講的差不多了…
小結
在寫這一系列文章時,我發現 cancan 的作者,其實把大部分的文件與範例,都寫在 lib/ 下的 RDOC 裏面了,光看 code comment 其實就可以瞭解大半流程。
不過我覺得 cancan 讓人覺得難讀的最大原因,可能還是官方缺乏一個 example ability.rb,對於被隱藏的自動完成部分也缺乏解釋,所以才造成大家覺得 cancan 是個難用的 magic library。事實上如果你開始搞懂 cancan 怎麼撰寫的話,它是可以幫你把網站的權限 code 處理的非常漂亮又易懂的。
後記
又積累了一項能力,cancan相關的。
簡介
權限存取設計是在開發 Application 中相當棘手的問題。
在網站開始建設的初期,通常這樣的問題並不會浮現,畢竟一般人的需求大半隻會有 user 和 admin 兩種角色。但是隨著網站演化,更多的業務需求浮現,第三種角色的出現,通常就會把原本乾淨的 code 弄得骯髒不堪。
多種角色的權限設計難題
當只有 user 和 admin 的情況下,你可以在 view 裏面單純的做出這樣的設計
<% if user.is_admin ? %>
<%= link_to("Admin Pannel", admin_panel_path ) %>
<% end %>
並且在 controller 裏面加上權限判斷
class Admin::ArticleController < ApplicationController
before_filter :require_is_admin
end
但一段時間之後,出現這樣的需求:
- 使用者可以被設定爲「editor」
- 擁有「editor」角色的使用者,可以進入 admin 後臺發表、編輯文章
- 擁有「edtior」角色的使用者,進入 admin 後臺內的活動範圍僅限縮在文章後臺內
- 擁有「edtior」角色的使用者,進入 admin 後臺內,不可以看到其他後臺選項。
身爲開發者的你,要如何在現有後臺內加入這樣的設計?
不用實際動手寫也知道,若如以往使用 if / else 的設計,Helper / Controller / View 鐵定變成一團血肉模糊。
抱怨不能解決問題,但世界上是否存在乾淨的解答?
答案就是:「Rule Engine」。
Rule-engine based authorization library: Cancan
「針對多種條件執行多種動作」,此類的需求,無論是使用 if / else,甚至是 case when,架構還是不免會一團混亂。與其承襲舊思路,不如啓用新想法 - 「Rule Engine」:預先設計撰寫一套邏輯規則引擎,而後程序針對預設的規則進行邏輯判斷後執行。
而「角色權限」的設計需求上,正特別適合用 Rule Engine 這樣的觀念去建構。Rails 中的授權庫(authorization library) cancan 正是以Rule Engine作爲基礎。
Cancan的特點1: 接口簡單,其把權限判定邏輯從 Helper / Controller / View 中, 移到 app/models/ability.rb,從而實現
- View 只需要判斷是否可以執行動作,而不必問是否有權限
<% if can? :update, @article %>
<%= link_to "Edit", edit_article_path(@article) %>
<% end %>
- Controller 無需手動判斷權限
class ArticlesController < ApplicationController
authorize_resource
def show
# @article is already authorized
end
end
實際上,權限判斷在view和controller通常是雷同的,分散處理常常故此失彼。所以,Cancan的第二個特點是: 權限中心化管理, 即將權限存取,則全交給 app/models/ability.rb 去判斷處理。
class Ability
include CanCan::Ability
def initialize(user)
if user.blank?
# not logged in
cannot :manage, :all
basic_read_only
elsif user.has_role?(:admin)
# admin
can :manage, :all
elsif user.has_role?(:member)
can :create, Topic
can :update, Topic do |topic|
(topic.user_id == user.id)
end
can :destroy, Topic do |topic|
(topic.user_id == user.id)
end
basic_read_only
else
# banned or unknown situation
cannot :manage, :all
basic_read_only
end
end
protected
def basic_read_only
can :read, Topic
can :list, Topic
can :search, Topic
end
end
使用Cancan 的限制:RESTful controller (resource)
初學者不太理解 cancan 兩個接口函數:load_and_authorize_resource
、authorize_resource
此外,cancan 也沒在Readme中說明其架構的限制,即:
- 必須爲 RESTful resource (cancan 直接假設了你一定使用 RESTful,畢竟這年頭誰還在寫 non-RESTful …?)
- resource 必須與 Controller 同名(@article 與 ArticlesController)
這兩條限制,也是Rails默認的配置,所以,使用過 cancan 的人,大概都「猜到」規則好像是這樣?
其實,其source code 裏面就寫的很清楚
load_and_authorize_resource
load_and_authorized_resource
做了兩件事:
def load_and_authorize_resource
load_resource
authorize_resource
end
load_resource
authorize_resource
load_resource
是load_resource_instance
的符號別名(:loard_resource
=> load_resource_instance
)
def load_resource_instance
if !parent? && new_actions.include?(@params[:action].to_sym)
build_resource
elsif id_param || @options[:singleton]
find_resource
end
end
okay,這段代碼的意思是如果你在 Controller 裏面下加入load_resource
,cancan
會自作聰明的幫你 自動 在每一個 action中加一個instance 。
class ArticlesController < ApplicationController
load_resource
def new
end
def show
# @article is already loaded
end
end
如果是 new 這個 action,效果會等於
def new
@article = Article.new
end
如果是 show 這個 action,效果會等於
def show
@article = Article.find(params[:id])
end
有好處也有壞處,好處是…你不需要自己打一行 code,壞處就是不熟 cancan 的人,找不到 @article 在哪裏會驚慌失措…
load_resource
還有一些其他進階用法,詳細介紹見controller_additions.rb
。
authorize_resource
authorize_resource
就是對 resource 判斷權限(根據
CanCan::Ability 裏的權限表)。
而這個 resource 必定是與同名的 instance。
如果是 ArticlesController 對應的必然是 @article。
但是你會想說這樣慘了?萬一我在 ArticlesController 裏面要用 @post 怎麼辦呢?
可以在 controller 裏面指定 資源實例的名字: authorize_resource
:post
class ArticlesController < ApplicationController
authorize_resource :post
def new
@post = Article.new
end
def show
@post = Article.find(params[:id])
end
end
Ability 代碼如下:
can :read, Post
can :create, Post
can :update, Post
resource 規則小結
所以 cancan 裏面的 resource 第一個會去喫 controller 的名稱當成 resource name,如果是 ArticlesController,instance 就會是 @article,而在 ability 裏面就會是 can :read, Article。這是在假設你已經使用同名設計 resource & controller 的情況下。
如果非同名。你可以做出指定:authorize_resource :post,雖然是 ArticlesController,但是這一組的 resource 名稱爲 post,所以 instance 就會是 @post,而在 ability 裏面就會是 can :read, Post。
一般開發者常會誤會的是
- ability 會綁到 model,實際上不是
- controller 名稱要與 @instance 名稱相同,實際上不一定
- @instance 要與 model 同名,實際上不用
- ability 喫的應該是 controller name,實際上不一定(喫的是 resource name,且可以被指定)。
Cancan 喫的是 resource,而且自作聰明的假設了大家「應該」都同名,而且 README example 也是使用「同名」,纔會造成了這麼多的誤解…
如果你有更多疑問,可以直接看 controller_resource.rb
的元代碼,
相信會讓你對整個架構更加的清楚… 下面介紹ability的設計。
角色判斷 current_ability
這是一段普通的 ability.rb 權限範例 code。
class Ability
include CanCan::Ability
def initialize(user)
if user.blank?
# not logged in
cannot :manage, :all
basic_read_only
elsif user.has_role?(:admin)
# admin
can :manage, :all
end
end
protected
def basic_read_only
can :read, Topic
can :list, Topic
can :search, Topic
end
end
一般開發者最有疑問的是def initialize(user)
這一段程序碼中的
user 到底是怎麼來的?怎麼會沒頭沒尾的天外飛來一個 user,然後對這個 user 進行角色判斷就可以動了?
這一段要追溯到lib/controller_additions.rb
中的這一段
current_ability。
cancan 裏面去判斷是否有權限的一直是 current_abibilty
,而 current_abibilty
initialize
的方式就是塞 current_user 進去。
def current_ability
@current_ability ||= ::Ability.new(current_user)
end
所以 initialize(user) 裏的 if user.blank? 其實就等於 if current_user.blank?(若沒登入)。
這樣去解讀程序碼,看起來就好理解很多了… 權限類別解說 :manage, :all, ..etc.
cancan 裏面用了一堆自定義縮寫,如 :manage、:read、:update、:all,讓人不是很瞭解在做什麼。
- :manage: 是指這個 controller 內所有的 action
- :read : 指 :index 和 :show
- :update: 指 :edit 和 :update
- :destroy: 指 :destroy
- :create: 指 :new 和 :crate
而 :all 是指所有 object (resource)
當然,不只是 CRUD 的 method 纔可以被列上去,如果你有其他非 RESTful 的 method 如 :search,也是可以寫上去..,只是要一條一條列上去,有點麻煩就是了。 組合技:alias_action
cancan 還提供了組合技,要是嫌原先的 :update, :read 這種組合包不夠用。還可以用 alias_action 自己另外再組。例如把 :update 和 :destroy 組成 :modify。
alias_action :update, :destroy, :to => :modify
can :modify, Comment
組合技: 定製method
要是你嫌每個角色都要一條一條把權限列上去,超麻煩。可以把一些共通的權限包成 method。用疊加 method 上去的方式列舉。比如把基礎權限都包成 basic_read_only、account_manager_only, etc…
def basic_read_only
can :read, Topic
can :list, Topic
can :search, Topic
end
針對物件狀態控管
在 User story 中,使用者固然 can :update, Topic,但還是讓人覺得覺得哪裏有點怪怪的?
是的。使用者應該只能編輯和修改屬於自己的文章,can :update, Topic 只有說使用者可以「修改文章」啊(等於可以修改所有文章) XD
所以 ability.rb 就要這樣設計了
can :update, Topic do |topic|
(topic.user_id == user.id)
end
can :destroy, Topic do |topic|
(topic.user_id == user.id)
end
可以玩的更加進階:
can :publish, Post do |post|
( post.draft? || post.submitted? ) && !post.published?
end
其他
cancan 還有其他進階主題可以繼續探討,讀者可以自行研究:
- Nested Resources
- Exception Handling
- Ensure Authorization
不過關於「難懂」和「難用」的部分,我想我應該講的差不多了…
小結
在寫這一系列文章時,我發現 cancan 的作者,其實把大部分的文件與範例,都寫在 lib/ 下的 RDOC 裏面了,光看 code comment 其實就可以瞭解大半流程。
不過我覺得 cancan 讓人覺得難讀的最大原因,可能還是官方缺乏一個 example ability.rb,對於被隱藏的自動完成部分也缺乏解釋,所以才造成大家覺得 cancan 是個難用的 magic library。事實上如果你開始搞懂 cancan 怎麼撰寫的話,它是可以幫你把網站的權限 code 處理的非常漂亮又易懂的。
後記
又積累了一項能力,cancan相關的。