Cancan權限角色設計的最佳實踐



15 November 2014

簡介

權限存取設計是在開發 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_resourceauthorize_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_resourceload_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相關的。

15 November 2014

簡介

權限存取設計是在開發 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_resourceauthorize_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_resourceload_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相關的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章