権限管理用のgemもいくつかあるみたいですが、今回は機能を自作してみました。
Rubyならビットフィールドを使わずともハッシュで事足りるんでしょうけど、
こっちのほうがちょっとスッキリするかも…?気持ち的にも。
目指すところ
特定の権限を持つユーザーにだけリンクを表示したい場合
<% if current_user.permission?('hoge') %>
<%= link_to 'link_to_hoge', hoge_path %>
<% end %>
特定の権限を持つユーザーだけアクセスできるようにしたい場合
(権限を持たなければ403エラー(Forbidden)にする)
class HogeController < ApplicationController
include CheckPermission
before_action :authenticate_user!
before_action -> { check_role_permission!('hoge') }
.....
end
それから…
- ユーザーの種類(ロール)ごとに付加データを持たせたい
- フォームでの表示名
- DB保存用のロールID
- ...
- 権限の変更を簡単にできるようにしたい
- 権限の種類が増えてきてもスッキリ書きたい
- ビット演算を使いたい!
ビットフィールドまわりの説明
ロールは、各権限の有無を表す数値を持つ。(0b***はRubyの2進数表記)
- 管理者:
0b1111_1111_1111_1111
- 一般 :
0b0010_1101_0001_0000
- ...
各ビットは、権限の有無を表している。
0ビット目はユーザー情報閲覧権限、1ビット目はユーザー情報編集権限、
2ビット目は顧客データの閲覧権限…など。
指定したビットが立っているか(権限を持つか)調べたい場合は、
そのビットだけ1になっている値との&
をとって0かどうかを調べればよい。
(&
をとると、両方のビットが1のときのみ1,それ以外のビットは0になる。)
たとえば顧客データの閲覧権限(2ビット目)を持つか調べたい場合は、
0b1111_1111_1111_1111
& 0b0000_0000_0000_0100
-----------------------
0b0000_0000_0000_0100 (!= 0)
0b0010_1101_0001_0000
& 0b0000_0000_0000_0100
-----------------------
0b0000_0000_0000_0000 (== 0)
といった具合。
作ってみる
用意するGem
plataformatec/devise(認証機能)
binarylogic/settingslogic(定数管理)
権限設定を書く
settingslogicの設定ファイルに。
一旦形を作っておけば後からどんどん追加、変更ができる。
桁数が多くなってきたら4ビットぐらいで区切ると見やすい。
<<
はシフト演算子で、指定した数だけビットを右にシフトしてくれる。
たとえば、1<<2
=> 0b0000_0001 << 2
=> 0b0000_0100
# 権限設定
permission:
#reserved: <%= 1 << 7 %> # 予約
announcement: <%= 1 << 6 %> # お知らせ記事投稿
item_write: <%= 1 << 5 %> # 商品情報編集
item_read: <%= 1 << 4 %> # 商品情報閲覧
customer_write: <%= 1 << 3 %> # 顧客情報編集
customer_read: <%= 1 << 2 %> # 顧客情報閲覧
user_write: <%= 1 << 1 %> # ユーザー情報編集
user_read: <%= 1 << 0 %> # ユーザー情報閲覧
# ロール設定
role:
admin:
name: 管理者 # ロール表示名
id: 10 # ロールID
permission: <%= 0b0111_1111 %> # 権限設定値
normal:
name: 一般ユーザー
id: 20
permission: <%= 0b0101_0000 %>
ユーザーテーブルにロールIDを持たせる
class AddUserToRoleID < ActiveRecord::Migration
def change
add_column :users, :role_id, :integer
end
end
権限チェックできるようにする
ついでに付加データも取れるようにしたり。
# ロールの権限をチェックする
def permission?(key)
role = __role_data
permission = role.present? ? role[1]['permission'] : 0
return (permission & Settings.permission[key]) != 0 # 上で説明したビット演算!
end
# ロール名を取得する
def role_name
role = __role_data
role.present? ? role[1]['name'] : nil
end
# ロールIDを収得する
def role_id
role = __role_data
role.present? ? role[1]['id'] : -1
end
private
# ロールのデータを取得する
def __role_data
Settings.role.detect { |_, val| val['id'] == self.role_id }
end
最初に書いたこれができるようになった!
<% if current_user.permission?('hoge') %>
<%= link_to 'link_to_hoge', hoge_path %>
<% end %>
権限によるアクセス制限ができるようにする
いろんなcontrollerファイルで使うと思うので、concernsにモジュールとして作成する。
module CheckPermission
extend ActiveSupport::Concern
# 権限があるかをチェック
def check_role_permission!(key)
render file: 'public/403.html', status: :forbidden unless current_user.permission?(key)
end
end
最初に書いたこれができるようになった!
class HogeController < ApplicationController
include CheckPermission
before_action :authenticate_user!
before_action -> { check_role_permission!('hoge') }
.....
end
ちなみに、特定のアクションのみ制限したいときは、
before_action -> { check_role_permission!('hoge') }, only: [:new, :edit]
といった感じで書ける。
参考
権限管理用のgemについて
rails 4 対応の認可の gem
deviseについて
Rails4 にて Devise でユーザー登録・ログイン認証・認可の機能を追加
settingslogicについて
Railsで定数を一元管理する(settingslogic)
concernsについて
ActiveSupport::Concern でハッピーなモジュールライフを送る