simple_cancan 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 081a175eb3429fa16be74dee2c7a57b5b5d6bf91
4
+ data.tar.gz: 86dc6d52dd8e69ced01b2c142a864d7dccbf16c8
5
+ SHA512:
6
+ metadata.gz: 83e050bc2577d86e994c2d1a294224bb6a0971faf5a772aef0d7efcaf14a8a9c2a4c42cedcd081fb2cdf506779d600bcc487ff24427bb9adba997684103f1f3f
7
+ data.tar.gz: 9640b98e04a5c430118807289294e5e58a321568eef48541193530d10959828b45486661d95175efb0f10d0ab220de1e62521e200933e691c38414991500ff88
@@ -0,0 +1,5 @@
1
+ require "simple_cancan/version"
2
+ require 'simple_cancan/ability'
3
+ require 'simple_cancan/rule'
4
+ require 'simple_cancan/controller_additions'
5
+ require 'simple_cancan/exceptions'
@@ -0,0 +1,260 @@
1
+ module SimpleCancan
2
+
3
+ module Ability
4
+
5
+ def can?(action, subject, *extra_args)
6
+ match = relevant_rules_for_match(action, subject).detect do |rule|
7
+ rule.matches_conditions?(action, subject, extra_args)
8
+ end
9
+ match ? match.base_behavior : false
10
+ end
11
+
12
+ # Convenience method which works the same as "can?" but returns the opposite value.
13
+ #
14
+ # cannot? :destroy, @project
15
+ #
16
+ def cannot?(*args)
17
+ !can?(*args)
18
+ end
19
+
20
+ # Defines which abilities are allowed using two arguments. The first one is the action
21
+ # you're setting the permission for, the second one is the class of object you're setting it on.
22
+ #
23
+ # can :update, Article
24
+ #
25
+ # You can pass an array for either of these parameters to match any one.
26
+ # Here the user has the ability to update or destroy both articles and comments.
27
+ #
28
+ # can [:update, :destroy], [Article, Comment]
29
+ #
30
+ # You can pass :all to match any object and :manage to match any action. Here are some examples.
31
+ #
32
+ # can :manage, :all
33
+ # can :update, :all
34
+ # can :manage, Project
35
+ #
36
+ # You can pass a hash of conditions as the third argument. Here the user can only see active projects which he owns.
37
+ #
38
+ # can :read, Project, :active => true, :user_id => user.id
39
+ #
40
+ # See ActiveRecordAdditions#accessible_by for how to use this in database queries. These conditions
41
+ # are also used for initial attributes when building a record in ControllerAdditions#load_resource.
42
+ #
43
+ # If the conditions hash does not give you enough control over defining abilities, you can use a block
44
+ # along with any Ruby code you want.
45
+ #
46
+ # can :update, Project do |project|
47
+ # project.groups.include?(user.group)
48
+ # end
49
+ #
50
+ # If the block returns true then the user has that :update ability for that project, otherwise he
51
+ # will be denied access. The downside to using a block is that it cannot be used to generate
52
+ # conditions for database queries.
53
+ #
54
+ # You can pass custom objects into this "can" method, this is usually done with a symbol
55
+ # and is useful if a class isn't available to define permissions on.
56
+ #
57
+ # can :read, :stats
58
+ # can? :read, :stats # => true
59
+ #
60
+ # IMPORTANT: Neither a hash of conditions or a block will be used when checking permission on a class.
61
+ #
62
+ # can :update, Project, :priority => 3
63
+ # can? :update, Project # => true
64
+ #
65
+ # If you pass no arguments to +can+, the action, class, and object will be passed to the block and the
66
+ # block will always be executed. This allows you to override the full behavior if the permissions are
67
+ # defined in an external source such as the database.
68
+ #
69
+ # can do |action, object_class, object|
70
+ # # check the database and return true/false
71
+ # end
72
+ #
73
+ def can(action = nil, subject = nil, conditions = nil, &block)
74
+ rules << Rule.new(true, action, subject, conditions, block)
75
+ end
76
+
77
+ # Defines an ability which cannot be done. Accepts the same arguments as "can".
78
+ #
79
+ # can :read, :all
80
+ # cannot :read, Comment
81
+ #
82
+ # A block can be passed just like "can", however if the logic is complex it is recommended
83
+ # to use the "can" method.
84
+ #
85
+ # cannot :read, Product do |product|
86
+ # product.invisible?
87
+ # end
88
+ #
89
+ def cannot(action = nil, subject = nil, conditions = nil, &block)
90
+ rules << Rule.new(false, action, subject, conditions, block)
91
+ end
92
+
93
+ # Alias one or more actions into another one.
94
+ #
95
+ # alias_action :update, :destroy, :to => :modify
96
+ # can :modify, Comment
97
+ #
98
+ # Then :modify permission will apply to both :update and :destroy requests.
99
+ #
100
+ # can? :update, Comment # => true
101
+ # can? :destroy, Comment # => true
102
+ #
103
+ # This only works in one direction. Passing the aliased action into the "can?" call
104
+ # will not work because aliases are meant to generate more generic actions.
105
+ #
106
+ # alias_action :update, :destroy, :to => :modify
107
+ # can :update, Comment
108
+ # can? :modify, Comment # => false
109
+ #
110
+ # Unless that exact alias is used.
111
+ #
112
+ # can :modify, Comment
113
+ # can? :modify, Comment # => true
114
+ #
115
+ # The following aliases are added by default for conveniently mapping common controller actions.
116
+ #
117
+ # alias_action :index, :show, :to => :read
118
+ # alias_action :new, :to => :create
119
+ # alias_action :edit, :to => :update
120
+ #
121
+ # This way one can use params[:action] in the controller to determine the permission.
122
+ def alias_action(*args)
123
+ target = args.pop[:to]
124
+ validate_target(target)
125
+ aliased_actions[target] ||= []
126
+ aliased_actions[target] += args
127
+ end
128
+
129
+ # User shouldn't specify targets with names of real actions or it will cause Seg fault
130
+ def validate_target(target)
131
+ raise Error, "You can't specify target (#{target}) as alias because it is real action name" if aliased_actions.values.flatten.include? target
132
+ end
133
+
134
+ # Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key.
135
+ def aliased_actions
136
+ @aliased_actions ||= default_alias_actions
137
+ end
138
+
139
+ # Removes previously aliased actions including the defaults.
140
+ def clear_aliased_actions
141
+ @aliased_actions = {}
142
+ end
143
+
144
+ def model_adapter(model_class, action)
145
+ adapter_class = ModelAdapters::AbstractAdapter.adapter_class(model_class)
146
+ adapter_class.new(model_class, relevant_rules_for_query(action, model_class))
147
+ end
148
+
149
+ # See ControllerAdditions#authorize! for documentation.
150
+ def authorize!(action, subject, *args)
151
+ message = nil
152
+ if args.last.kind_of?(Hash) && args.last.has_key?(:message)
153
+ message = args.pop[:message]
154
+ end
155
+ if cannot?(action, subject, *args)
156
+ message ||= unauthorized_message(action, subject)
157
+ raise AccessDenied.new(message, action, subject)
158
+ end
159
+ subject
160
+ end
161
+
162
+ def unauthorized_message(action, subject)
163
+ keys = unauthorized_message_keys(action, subject)
164
+ variables = {:action => action.to_s}
165
+ variables[:subject] = (subject.class == Class ? subject : subject.class).to_s.underscore.humanize.downcase
166
+ message = I18n.translate(nil, variables.merge(:scope => :unauthorized, :default => keys + [""]))
167
+ message.blank? ? nil : message
168
+ end
169
+
170
+ def attributes_for(action, subject)
171
+ attributes = {}
172
+ relevant_rules(action, subject).map do |rule|
173
+ attributes.merge!(rule.attributes_from_conditions) if rule.base_behavior
174
+ end
175
+ attributes
176
+ end
177
+
178
+ def has_block?(action, subject)
179
+ relevant_rules(action, subject).any?(&:only_block?)
180
+ end
181
+
182
+ def has_raw_sql?(action, subject)
183
+ relevant_rules(action, subject).any?(&:only_raw_sql?)
184
+ end
185
+
186
+ def merge(ability)
187
+ ability.send(:rules).each do |rule|
188
+ rules << rule.dup
189
+ end
190
+ self
191
+ end
192
+
193
+ private
194
+
195
+ def unauthorized_message_keys(action, subject)
196
+ subject = (subject.class == Class ? subject : subject.class).name.underscore unless subject.kind_of? Symbol
197
+ [subject, :all].map do |try_subject|
198
+ [aliases_for_action(action), :manage].flatten.map do |try_action|
199
+ :"#{try_action}.#{try_subject}"
200
+ end
201
+ end.flatten
202
+ end
203
+
204
+ # Accepts an array of actions and returns an array of actions which match.
205
+ # This should be called before "matches?" and other checking methods since they
206
+ # rely on the actions to be expanded.
207
+ def expand_actions(actions)
208
+ actions.map do |action|
209
+ aliased_actions[action] ? [action, *expand_actions(aliased_actions[action])] : action
210
+ end.flatten
211
+ end
212
+
213
+ # Given an action, it will try to find all of the actions which are aliased to it.
214
+ # This does the opposite kind of lookup as expand_actions.
215
+ def aliases_for_action(action)
216
+ results = [action]
217
+ aliased_actions.each do |aliased_action, actions|
218
+ results += aliases_for_action(aliased_action) if actions.include? action
219
+ end
220
+ results
221
+ end
222
+
223
+ def rules
224
+ @rules ||= []
225
+ end
226
+
227
+ # Returns an array of Rule instances which match the action and subject
228
+ # This does not take into consideration any hash conditions or block statements
229
+ def relevant_rules(action, subject)
230
+ rules.reverse.select do |rule|
231
+ rule.expanded_actions = expand_actions(rule.actions)
232
+ rule.relevant? action, subject
233
+ end
234
+ end
235
+
236
+ def relevant_rules_for_match(action, subject)
237
+ relevant_rules(action, subject).each do |rule|
238
+ if rule.only_raw_sql?
239
+ raise Error, "The can? and cannot? call cannot be used with a raw sql 'can' definition. The checking code cannot be determined for #{action.inspect} #{subject.inspect}"
240
+ end
241
+ end
242
+ end
243
+
244
+ def relevant_rules_for_query(action, subject)
245
+ relevant_rules(action, subject).each do |rule|
246
+ if rule.only_block?
247
+ raise Error, "The accessible_by call cannot be used with a block 'can' definition. The SQL cannot be determined for #{action.inspect} #{subject.inspect}"
248
+ end
249
+ end
250
+ end
251
+
252
+ def default_alias_actions
253
+ {
254
+ :read => [:index, :show],
255
+ :create => [:new],
256
+ :update => [:edit],
257
+ }
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,91 @@
1
+ module SimpleCancan
2
+
3
+ module ControllerAdditions
4
+ module ClassMethods
5
+ def load_and_authorize_resource(*args)
6
+ cancan_resource_class.add_before_filter(self, :load_and_authorize_resource, *args)
7
+ end
8
+
9
+ def load_resource(*args)
10
+ cancan_resource_class.add_before_filter(self, :load_resource, *args)
11
+ end
12
+
13
+ def authorize_resource(*args)
14
+ cancan_resource_class.add_before_filter(self, :authorize_resource, *args)
15
+ end
16
+
17
+ def skip_load_and_authorize_resource(*args)
18
+ skip_load_resource(*args)
19
+ skip_authorize_resource(*args)
20
+ end
21
+
22
+ def skip_load_resource(*args)
23
+ options = args.extract_options!
24
+ name = args.first
25
+ cancan_skipper[:load][name] = options
26
+ end
27
+
28
+ def skip_authorize_resource(*args)
29
+ options = args.extract_options!
30
+ name = args.first
31
+ cancan_skipper[:authorize][name] = options
32
+ end
33
+
34
+ def check_authorization(options = {})
35
+ self.after_filter(options.slice(:only, :except)) do |controller|
36
+ next if controller.instance_variable_defined?(:@_authorized)
37
+ next if options[:if] && !controller.send(options[:if])
38
+ next if options[:unless] && controller.send(options[:unless])
39
+ raise AuthorizationNotPerformed, "This action failed the check_authorization because it does not authorize_resource. Add skip_authorization_check to bypass this check."
40
+ end
41
+ end
42
+
43
+ def skip_authorization_check(*args)
44
+ self.before_filter(*args) do |controller|
45
+ controller.instance_variable_set(:@_authorized, true)
46
+ end
47
+ end
48
+
49
+ def skip_authorization(*args)
50
+ raise ImplementationRemoved, "The CanCan skip_authorization method has been renamed to skip_authorization_check. Please update your code."
51
+ end
52
+
53
+ def cancan_resource_class
54
+ if ancestors.map(&:to_s).include? "InheritedResources::Actions"
55
+ InheritedResource
56
+ else
57
+ ControllerResource
58
+ end
59
+ end
60
+
61
+ def cancan_skipper
62
+ @_cancan_skipper ||= {:authorize => {}, :load => {}}
63
+ end
64
+ end
65
+
66
+ def self.included(base)
67
+ base.extend ClassMethods
68
+ end
69
+
70
+ def authorize!(*args)
71
+ @_authorized = true
72
+ current_ability.authorize!(*args)
73
+ end
74
+
75
+ def unauthorized!(message = nil)
76
+ raise ImplementationRemoved, "The unauthorized! method has been removed from CanCan, use authorize! instead."
77
+ end
78
+
79
+ def current_ability
80
+ @current_ability ||= ::Ability.new(current_account)
81
+ end
82
+
83
+ def can?(*args)
84
+ current_ability.can?(*args)
85
+ end
86
+
87
+ def cannot?(*args)
88
+ current_ability.cannot?(*args)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,50 @@
1
+ module SimpleCancan
2
+ # A general CanCan exception
3
+ class Error < StandardError; end
4
+
5
+ # Raised when behavior is not implemented, usually used in an abstract class.
6
+ class NotImplemented < Error; end
7
+
8
+ # Raised when removed code is called, an alternative solution is provided in message.
9
+ class ImplementationRemoved < Error; end
10
+
11
+ # Raised when using check_authorization without calling authorized!
12
+ class AuthorizationNotPerformed < Error; end
13
+
14
+ # This error is raised when a user isn't allowed to access a given controller action.
15
+ # This usually happens within a call to ControllerAdditions#authorize! but can be
16
+ # raised manually.
17
+ #
18
+ # raise CanCan::AccessDenied.new("Not authorized!", :read, Article)
19
+ #
20
+ # The passed message, action, and subject are optional and can later be retrieved when
21
+ # rescuing from the exception.
22
+ #
23
+ # exception.message # => "Not authorized!"
24
+ # exception.action # => :read
25
+ # exception.subject # => Article
26
+ #
27
+ # If the message is not specified (or is nil) it will default to "You are not authorized
28
+ # to access this page." This default can be overridden by setting default_message.
29
+ #
30
+ # exception.default_message = "Default error message"
31
+ # exception.message # => "Default error message"
32
+ #
33
+ # See ControllerAdditions#authorized! for more information on rescuing from this exception
34
+ # and customizing the message using I18n.
35
+ class AccessDenied < Error
36
+ attr_reader :action, :subject
37
+ attr_writer :default_message
38
+
39
+ def initialize(message = nil, action = nil, subject = nil)
40
+ @message = message
41
+ @action = action
42
+ @subject = subject
43
+ @default_message = I18n.t(:"unauthorized.default", :default => "You are not authorized to access this page.")
44
+ end
45
+
46
+ def to_s
47
+ @message || @default_message
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,147 @@
1
+ module SimpleCancan
2
+ # This class is used internally and should only be called through Ability.
3
+ # it holds the information about a "can" call made on Ability and provides
4
+ # helpful methods to determine permission checking and conditions hash generation.
5
+ class Rule # :nodoc:
6
+ attr_reader :base_behavior, :subjects, :actions, :conditions
7
+ attr_writer :expanded_actions
8
+
9
+ # The first argument when initializing is the base_behavior which is a true/false
10
+ # value. True for "can" and false for "cannot". The next two arguments are the action
11
+ # and subject respectively (such as :read, @project). The third argument is a hash
12
+ # of conditions and the last one is the block passed to the "can" call.
13
+ def initialize(base_behavior, action, subject, conditions, block)
14
+ raise Error, "You are not able to supply a block with a hash of conditions in #{action} #{subject} ability. Use either one." if conditions.kind_of?(Hash) && !block.nil?
15
+ @match_all = action.nil? && subject.nil?
16
+ @base_behavior = base_behavior
17
+ @actions = [action].flatten
18
+ @subjects = [subject].flatten
19
+ @conditions = conditions || {}
20
+ @block = block
21
+ end
22
+
23
+ # Matches both the subject and action, not necessarily the conditions
24
+ def relevant?(action, subject)
25
+ subject = subject.values.first if subject.class == Hash
26
+ @match_all || (matches_action?(action) && matches_subject?(subject))
27
+ end
28
+
29
+ # Matches the block or conditions hash
30
+ def matches_conditions?(action, subject, extra_args)
31
+ if @match_all
32
+ call_block_with_all(action, subject, extra_args)
33
+ elsif @block && !subject_class?(subject)
34
+ @block.call(subject, *extra_args)
35
+ elsif @conditions.kind_of?(Hash) && subject.class == Hash
36
+ nested_subject_matches_conditions?(subject)
37
+ elsif @conditions.kind_of?(Hash) && !subject_class?(subject)
38
+ matches_conditions_hash?(subject)
39
+ else
40
+ # Don't stop at "cannot" definitions when there are conditions.
41
+ @conditions.empty? ? true : @base_behavior
42
+ end
43
+ end
44
+
45
+ def only_block?
46
+ conditions_empty? && !@block.nil?
47
+ end
48
+
49
+ def only_raw_sql?
50
+ @block.nil? && !conditions_empty? && !@conditions.kind_of?(Hash)
51
+ end
52
+
53
+ def conditions_empty?
54
+ @conditions == {} || @conditions.nil?
55
+ end
56
+
57
+ def unmergeable?
58
+ @conditions.respond_to?(:keys) && @conditions.present? &&
59
+ (!@conditions.keys.first.kind_of? Symbol)
60
+ end
61
+
62
+ def associations_hash(conditions = @conditions)
63
+ hash = {}
64
+ conditions.map do |name, value|
65
+ hash[name] = associations_hash(value) if value.kind_of? Hash
66
+ end if conditions.kind_of? Hash
67
+ hash
68
+ end
69
+
70
+ def attributes_from_conditions
71
+ attributes = {}
72
+ @conditions.each do |key, value|
73
+ attributes[key] = value unless [Array, Range, Hash].include? value.class
74
+ end if @conditions.kind_of? Hash
75
+ attributes
76
+ end
77
+
78
+ private
79
+
80
+ def subject_class?(subject)
81
+ klass = (subject.kind_of?(Hash) ? subject.values.first : subject).class
82
+ klass == Class || klass == Module
83
+ end
84
+
85
+ def matches_action?(action)
86
+ @expanded_actions.include?(:manage) || @expanded_actions.include?(action)
87
+ end
88
+
89
+ def matches_subject?(subject)
90
+ @subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject)
91
+ end
92
+
93
+ def matches_subject_class?(subject)
94
+ @subjects.any? { |sub| sub.kind_of?(Module) && (subject.kind_of?(sub) || subject.class.to_s == sub.to_s || subject.kind_of?(Module) && subject.ancestors.include?(sub)) }
95
+ end
96
+
97
+ # Checks if the given subject matches the given conditions hash.
98
+ # This behavior can be overriden by a model adapter by defining two class methods:
99
+ # override_matching_for_conditions?(subject, conditions) and
100
+ # matches_conditions_hash?(subject, conditions)
101
+ def matches_conditions_hash?(subject, conditions = @conditions)
102
+ if conditions.empty?
103
+ true
104
+ else
105
+ if model_adapter(subject).override_conditions_hash_matching? subject, conditions
106
+ model_adapter(subject).matches_conditions_hash? subject, conditions
107
+ else
108
+ conditions.all? do |name, value|
109
+ if model_adapter(subject).override_condition_matching? subject, name, value
110
+ model_adapter(subject).matches_condition? subject, name, value
111
+ else
112
+ attribute = subject.send(name)
113
+ if value.kind_of?(Hash)
114
+ if attribute.kind_of? Array
115
+ attribute.any? { |element| matches_conditions_hash? element, value }
116
+ else
117
+ !attribute.nil? && matches_conditions_hash?(attribute, value)
118
+ end
119
+ elsif !value.is_a?(String) && value.kind_of?(Enumerable)
120
+ value.include? attribute
121
+ else
122
+ attribute == value
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def nested_subject_matches_conditions?(subject_hash)
131
+ parent, child = subject_hash.first
132
+ matches_conditions_hash?(parent, @conditions[parent.class.name.downcase.to_sym] || {})
133
+ end
134
+
135
+ def call_block_with_all(action, subject, extra_args)
136
+ if subject.class == Class
137
+ @block.call(action, subject, nil, *extra_args)
138
+ else
139
+ @block.call(action, subject.class, subject, *extra_args)
140
+ end
141
+ end
142
+
143
+ def model_adapter(subject)
144
+ CanCan::ModelAdapters::AbstractAdapter.adapter_class(subject_class?(subject) ? subject : subject.class)
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,3 @@
1
+ module SimpleCancan
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_cancan
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tianliang Bai
8
+ - Fuhao Xu
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2015-08-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ~>
19
+ - !ruby/object:Gem::Version
20
+ version: '1.10'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ version: '1.10'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: '10.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ~>
40
+ - !ruby/object:Gem::Version
41
+ version: '10.0'
42
+ description: A simpler cancan for padrino
43
+ email:
44
+ - happybyronbai@gmail.com
45
+ - xufuhaomap@gmail.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - lib/simple_cancan.rb
51
+ - lib/simple_cancan/ability.rb
52
+ - lib/simple_cancan/controller_additions.rb
53
+ - lib/simple_cancan/exceptions.rb
54
+ - lib/simple_cancan/rule.rb
55
+ - lib/simple_cancan/version.rb
56
+ homepage: https://rubygems.org/gems/simple_cancan
57
+ licenses:
58
+ - MIT
59
+ metadata: {}
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 2.4.8
77
+ signing_key:
78
+ specification_version: 4
79
+ summary: A simpler cancan for padrino
80
+ test_files: []