can_play 0.2.4 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +68 -33
- data/can_play.gemspec +1 -0
- data/lib/can_play/ability.rb +2 -2
- data/lib/can_play/controller.rb +24 -1
- data/lib/can_play/power.rb +8 -6
- data/lib/can_play/version.rb +1 -1
- data/lib/can_play.rb +98 -54
- data/lib/generators/templates/can_play.rb +3 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8defb79962f3b993057d0f3adcf9a10dc76b3b53
|
4
|
+
data.tar.gz: 7be32b34ecd58d7a57c03b6bd586868198d01092
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cec401f5b1dd9a1fd5de254f720a1704f06fd8e482553b12d0637067c8b95782db584f65e72c001324196c2c66378e4774d5381479dcd3a69d3c0692a8a79d46
|
7
|
+
data.tar.gz: 8e913fc5a5d4dda021b0a03d2c1414884f02b4942fc7c4a97ac7d8e07bc169d4692d159ee99bddc7e2d62ad6e3f8d2898ea7768a42aa99371bf4dbe7f2a085d0
|
data/README.md
CHANGED
@@ -1,33 +1,69 @@
|
|
1
|
-
can_plan集成了cancancan和consul
|
1
|
+
系统权限的管理,就是控制用户对某个资源的操作权限。这里说的资源并不仅仅是指ActiveRecord::Base的子类,也可以是ruby的其他类或者模型。can_plan集成了cancancan和consul的功能,使用简单的DSL描述用户对单个资源或某类资源的操作权限。它的权限管理方式是基于角色的,不同角色可以赋予不同的权限,我们可以让用户和资源的操作权限之间建立关联并保存进数据库.
|
2
2
|
|
3
3
|
## 安装方式
|
4
4
|
|
5
|
-
### 安装cancancan
|
6
5
|
|
7
|
-
cancancan
|
8
|
-
### 安装consul
|
9
|
-
|
10
|
-
consul的使用请参见consul主页,在此我们安装后,不需要设置power文件,也无需在controller中设置current_power,can_play在内部集成了这些设置。
|
6
|
+
can_play在内部集成了cancancan和consul两个gem,所以没必要再安装这两个gem。
|
11
7
|
### can_play安装
|
12
|
-
在gemfile中加入can_play
|
8
|
+
在gemfile中加入can_play。
|
9
|
+
|
10
|
+
```
|
11
|
+
gem 'can_play'
|
12
|
+
```
|
13
13
|
|
14
|
-
|
14
|
+
运行bundle,安装完成后执行如下命令
|
15
15
|
|
16
16
|
```
|
17
17
|
rails generate can_play:install
|
18
18
|
```
|
19
19
|
|
20
|
-
|
20
|
+
执行该命令会在initializer和locales文件夹下生成配置文件。
|
21
21
|
initializer文件夹下的can_play.rb是can_play的基本配置文件。
|
22
22
|
locales下的can_play.zh-Cn.yml文件用于描述权限名称。
|
23
|
-
|
24
|
-
|
23
|
+
|
24
|
+
# path_to_config/initializers/can_play.rb
|
25
|
+
CanPlay::Config.setup do |config|
|
26
|
+
config.user_class_name = 'User'
|
27
|
+
config.role_class_name = 'Role'
|
28
|
+
config.role_resources_relation_name = 'role_resources'
|
29
|
+
config.super_roles = ['超级管理员']
|
30
|
+
end
|
31
|
+
|
32
|
+
can_play.rb配置文件中,可以传入用户类名(默认User)、角色类名(默认Role)、以及角色和权限的关联的名称(默认role_resources),role_resources_relation_name是角色类和操作权限资源之间的关联。
|
33
|
+
|
34
|
+
---
|
35
|
+
zh-CN:
|
36
|
+
can_play:
|
37
|
+
class_name:
|
38
|
+
test: 测试
|
39
|
+
contract: 合同
|
40
|
+
authority_name:
|
41
|
+
common:
|
42
|
+
list: 列表查看
|
43
|
+
create: 新建
|
44
|
+
read: 查看
|
45
|
+
update: 修改
|
46
|
+
delete: 删除
|
47
|
+
crud: 管理
|
48
|
+
menus_roles: 菜单分配
|
49
|
+
role_resources: 权限分配
|
50
|
+
improve: 完善信息
|
51
|
+
contract:
|
52
|
+
terminate: 终止
|
53
|
+
purchaser_confirm: 采购人确认
|
54
|
+
supplier_confirm: 供应商确认
|
55
|
+
|
56
|
+
can_play.zh-CN.yml是中文翻译文件,在这里写下权限的英文和中文名称的对应,在前端就可以获取到权限的中文描述,其中common是一些常用权限名称,特别的权限名称,可以单独写,如contract下的terminate权限是合同独有的权限名称,必须单独写,而class_name也可以写上资源名称,如contract,如果不写,会默认去ActiveRecord的翻译文件下去取中文翻译。
|
57
|
+
|
58
|
+
|
59
|
+
### DSL文件描述权限的方法
|
25
60
|
dsl文件写法如下:
|
26
61
|
|
27
62
|
#用哪个类用来描写权限,可在intializer下的can_play.rb文件下描写。
|
28
63
|
class Resource
|
29
64
|
include CanPlay
|
30
|
-
|
65
|
+
self.module_name = '核心模块'
|
66
|
+
|
31
67
|
# 所有limit块、collection块和member块中都注入了user这个变量,指向当前登录用户,可直接使用。
|
32
68
|
|
33
69
|
group Contract do |klass|
|
@@ -37,9 +73,9 @@ dsl文件写法如下:
|
|
37
73
|
if user.is_admin?
|
38
74
|
klass.all
|
39
75
|
elsif user.role? '供应商'
|
40
|
-
klass.where(
|
76
|
+
klass.where(supplier: user.supplier)
|
41
77
|
elsif user.role? '采购人'
|
42
|
-
klass.where(
|
78
|
+
klass.where(purchaser: user.purchaser)
|
43
79
|
else
|
44
80
|
klass.none
|
45
81
|
end
|
@@ -55,9 +91,9 @@ dsl文件写法如下:
|
|
55
91
|
if user.is_admin?
|
56
92
|
true
|
57
93
|
elsif user.role? '供应商'
|
58
|
-
obj.
|
94
|
+
obj.supplier.is? user.supplier
|
59
95
|
elsif user.role? '采购人'
|
60
|
-
obj.
|
96
|
+
obj.purchaser.is? user.purchaser
|
61
97
|
else
|
62
98
|
false
|
63
99
|
end
|
@@ -69,26 +105,25 @@ dsl文件写法如下:
|
|
69
105
|
end
|
70
106
|
end
|
71
107
|
|
72
|
-
|
108
|
+
end
|
109
|
+
|
110
|
+
`limit`方法用于控制某个用户可以查看的资源的额列表,如Contract类下的limit限制了管理员可以查看所有合同,供应商和采购人只能查看和自己相关的合同。limit方法会让在controller中生成一个动态方法,`current_power.contracts`,这个方法返回的是是我们再limit中写如的对象,这样就能根据用户的信息返回不同的资源数组。
|
73
111
|
|
74
|
-
|
75
|
-
if user.is_admin?
|
76
|
-
klass.all
|
77
|
-
else
|
78
|
-
klass.none
|
79
|
-
end
|
80
|
-
end
|
112
|
+
`collection`方法,可以控制某个用户对某类资源的控制权限。如list和create权限,在controller中,我们可以用`authorize!(:read, Contract)`来限制用户的访问。
|
81
113
|
|
82
|
-
|
83
|
-
user.is_admin?
|
84
|
-
end
|
114
|
+
`member`方法,可以控制用户对某个资源的控制权限,如read权限,在controller中我们可以用authorize!(:read, @contract)来限制用户的访问。
|
85
115
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
end
|
90
|
-
end
|
116
|
+
`self.module_name = '核心模块'`是用来处理在多模块开发的环境下,各个模块可能有自己的resource文件,并可能出现中文的重名,权限最终要集中管理,module_name可以做个简单的分隔,让用户清楚某个权限属于哪个模块。
|
117
|
+
|
118
|
+
这些在controller中需要使用的current_power方法和authorize!方法分别是consul和cancancan中既有的方法。
|
91
119
|
|
92
120
|
### 和角色类之间建立关联
|
93
121
|
|
94
|
-
此处的
|
122
|
+
此处的resouce文件相当于在数据库中的resouces表,以动态的语言记录了所有的权限。我们需要通过role_resources这样的中间表,建立角色和权限之间的关联。可以在数据库建立中间表role_resources。
|
123
|
+
|
124
|
+
在controller或view中只要调用 CanPlay.splat_grouped_resources_with_chinese_desc就能返回所有的权限hash,并按资源进行了分组。如果调用CanPlay.grouped_resources_with_chinese_desc则返回按module_name分组后,再按资源名称分组的权限hash。我们用这个hash在表单中呈现,让用户勾选,然后在controller中保存角色和资源权限的关联。
|
125
|
+
|
126
|
+
|
127
|
+
其中roles_resources的表结构,至少要有role_id、resource_name字段,其中role_id关联角色,而 resource_name用于关联resource类(伪关联,实际不用加belongs_to这类关联)。
|
128
|
+
|
129
|
+
有了这个关联,加载页面,就可以自动执行权限的限制了,当然前提是你在controller的每个action加入了cancancan的权限限制语句authorize!。
|
data/can_play.gemspec
CHANGED
data/lib/can_play/ability.rb
CHANGED
@@ -5,12 +5,12 @@ class Ability
|
|
5
5
|
def initialize(user)
|
6
6
|
self.user = user||CanPlay::Config.user_class_name.constantize.new
|
7
7
|
CanPlay::Config.super_roles.each do |role_name|
|
8
|
-
can(:manage, :all) if user.
|
8
|
+
can(:manage, :all) if user.send(CanPlay::Config.role_judge_method, role_name)
|
9
9
|
end
|
10
10
|
CanPlay::Config.role_class_name.constantize.all.each do |role|
|
11
11
|
next unless user.role?(role.name)
|
12
12
|
role.send(CanPlay::Config.role_resources_relation_name).each do |role_resource|
|
13
|
-
resource = CanPlay.
|
13
|
+
resource = CanPlay.find_by_name_and_code(role_resource.resource_name, CanPlay.override_code)
|
14
14
|
next unless resource
|
15
15
|
if resource[:type] == 'collection'
|
16
16
|
if resource[:behavior]
|
data/lib/can_play/controller.rb
CHANGED
@@ -1,6 +1,29 @@
|
|
1
1
|
class ActionController::Base
|
2
2
|
include Consul::Controller
|
3
3
|
current_power do
|
4
|
-
Power.new(current_user)
|
4
|
+
CanPlay::Power.new(current_user)
|
5
5
|
end
|
6
|
+
|
7
|
+
# 对current_power采用动态方法调用的装饰者。
|
8
|
+
class PlayResourceObject < BasicObject
|
9
|
+
def initialize(obj, klass)
|
10
|
+
@obj = obj
|
11
|
+
@klass = klass
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(method, *args, &block)
|
15
|
+
if @obj.respond_to? "#{method}_evaluate_in_#{@klass.override_code}_scope"
|
16
|
+
@obj.send("#{method}_evaluate_in_#{@klass.override_code}_scope", *args, &block)
|
17
|
+
elsif @obj.respond_to? method
|
18
|
+
@obj.send(method, *args, &block)
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def play_resources
|
26
|
+
@play_resource_object ||= PlayResourceObject.new(current_power, CanPlay)
|
27
|
+
end
|
28
|
+
|
6
29
|
end
|
data/lib/can_play/power.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
module CanPlay
|
2
|
+
class Power
|
3
|
+
include Consul::Power
|
4
|
+
attr_accessor :user
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
6
|
+
def initialize(user)
|
7
|
+
self.user = user
|
8
|
+
end
|
8
9
|
|
10
|
+
end
|
9
11
|
end
|
data/lib/can_play/version.rb
CHANGED
data/lib/can_play.rb
CHANGED
@@ -1,41 +1,41 @@
|
|
1
1
|
require 'ror_hack'
|
2
2
|
require 'consul'
|
3
3
|
require 'cancancan'
|
4
|
-
require
|
5
|
-
require "can_play/power"
|
6
|
-
require "can_play/ability"
|
7
|
-
require "can_play/controller"
|
4
|
+
require 'modularity'
|
8
5
|
|
9
6
|
module CanPlay
|
10
|
-
mattr_accessor :resources
|
11
|
-
|
7
|
+
mattr_accessor :resources, :override_resources, :override_code
|
8
|
+
self.override_code = nil
|
9
|
+
self.resources = []
|
10
|
+
self.override_resources = {}.with_indifferent_access
|
12
11
|
|
13
12
|
class << self
|
14
13
|
def included(base)
|
15
14
|
base.class_eval <<-RUBY
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
@module_name = ''
|
15
|
+
singleton_attr_accessor(:groups, :current_group, :module_name)
|
16
|
+
self.groups = []
|
17
|
+
self.current_group = nil
|
18
|
+
self.module_name = ''
|
21
19
|
RUBY
|
22
20
|
base.extend ClassMethods
|
23
21
|
end
|
24
22
|
|
25
|
-
def
|
26
|
-
CanPlay.
|
23
|
+
def find_by_name_and_code(name, code)
|
24
|
+
resource = CanPlay.override_resources[code].p2a.find { |r| r.name.to_s == name.to_s }
|
25
|
+
resource || CanPlay.resources.find { |r| r.name.to_s == name.to_s }
|
27
26
|
end
|
28
27
|
|
29
|
-
def
|
30
|
-
|
28
|
+
def conjunct_resources
|
29
|
+
resources = CanPlay.override_resources[CanPlay.override_code].p2a + CanPlay.resources
|
30
|
+
resources.uniq { |i| i.name }
|
31
31
|
end
|
32
32
|
|
33
|
-
def
|
34
|
-
|
33
|
+
def grouped_resources
|
34
|
+
conjunct_resources.multi_group_by :module_name, :group
|
35
35
|
end
|
36
36
|
|
37
|
-
def
|
38
|
-
|
37
|
+
def splat_grouped_resources
|
38
|
+
conjunct_resources.multi_group_by :group
|
39
39
|
end
|
40
40
|
|
41
41
|
def grouped_resources_with_chinese_desc
|
@@ -60,8 +60,8 @@ module CanPlay
|
|
60
60
|
splat_grouped_resources.tap do |i|
|
61
61
|
i.each do |group, resources|
|
62
62
|
group[:chinese_desc] = begin
|
63
|
-
name = I18n.t("can_play.class_name.#{group
|
64
|
-
name = group
|
63
|
+
name = I18n.t("can_play.class_name.#{group.name.singularize}", default: '')
|
64
|
+
name = group.klass.model_name.human if name.blank?
|
65
65
|
name
|
66
66
|
end
|
67
67
|
resources.each do |resource|
|
@@ -74,47 +74,52 @@ module CanPlay
|
|
74
74
|
end
|
75
75
|
|
76
76
|
module Config
|
77
|
-
mattr_accessor :user_class_name, :role_class_name, :role_resources_relation_name, :super_roles
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
77
|
+
mattr_accessor :user_class_name, :role_class_name, :role_resources_relation_name, :super_roles, :role_judge_method
|
78
|
+
self.user_class_name = 'User'
|
79
|
+
self.role_class_name = 'Role'
|
80
|
+
self.role_resources_relation_name = 'role_resources'
|
81
|
+
self.super_roles = []
|
82
|
+
self.role_judge_method = 'role?'
|
82
83
|
|
83
84
|
def self.setup
|
84
85
|
yield self
|
85
86
|
end
|
86
87
|
end
|
87
88
|
|
88
|
-
|
89
|
-
|
90
89
|
module ClassMethods
|
91
90
|
|
91
|
+
class NameImportantOpenStruct < OpenStruct
|
92
|
+
def eql?(another)
|
93
|
+
self.name == another.name
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
92
97
|
# 为每个 resource 添加一个 group, 方便管理
|
93
98
|
def group(opts, &block)
|
94
99
|
if opts.is_a?(Hash)
|
95
100
|
opts = opts.with_indifferent_access
|
96
|
-
group =
|
101
|
+
group = NameImportantOpenStruct.new(name: opts.delete(:name).to_s, klass: opts.delete(:klass))
|
97
102
|
elsif opts.is_a?(Module)
|
98
|
-
name
|
99
|
-
group =
|
103
|
+
name = opts.try(:table_name).presence || opts.to_s.underscore.gsub('/', '_').pluralize
|
104
|
+
group = NameImportantOpenStruct.new(name: name, klass: opts)
|
100
105
|
else
|
101
106
|
# do nothing
|
102
107
|
end
|
103
|
-
|
104
|
-
|
105
|
-
|
108
|
+
self.groups << group
|
109
|
+
self.groups = groups.uniq(&:name)
|
110
|
+
self.current_group = group
|
106
111
|
block.call(group.klass)
|
107
|
-
|
112
|
+
self.current_group = nil
|
108
113
|
end
|
109
114
|
|
110
115
|
def limit(name=nil, &block)
|
111
|
-
raise "Need define group first" if
|
112
|
-
Power.power(name
|
116
|
+
raise "Need define group first" if current_group.nil?
|
117
|
+
CanPlay::Power.power(name||current_group.name, &block)
|
113
118
|
end
|
114
119
|
|
115
|
-
def collection(verb_or_verbs, &block)
|
116
|
-
raise "Need define group first" if
|
117
|
-
group =
|
120
|
+
def collection(verb_or_verbs, opts={}.with_indifferent_access, &block)
|
121
|
+
raise "Need define group first" if current_group.nil?
|
122
|
+
group = current_group
|
118
123
|
behavior = nil
|
119
124
|
if block
|
120
125
|
behavior = lambda do |user|
|
@@ -127,16 +132,16 @@ module CanPlay
|
|
127
132
|
|
128
133
|
if verb_or_verbs.kind_of?(Array)
|
129
134
|
verb_or_verbs.each do |verb|
|
130
|
-
add_resource(group, verb, group.klass, 'collection', behavior)
|
135
|
+
add_resource(group, verb, group.klass, 'collection', behavior, opts)
|
131
136
|
end
|
132
137
|
else
|
133
|
-
add_resource(group, verb_or_verbs, group.klass, 'collection', behavior)
|
138
|
+
add_resource(group, verb_or_verbs, group.klass, 'collection', behavior, opts)
|
134
139
|
end
|
135
140
|
end
|
136
141
|
|
137
|
-
def member(verb_or_verbs, &block)
|
138
|
-
raise "Need define group first" if
|
139
|
-
group =
|
142
|
+
def member(verb_or_verbs, opts={}.with_indifferent_access, &block)
|
143
|
+
raise "Need define group first" if current_group.nil?
|
144
|
+
group = current_group
|
140
145
|
behavior = nil
|
141
146
|
if block
|
142
147
|
behavior = lambda do |user, obj|
|
@@ -149,26 +154,65 @@ module CanPlay
|
|
149
154
|
|
150
155
|
if verb_or_verbs.kind_of?(Array)
|
151
156
|
verb_or_verbs.each do |verb|
|
152
|
-
add_resource(group, verb, group.klass, 'member', behavior)
|
157
|
+
add_resource(group, verb, group.klass, 'member', behavior, opts)
|
153
158
|
end
|
154
159
|
else
|
155
|
-
add_resource(group, verb_or_verbs, group.klass, 'member', behavior)
|
160
|
+
add_resource(group, verb_or_verbs, group.klass, 'member', behavior, opts)
|
156
161
|
end
|
157
162
|
end
|
158
163
|
|
159
|
-
def add_resource(group, verb, object, type, behavior)
|
164
|
+
def add_resource(group, verb, object, type, behavior, opts)
|
160
165
|
name = "#{verb}_#{group.name}"
|
161
166
|
resource = OpenStruct.new(
|
162
167
|
module_name: module_name,
|
163
|
-
name:
|
164
|
-
group:
|
165
|
-
verb:
|
166
|
-
object:
|
167
|
-
type:
|
168
|
-
behavior:
|
168
|
+
name: name,
|
169
|
+
group: group,
|
170
|
+
verb: verb,
|
171
|
+
object: object,
|
172
|
+
type: type,
|
173
|
+
behavior: behavior,
|
174
|
+
opts: opts
|
169
175
|
)
|
170
176
|
CanPlay.resources.keep_if { |i| i.name != name }
|
171
177
|
CanPlay.resources << resource
|
172
178
|
end
|
173
179
|
end
|
180
|
+
|
181
|
+
module Override
|
182
|
+
as_trait do |code|
|
183
|
+
extend ClassMethods
|
184
|
+
singleton_attr_accessor(:groups, :current_group, :module_name)
|
185
|
+
self.groups = []
|
186
|
+
self.current_group = nil
|
187
|
+
self.module_name = ''
|
188
|
+
|
189
|
+
define_singleton_method(:limit) do |name=nil, &block|
|
190
|
+
raise "Need define group first" if current_group.nil?
|
191
|
+
method_name = name ? "#{name}_evaluate_in_#{code}_scope" : "#{current_group.name}_evaluate_in_#{code}_scope"
|
192
|
+
Power.power(method_name, &block)
|
193
|
+
end
|
194
|
+
|
195
|
+
define_singleton_method(:add_resource) do |group, verb, object, type, behavior, opts|
|
196
|
+
super(group, verb, object, type, behavior, opts) and return if code.blank?
|
197
|
+
CanPlay.override_resources[code] ||= []
|
198
|
+
name = "#{verb}_#{group.name}"
|
199
|
+
resource = OpenStruct.new(
|
200
|
+
module_name: module_name,
|
201
|
+
name: name,
|
202
|
+
group: group,
|
203
|
+
verb: verb,
|
204
|
+
object: object,
|
205
|
+
type: type,
|
206
|
+
behavior: behavior,
|
207
|
+
opts: opts
|
208
|
+
)
|
209
|
+
CanPlay.override_resources[code].keep_if { |i| i.name != name }
|
210
|
+
CanPlay.override_resources[code] << resource
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
174
214
|
end
|
215
|
+
|
216
|
+
require "can_play/power"
|
217
|
+
require "can_play/controller"
|
218
|
+
require "can_play/ability"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: can_play
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- happyming9527
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-09-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - ~>
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0.1'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: modularity
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.0'
|
83
97
|
description: control user's permissions based on role and resource.
|
84
98
|
email:
|
85
99
|
- happyming9527@gmail.com
|