TL;DR
権限判断
admin = 1 # 0001
editor = 2 # 0010
observer = 4 # 0100
my_roles = 3 # 0011
my_roles & admin == admin # true
my_roles & editor == editor # true
my_roles & observer == observer # false
# So `my_roles` Has `admin` and `editor`
Scopeで検索
class User < ApplicationRecord
# ...
scope :has_role, lambda { |role|
where('roles_code & ? = ?', role, role)
}
scope :roles_equal, lambda { |roles|
where(roles_code: roles.reduce(:|))
}
# ...
end
# Eg:
# User.has_role('0001'.to_i(2)).has_role('0100'.to_i(2))
# > User Load (0.6ms) SELECT "users".* FROM "users" WHERE (roles_code & 1 = 1) AND (roles_code & 4 = 4)
#
# User.roles_equal([1, 2])
# > User Load (4.9ms) SELECT "users".* FROM "users" WHERE "users"."roles_code" = $1 [["roles_code", 3]]
はじめに
Railsでは、ユーザーに複数の権限や商品に複数のタグを付けたい時、has_and_belongs_to_many
を使って多対多のリレーションを作成することが多いと思います。
しかし多対多リレーションは中間テーブルを必要で、DBに必要以上多量なデータを保存してしまい、検索する際も非効率です。
そこでヒントになったのはUnix/Linuxの権限の持ち方でした。
*nix系システムでは、755
, 600
など、8進数3個でオーナ-
、グループ
、その他
のロールにそれぞれ8種類の権限を表示しています。
例えば権限600
のファイルは「オーナのみ読み書きができる(rw-------
)」を意味しています。
そしてIPアドレスも同じ考え方が用いられています。例えばサブネットを設定する時、255.255.255.0
のようなサブネットマスクとオリジンのIPとビット演算することで、通信を制御しています。
ちなみにこれらの手法は基本情報技術者試験に出題されるので、ちょうど復習にもなれます。
方法
Modelの作成
整数型のroles_code
に、権限情報を保存するようにします。
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name, null: false, default: ""
t.integer :roles_code, null: false, default: 0
end
add_index :users, :name
add_index :users, :roles_code
end
end
class User < ApplicationRecord
end
権限の定義
例として権限を下記の風に定義します。各権限はそれぞれ独立です。
下記のテーブルが示したように、16進数(4ビット)と照らし合わしてみると、
最下位のビットはadmin
の権限の有無を表記、右から第2のビット、第3のビットはeditor
とobserver
の有無を記載しています。最上位は使用していません。今後新たに権限が追加できるように、最下位から上の桁に追加するのは重要です。
権限名: (未使用) observer editor admin
ビット: 0 1 1 1
権限の認証
サブネットマスクのように、対象コードと指定のビットマスクとAND演算(論理積演算)を行うと、もし結果がマスクと同じであれば、該当マスクに「通過した」考え、該当マスクの権限を所有すると判断します。
例題
admin_mask = 1 # 0001
editor_mask = 2 # 0010
observer_mask = 4 # 0100
my_roles = 3 # 0011
#### 権限の認証 ####
my_roles & admin_mask == admin_mask # true
# 0011 & 0001 == 0001
my_roles & editor_mask == editor_mask # true
# 0011 & 0010 == 0010
my_roles & observer_mask == observer_mask # false
# 0011 & 0100 => 0000 != 0100
# 故に`my_roles` に `admin` と `editor` の権限があることがわかる
#### 権限の付与 ####
# `my_roles_2` に `editor` と `observer` の権限を付与したい
# 観察すると、`0110` にするとわかった。
# `editor_mask` と `observer_mask` のOR演算(論理和)をとればいい
my_roles_2 = editor_mask | observer_mask
# 0110 = 0010 | 0010
my_roles_2
# 6 => 0110
マスクの設定
class User < ApplicationRecord
def self.role_masks
{
admin: 1, # 0001
editor: 2, # 0010
observer: 4, # 0100
}
end
end
所有する全ての権限を表示
簡単に言えば、roles
というattr_accessorを自前で設定します。
class User < ApplicationRecord
@roles
def roles
@roles = []
User.role_masks.each do |role, mask|
if roles_code & mask == mask
@roles << role_text
end
end
@roles
end
def self.role_masks
{
admin: 1, # 0001
editor: 2, # 0010
observer: 4, # 0100
}
end
end
user = User.new(name: 'user1', roles_code: 1)
user.roles
# [:admin]
所有する権限の変更
class User < ApplicationRecord
@roles
def roles
@roles = []
User.role_masks.each do |role, mask|
if roles_code & mask == mask
@roles << role
end
end
@roles
end
def roles=(roles)
self.roles_code = roles
.map{|role| User.role_masks[role]}
.compact.reduce(:|)
end
def self.role_masks
{
admin: 1, # 0001
editor: 2, # 0010
observer: 4, # 0100
}
end
end
user = User.new(name: 'user1', roles_code: 1)
user.roles
# [:admin]
user.roles = [:editor]
user.roles_code
# 2 => '0010'
user.roles = user.roles << :observer
user.roles_code
# 6 => '0110'
所有する権限で検索
SQLでは、WHERE
文に簡単な計算が行うこともできるので、下記のscopeを設定すれば検索出来ます。
class User < ApplicationRecord
@roles
scope :has_role, lambda { |role|
role_code = User.role_masks[role]
where('roles_code & ? = ?', role_code, role_code)
}
def roles
@roles = []
User.role_masks.each do |role, mask|
if roles_code & mask == mask
@roles << role
end
end
@roles
end
def roles=(roles)
self.roles_code = roles
.map{|role| User.role_masks[role]}
.compact.reduce(:|)
end
def self.role_masks
{
admin: 1, # 0001
editor: 2, # 0010
observer: 4, # 0100
}
end
end
User.has_role(:admin).has_role(:editor)
# User Load (0.6ms) SELECT "users".* FROM "users" WHERE (roles_code & 1 = 1) AND (roles_code & 2 = 2)
enum風にrole?
のようなメソッドの実装
メタプログラミングの手法を活用すれば、簡単にできます
class User < ApplicationRecord
@roles
scope :has_role, lambda { |role|
role_code = User.role_masks[role]
where('roles_code & ? = ?', role_code, role_code)
}
role_masks.keys.each do |role|
# `user.admin?` のようなメソッドの生成
method_name = role.to_s + "?"
define_method(method_name.to_sym) {
self.roles.include?(role)
}
end
def roles
@roles = []
User.role_masks.each do |role, mask|
if roles_code & mask == mask
@roles << role
end
end
@roles
end
def roles=(roles)
self.roles_code = roles
.map{|role| User.role_masks[role]}
.compact.reduce(:|)
end
def self.role_masks
{
admin: 1, # 0001
editor: 2, # 0010
observer: 4, # 0100
}
end
end
user = User.new(name: 'user1', roles_code: 3) # [:admin, :editor]
user.admin? # true
user.editor? # true
user.observer? # false
どうせなら、Enum風にScopeも自動生成しましょう。
class User < ApplicationRecord
@roles
scope :has_role, lambda { |role|
role_code = User.role_masks[role]
where('roles_code & ? = ?', role_code, role_code)
}
role_masks.keys.each do |role|
# `user.admin?` のようなメソッドの生成
method_name = role.to_s + "?"
define_method(method_name.to_sym) {
self.roles.include?(role)
}
# `User.admin` のようなスコープの生成
scope role, lambda {
role_code = User.role_masks[role]
where('roles_code & ? = ?', role_code, role_code)
}
end
def roles
@roles = []
User.role_masks.each do |role, mask|
if roles_code & mask == mask
@roles << role
end
end
@roles
end
def roles=(roles)
self.roles_code = roles
.map{|role| User.role_masks[role]}
.compact.reduce(:|)
end
def self.role_masks
{
admin: 1, # 0001
editor: 2, # 0010
observer: 4, # 0100
}
end
end
User.admin.first
# User Load (2.6ms) SELECT "users".* FROM "users" WHERE (roles_code & 1 = 1) ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]]
Ransackで検索可能にする(2019-07-03更新)
Ransackで検索したい時は、ransackable_scopes
を活用すれば簡単にできます。
詳しくはこちらの記事を参考してください。
class User < ApplicationRecord
@roles
scope :has_role, lambda { |role|
role_code = User.role_masks[role]
where('roles_code & ? = ?', role_code, role_code)
}
# ...省略...
def self.ransackable_scopes(auth_object = nil)
%i(has_role)
end
def self.role_masks
{
admin: 1, # 0001
editor: 2, # 0010
observer: 4, # 0100
}
end
end
User.ransack({has_role: 'admin'}).result
=> User Load (5.3ms) SELECT "users".* FROM "users" WHERE (roles_code & 1 = 1)
参考
https://qiita.com/koara-local/items/185838ea3fa37d9007f7
https://qiita.com/lnznt/items/d2d18e45bcab48cf107d
https://qiita.com/okamos/items/724a4a162dfa9e27754a
https://db.just4fun.biz/?SQL/SELECT句で四則演算を行う
https://stackoverflow.com/a/14061680/11263491