permit 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +5 -0
  2. data/.yardopts +3 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.mkd +238 -0
  5. data/Rakefile +69 -0
  6. data/VERSION.yml +5 -0
  7. data/generators/permit/USAGE +40 -0
  8. data/generators/permit/permit_generator.rb +25 -0
  9. data/generators/permit/templates/authorization.rb +2 -0
  10. data/generators/permit/templates/initializer.rb +37 -0
  11. data/generators/permit/templates/migration.rb +28 -0
  12. data/generators/permit/templates/role.rb +2 -0
  13. data/init.rb +1 -0
  14. data/install.rb +1 -0
  15. data/lib/models/association.rb +89 -0
  16. data/lib/models/authorizable.rb +31 -0
  17. data/lib/models/authorization.rb +54 -0
  18. data/lib/models/person.rb +148 -0
  19. data/lib/models/role.rb +59 -0
  20. data/lib/permit/controller.rb +132 -0
  21. data/lib/permit/permit_rule.rb +198 -0
  22. data/lib/permit/permit_rules.rb +141 -0
  23. data/lib/permit/support.rb +67 -0
  24. data/lib/permit.rb +134 -0
  25. data/permit.gemspec +91 -0
  26. data/rails/init.rb +7 -0
  27. data/spec/models/alternate_models_spec.rb +54 -0
  28. data/spec/models/authorizable_spec.rb +78 -0
  29. data/spec/models/authorization_spec.rb +77 -0
  30. data/spec/models/person_spec.rb +278 -0
  31. data/spec/models/role_spec.rb +121 -0
  32. data/spec/permit/controller_spec.rb +308 -0
  33. data/spec/permit/permit_rule_spec.rb +452 -0
  34. data/spec/permit/permit_rules_spec.rb +273 -0
  35. data/spec/permit_spec.rb +58 -0
  36. data/spec/spec_helper.rb +73 -0
  37. data/spec/support/helpers.rb +13 -0
  38. data/spec/support/models.rb +38 -0
  39. data/spec/support/permits_controller.rb +7 -0
  40. data/tasks/permit_tasks.rake +4 -0
  41. data/uninstall.rb +1 -0
  42. metadata +107 -0
@@ -0,0 +1,148 @@
1
+ module Permit
2
+ module Models
3
+ module PersonExtensions
4
+ def self.included(klass)
5
+ klass.extend PersonClassMethods
6
+ klass.extend Permit::Support::ClassMethods
7
+ end
8
+
9
+ module PersonClassMethods
10
+ # Defines the current model class as handling people for Permit.
11
+ def permit_person
12
+ return if include? Permit::Models::PersonExtensions::PersonInstanceMethods
13
+
14
+ permit_authorized_model
15
+
16
+ include Permit::Support
17
+ include Permit::Models::PersonExtensions::PersonInstanceMethods
18
+ end
19
+ end
20
+
21
+ module PersonInstanceMethods
22
+ # Determines if the current person is authorized for any of the given
23
+ # role(s) and resource.
24
+ #
25
+ # @param [Role, String, Symbol, <Role, String, Symbol>] roles the roles
26
+ # to check for authorization on.
27
+ # @param [Authorizable, nil, :any] resource the resource to check for
28
+ # authorization on.
29
+ # @return [true, false] true if the person is authorized on any of the a
30
+ # roles, false otherwise.
31
+ def authorized?(roles, resource)
32
+ permit_arrayify(roles).each do |r|
33
+ role = get_role(r)
34
+ next unless role
35
+ conditions = authorization_conditions(role, resource)
36
+ return true if permit_authorizations_proxy.exists?(conditions)
37
+ end
38
+ return false
39
+ end
40
+
41
+ # Determines if the current person is authorized for all of the given
42
+ # roles and resource.
43
+ #
44
+ # @param [permit_role, String, Symbol, <permit_role, String, Symbol>]
45
+ # roles the roles to check for authorization on.
46
+ # @param [permit_authorizable, nil, :any] resource the resource to check for
47
+ # authorization on.
48
+ # @return [true, false] true if the person is authorized on all of the a
49
+ # roles, false otherwise.
50
+ def authorized_all?(roles, resource)
51
+ permit_arrayify(roles).each do |r|
52
+ role = get_role(r)
53
+ return false unless role
54
+ conditions = authorization_conditions(role, resource)
55
+ return false unless permit_authorizations_proxy.exists?(conditions)
56
+ end
57
+ return true
58
+ end
59
+
60
+ # Authorizes the current person for all of the roles for the given
61
+ # resource, skipping any authorizations that the person already has. If
62
+ # there are any issues with the authorization an error will be raised.
63
+ #
64
+ # <em>The authorizations are run in a transaction. If an error is
65
+ # raised, *all* authorizations for the call will be rolled back.</em>
66
+ #
67
+ # @param [permit_role, String, Symbol, <permit_role, String, Symbol>]
68
+ # roles the roles to authorize the person on.
69
+ # @param [permit_authorizable, nil] resource the resource to authorize
70
+ # the person on.
71
+ # @return [true] true if no errors occur during authorization.
72
+ def authorize(roles, resource = nil)
73
+ Permit::Config.authorization_class.transaction do
74
+ permit_arrayify(roles).each do |r|
75
+ role = get_role(r)
76
+ next if authorized?(role, resource)
77
+
78
+ authz = permit_authorizations_proxy.build
79
+ authz.send("#{Permit::Config.role_class.class_symbol}=", role)
80
+ authz.resource = resource
81
+ authz.save!
82
+ end
83
+ end
84
+ return true
85
+ end
86
+
87
+ # Revokes existing authorizations from the current person for the given
88
+ # roles and resource. If there are any issues with the revocation an
89
+ # error will be raised. Otherwise, the operation will return an Array of
90
+ # the Authorizations affected by the operation.
91
+ #
92
+ # This operation uses ActiveRecord's <tt>destroy_all</tt> method. For
93
+ # more information on what this means, please reference the ActiveRecord
94
+ # documentation.
95
+ #
96
+ # <em>The revocations are run in a transaction. If an error is raised,
97
+ # *all* revocations for the call will be rolled back.</em>
98
+ #
99
+ # @param [permit_role, String, Symbol, <permit_role, String, Symbol>]
100
+ # roles the roles to revoke from the person.
101
+ # @param [permit_authorizable, nil, :any] resource the resource to
102
+ # revoke roles for. If +:any+ is given then any authorizations for the
103
+ # roles will be revoked.
104
+ # @return [<permit_authorization>] the authorizations that were revoked.
105
+ # @raise any errors that ActiveRecord encounters during processing.
106
+ def revoke(roles, resource)
107
+ remove_authorizations roles, resource do |conditions|
108
+ Permit::Config.authorization_class.destroy_all conditions
109
+ end
110
+ end
111
+
112
+ # Revokes existing authorizations from the current person for the given
113
+ # roles and resource. If there are any issues with the revocation an
114
+ # error will be raised. Otherwise, the operation will return the number
115
+ # of authorizations affected.
116
+ #
117
+ # This operation uses ActiveRecord's <tt>delete_all</tt> method. For
118
+ # more information on what this means, please reference the ActiveRecord
119
+ # documentation.
120
+ #
121
+ # <em>The revocations are run in a transaction. If an error is raised,
122
+ # *all* revocations for the call will be rolled back.</em>
123
+ #
124
+ # @param [permit_role, String, Symbol, <permit_role, String, Symbol>]
125
+ # roles the roles to revoke from the person.
126
+ # @param [permit_authorizable, nil, :any] resource the resource to
127
+ # revoke roles for. If +:any+ is given then any authorizations for the
128
+ # roles will be revoked.
129
+ # @return [Fixnum] the number of authorizations revoked.
130
+ # @raise any errors that ActiveRecord encounters during processing.
131
+ def revoke!(roles, resource)
132
+ remove_authorizations roles, resource do |conditions|
133
+ Permit::Config.authorization_class.delete_all conditions
134
+ end
135
+ end
136
+
137
+ protected
138
+ def remove_authorizations(roles, resource)
139
+ Permit::Config.authorization_class.transaction do
140
+ conditions = authorization_conditions(roles, resource, self)
141
+ yield conditions
142
+ end
143
+ end
144
+
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,59 @@
1
+ module Permit
2
+ module Models
3
+ module RoleExtensions
4
+ def self.included(klass)
5
+ klass.extend RoleClassMethods
6
+ klass.extend Permit::Support::ClassMethods
7
+ end
8
+
9
+ module RoleClassMethods
10
+ def permit_role
11
+ return if include? Permit::Models::RoleExtensions::RoleInstanceMethods
12
+
13
+ permit_authorized_model
14
+
15
+ validates_presence_of :key, :name
16
+ validates_inclusion_of :requires_resource, :authorize_resource, :in => [true, false]
17
+ validates_uniqueness_of :key, :case_sensistive => false
18
+ validate :resource_requirement
19
+
20
+ # Finds the role by its key, preparing the passed in value before
21
+ # querying.
22
+ #
23
+ # @param [String, Symbol] val the key value
24
+ # @return [Role, nil] the role that matches the key. nil if none are
25
+ # found.
26
+ def find_by_key(val)
27
+ find(:first, :conditions => {:key => prepare_key(val)})
28
+ end
29
+
30
+ # Prepares the key value for use.
31
+ #
32
+ # @param [String, Symbol] val the key value
33
+ # @return [String] the formatted key value.
34
+ def prepare_key(val)
35
+ val.nil? ? val : val.to_s.downcase
36
+ end
37
+
38
+ include Permit::Support
39
+ include Permit::Models::RoleExtensions::RoleInstanceMethods
40
+ end
41
+ end
42
+
43
+ module RoleInstanceMethods
44
+ # Sets the key for the role with extra processing to convert it from a
45
+ # symbol and downcase it.
46
+ #
47
+ # @param [String, Symbol] val the key value.
48
+ def key=(val)
49
+ write_attribute :key, self.class.prepare_key(val)
50
+ end
51
+
52
+ protected
53
+ def resource_requirement
54
+ errors.add(:requires_resource, "cannot be true if authorize_resource is false") if !authorize_resource? && requires_resource?
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,132 @@
1
+ module Permit
2
+ module ControllerExtensions
3
+ def self.included(klass)
4
+ klass.send :class_inheritable_accessor, :permit_rules
5
+ klass.send :include, PermitInstanceMethods
6
+ klass.send :extend, PermitClassMethods
7
+ klass.send :permit_rules=, PermitRules.new(Rails.logger)
8
+ klass.send :helper_method, :authorized?, :allowed?, :denied?
9
+
10
+ # This is only needed in development mode since models are not cached, and
11
+ # causes the models to end up in a weird state. This forces Permit to
12
+ # reestablish the core classes it uses internally.
13
+ klass.send :before_filter, :reset_permit_core if Rails.env.development?
14
+ end
15
+
16
+ module PermitClassMethods
17
+ # Creates a new block of Permit authorization rules, and sets the before
18
+ # filter to run them. The order of +deny+ and +allow+ rules do not matter.
19
+ # +deny+ rules will always be run first, and evaluation terminates on the
20
+ # first match.
21
+ #
22
+ # @example
23
+ # permit do
24
+ # deny :developer, :from => :all, :unless => Proc.new {(8..17).include?(Time.now.hour)}
25
+ # allow :person, :who => :is_member, :of => :team, :to => :read
26
+ # allow [:project_manager, :developer], :on => :project, :to => :all
27
+ # end
28
+ #
29
+ # @param [Hash] options options to use when evaluating this block of
30
+ # authorizations.
31
+ # @option options [:allow, :deny] :default_access overrides the
32
+ # {Permit::Config.default_access} setting.
33
+ # @param [Block] &block the block containing the authorization rules. See
34
+ # {PermitRules#allow} and {PermitRules#deny} for the syntax for the
35
+ # respective types of rules.
36
+ def permit(options = {}, &block)
37
+ rules = PermitRules.new(Rails.logger, options)
38
+ rules.instance_eval(&block) if block_given?
39
+ self.permit_rules = rules
40
+ set_permit_before_filter
41
+ end
42
+
43
+ private
44
+ def set_permit_before_filter
45
+ # Remove check_authorizations if it was set in a super class so that
46
+ # other before filters that possibly set the needed resource have a
47
+ # chance to run.
48
+ filter_chain.delete_if {|f| f.method == :check_authorizations}
49
+ before_filter :check_authorizations unless filter_chain.include?(:check_authorizations)
50
+ end
51
+ end
52
+
53
+ module PermitInstanceMethods
54
+ protected
55
+ # Needed to reset the core models in development mode as they were defined
56
+ # in the initializer for Permit.
57
+ def reset_permit_core
58
+ Permit::Config.reset_core_models
59
+ return true
60
+ end
61
+
62
+ # Called by {#check_authorizations} when a person is not authorized to
63
+ # access the current action. It calls +render_optional_error_file(401)+ on
64
+ # the controller, to render a Not Authorized error.
65
+ #
66
+ # If +#access_denied+ is already defined on the superclass, or redefined
67
+ # in the current controller then that will be called instead.
68
+ #
69
+ # @return [false] always returns false.
70
+ def access_denied
71
+ defined?(super) ? super : render_optional_error_file(401)
72
+ return false
73
+ end
74
+
75
+ # Evaluates the Permit authorization rules for the current person on the
76
+ # current action. If the person is not permitted {#access_denied} will be
77
+ # called.
78
+ def check_authorizations
79
+ return access_denied unless self.permit_rules.permitted?(permit_authorization_subject, params[:action].to_sym, binding)
80
+ true
81
+ end
82
+
83
+ # Creates a PermitRule with the arguments that are given, and attempts to
84
+ # match it based on the current person and binding context.
85
+ #
86
+ # For information on the parameters for this method see
87
+ # {PermitRule#initialize}.
88
+ #
89
+ # @return [Boolean] true if the rule matches, otherwise false.
90
+ def allowed?(roles, options = {})
91
+ rule = PermitRule.new roles, options
92
+ rule.matches? permit_authorization_subject, binding
93
+ end
94
+
95
+ # Creates a PermitRule with the arguments that are given, and attempts to
96
+ # match it based on the current person and binding context.
97
+ #
98
+ # For information on the parameters for this method see
99
+ # {PermitRule#initialize}.
100
+ #
101
+ # @return [Boolean] true if the rule does not match, otherwise false.
102
+ def denied?(roles, options = {})
103
+ !allowed? roles, options
104
+ end
105
+
106
+ # Shortcut for +current_person#authorized?+. If the current person is a
107
+ # guest this will automatically return false.
108
+ #
109
+ # For information on the parameters for this method see
110
+ # {Permit::Models::PersonExtensions::PersonInstanceMethods#authorized?}
111
+ def authorized?(roles, resource)
112
+ permit_authorization_subject.guest? ? false : permit_authorization_subject.authorized?(roles, resource)
113
+ end
114
+
115
+ private
116
+ def permit_authorization_subject
117
+ return send(@controller_subject_method) if @controller_subject_method
118
+
119
+ @controller_subject_method = if Permit::Config.controller_subject_method
120
+ Permit::Config.controller_subject_method
121
+ elsif Permit::Config.person_class
122
+ klass_name = Permit::Config.person_class.class_name.underscore
123
+ "current_#{klass_name}".to_sym
124
+ else
125
+ :current_person
126
+ end
127
+
128
+ send(@controller_subject_method)
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,198 @@
1
+ module Permit
2
+ class PermitRule
3
+ include Permit::Support
4
+
5
+ VALID_OPTION_KEYS = [:who, :that, :of, :on, :if, :unless]
6
+ BUILTIN_ROLES = [:person, :guest, :everyone]
7
+
8
+ attr_reader :roles, :target_var, :method, :if, :unless
9
+
10
+ # Creates a new PermitRule.
11
+ #
12
+ # +:if+ and +:unless+ conditions may be evaluated for static, dynamic, and
13
+ # named authorizations. They are evaluated after the other rule checks are
14
+ # applied, and only if the rule still matches. The conditionals may make a
15
+ # matching rule not match, but will not make an unmatched rule match. If
16
+ # both +:if+ and +:unless+ are given the +:if+ condition is run first, and
17
+ # if the rule still matches the +:unless+ will be run.
18
+ #
19
+ # @param [:person, :guest, :everyone, Symbol, <Symbol>] roles the role(s) to
20
+ # test against.
21
+ # - :person - +current_person.guest? == false+ This person should be
22
+ # authenticated. This indicates a dynamic authorization.
23
+ # - :guest - +current_person.guest? == true+ This is a person that is not
24
+ # authenticated. This is a static authorization.
25
+ # - :everyone - Any user of the system. This is a static authorization.
26
+ # - Symbol/<Symbol> - This is the key or keys of any of the role(s) to
27
+ # match against in the database. This indicates a named authorization.
28
+ # @param [Hash] options the options to use to configure the authorization.
29
+ # @option options [Symbol] :who Indicates that a method should be checked on
30
+ # the target object to authorize. Checks a variety of possibilities,
31
+ # taking the first variation that the target responds to.
32
+ #
33
+ # When the symbol is prefixed with 'is_' then multiple methods will be
34
+ # tried passing the person in. The methods tried for +:is_owner+ would be
35
+ # +is_owner()+, +is_owner?()+, +owner()+, +owner+, +owners.exist?()+. If
36
+ # this option is given +:of+/+:on+ must also be given.
37
+ # @option options [Symbol] :that alias for +:who+
38
+ # @option options [Symbol] :of The name of the instance variable to use as
39
+ # the target resource.
40
+ #
41
+ # In a dynamic authorization this is the object that will be tested using
42
+ # the value of +:who+/+:that+.
43
+ #
44
+ # In a named authorization this is the resource the person must be
45
+ # authorized on for one or more of the roles. +:any+ may be given to
46
+ # indicate a match if the person has one of the roles for any resource. If
47
+ # not given, or set to +nil+, then the match will apply to a person that
48
+ # has a matching role authorization for a nil resource.
49
+ # @option options [Symbol] :on alias for +:of+
50
+ # @option options [Symbol, String, Proc] :if code to evaluate at the end of the
51
+ # match if it is still valid. If it returns false, the rule will not match.
52
+ # @option options [Symbol, String, Proc] :unless code to evaluate at the end
53
+ # of the match if it is still valid. If it returns true, the rule will not
54
+ # match.
55
+ #
56
+ # @raise [PermitConfigurationError] if the rule options are invalid.
57
+ def initialize(roles, options = {})
58
+ options.assert_valid_keys *VALID_OPTION_KEYS
59
+
60
+ @roles = validate_roles(roles).freeze
61
+
62
+ validate_options options
63
+
64
+ @method = options[:who] || options[:that]
65
+ @target_var = options[:of] || options[:on]
66
+
67
+ @if = options[:if]
68
+ @unless = options[:unless]
69
+ end
70
+
71
+ # Determine if the passed in person matches this rule.
72
+ #
73
+ # @param [permit_person] person the person to evaluate for authorization
74
+ # @param [Binding] context_binding the binding to use to locate the resource
75
+ # and/or evaluate the if/unless conditions.
76
+ # @return [Boolean] true if the person matches the rule, otherwise
77
+ # false.
78
+ # @raise [PermitEvaluationError] if there is a problem evaluating the rule.
79
+ def matches?(person, context_binding)
80
+ matched = if BUILTIN_ROLES.include? @roles[0]
81
+ has_builtin_authorization? person, context_binding
82
+ else
83
+ has_named_authorizations? person, context_binding
84
+ end
85
+
86
+ passed_conditionals = matched ? passes_conditionals?(context_binding) : false
87
+ passed = matched && passed_conditionals
88
+ return passed
89
+ end
90
+
91
+ private
92
+ def validate_roles(roles)
93
+ roles = permit_arrayify(roles).compact.uniq
94
+ raise PermitConfigurationError, "At least one role must be specified." if roles.empty?
95
+ raise PermitConfigurationError, "Only one role may be specified when using :person, :guest, or :everyone" if (roles & BUILTIN_ROLES).size > 0 && roles.size > 1
96
+ roles.freeze
97
+ end
98
+
99
+ def validate_options(options)
100
+ if (options[:of] || options[:on]) && @roles[0] == :person
101
+ raise PermitConfigurationError, "When :of or :on are specified for the :person role a corresponding :who or :that must be given" unless options[:who] || options[:that]
102
+ end
103
+
104
+ if options[:who] || options[:that]
105
+ raise PermitConfigurationError, "The :who and :that options are only valid for the :person role." unless @roles[0] == :person
106
+ raise PermitConfigurationError, "When :who or :that is specified a corresponding :of or :on must be given" unless options[:of] || options[:on]
107
+ end
108
+
109
+ raise PermitConfigurationError, "Either :who or :that may be specified, but not both." if options[:who] && options[:that]
110
+ raise PermitConfigurationError, "Either :of or :on may be specified, but not both." if options[:of] && options[:on]
111
+ end
112
+
113
+ def has_builtin_authorization?(person, context_binding)
114
+ case @roles[0]
115
+ when :everyone then true
116
+ when :guest then person.guest?
117
+ when :person then has_dynamic_authorization? person, context_binding
118
+ else false
119
+ end
120
+ end
121
+
122
+ def has_named_authorizations?(person, context_binding)
123
+ return false if person.guest?
124
+ resource = case @target_var
125
+ when nil then nil
126
+ when :any then :any
127
+ else get_resource(context_binding)
128
+ end
129
+ person.authorized? @roles, resource
130
+ end
131
+
132
+ def has_dynamic_authorization?(person, context_binding)
133
+ return false if person.guest?
134
+ return true if @target_var.nil?
135
+
136
+ resource = get_resource context_binding
137
+ methods = determine_method_sequence @method
138
+
139
+ methods.each do |name, type|
140
+ next unless resource.respond_to? name
141
+
142
+ case type
143
+ when :method then return resource.send name, person
144
+ when :getter then return resource.send(name) == person
145
+ when :collection then return resource.send(name).exists?(person)
146
+ else return false
147
+ end
148
+ end
149
+
150
+ # Target didn't respond to any attempts. This would be a problem.
151
+ raise PermitEvaluationError, "Target object ':#{@target_var}' evaluated as #{resource.inspect} did not respond to any of the following: #{methods.collect {|n,t| n}.join(', ')}"
152
+ end
153
+
154
+ # is_owner - is_owner(), is_owner?(), owner?(), owner, owners.exists()
155
+ # is_manager? - is_manager?(), manager?()
156
+ # has_something - has_something()
157
+ # does_whatever - does_whatever()
158
+ def determine_method_sequence(method)
159
+ method = method.to_s
160
+ names = /^is_([\w\-]+(\?)?)$/.match method
161
+ return [[method, :method]] unless names
162
+
163
+ # Name ends with question mark
164
+ if names[2] == "?"
165
+ [[names[0], :method], [names[1], :method]]
166
+ else
167
+ [
168
+ [names[0], :method],
169
+ [names[0] + '?', :method],
170
+ [names[1], :getter],
171
+ [names[1] + '?', :method],
172
+ [names[1].pluralize, :collection]
173
+ ]
174
+ end
175
+ end
176
+
177
+ def get_resource(context_binding)
178
+ eval "@#{@target_var.to_s}", context_binding
179
+ end
180
+
181
+ def passes_conditionals?(context_binding)
182
+ return false unless eval_conditional @if, true, context_binding
183
+ return false if eval_conditional @unless, false, context_binding
184
+ true
185
+ end
186
+
187
+ def eval_conditional(condition, default, context_binding)
188
+ if condition
189
+ condition = condition.to_s if Symbol===condition
190
+ return (String===condition ? eval(condition, context_binding) : condition.call)
191
+ else
192
+ return default
193
+ end
194
+ end
195
+
196
+ end
197
+
198
+ end
@@ -0,0 +1,141 @@
1
+ module Permit
2
+ class PermitRules
3
+ include Permit::Support
4
+
5
+ attr_accessor :action_deny_rules, :action_allow_rules, :logger, :options
6
+
7
+ # @param [#info] logger the logger to use when evaluating rules
8
+ # @param [Hash] options the set of options to use during rule evaluation
9
+ # @option options [Symbol] :default_access overrides the value in
10
+ # Permit::Config#default_access to indicate how {#permitted?} will behave if
11
+ # no rules match.
12
+ def initialize(logger, options = {})
13
+ @action_deny_rules = {}
14
+ @action_allow_rules = {}
15
+ @logger = logger
16
+ @options = options
17
+ end
18
+
19
+ # Determines if the person is permitted on the specified action by first
20
+ # evaluating deny rules, and then allow rules. If the +:default_access+
21
+ # option is set then its value will be used instead of the value from
22
+ # Permit::Config#default_access.
23
+ #
24
+ # @param [permit_person] person the person to check for authorization
25
+ # @param [Symbol] action the action to check for authorization on.
26
+ # @param [Binding] context_binding the binding to use to locate the resource
27
+ # and/or process if/unless constraints.
28
+ # @return [true, false] true if the person is permitted on the given action,
29
+ # false otherwise.
30
+ # @raise [PermitEvaluationError] if an error occurs while evaluating one of
31
+ # the rules.
32
+ def permitted?(person, action, context_binding)
33
+ # Denial takes priority over allow
34
+ return false if has_action_rule_match?(:deny, @action_deny_rules, person, action, context_binding)
35
+
36
+ return true if has_action_rule_match?(:allow, @action_allow_rules, person, action, context_binding)
37
+
38
+ # Default to no access if no rules match
39
+ default_access = (@options[:default_access] || Permit::Config.default_access)
40
+ return (default_access == :allow ? true : false)
41
+ end
42
+
43
+ # Adds an allow rule for the given actions to the collection.
44
+ #
45
+ # @example Allow a person that is a member of a team to show
46
+ # allow :person, :who => :is_member, :of => :team, :to => :show
47
+ # @example Allow a person with either of the named roles for a resource to perform any "write" operations.
48
+ # allow [:project_admin, :project_manager], :of => :project, :to => :write
49
+ #
50
+ # @param [Symbol, <Symbol>] roles the role(s) that the rule will apply to.
51
+ # @param [Hash] options the options used to build the rule.
52
+ # @option options [Symbol] :who the method to call on the target resource.
53
+ # @option options [Symbol] :that alias for :who
54
+ # @option options [Symbol] :of the name of the instance variable holding the target
55
+ # resource. If set to +:any+ then the match will apply to a person that has
56
+ # a matching role authorization for any resource. If not given, or set to
57
+ # +nil+, then the match will apply to a person that has a matching role
58
+ # authorization for a nil resource. +:any/nil+ functionality only applies
59
+ # when using named roles. (see Permit::NamedRoles).
60
+ # @option options [Symbol] :on alias for +:of+
61
+ # @option options [Symbol, <Symbol>] :to the action(s) to allow access to if this
62
+ # rule matches. +:all+ may be given to indicate that access is given to all
63
+ # actions if the rule matches. Actions will be expanded using the aliases
64
+ # defined in {Permit::Config.action_aliases}. The expansion operation is
65
+ # not recursive.
66
+ # @return [PermitRule] the rule that was created for the parameters.
67
+ # @raise [PermitConfigurationError] if +:to+ is not valid, or if the rule
68
+ # cannot be created.
69
+ def allow(roles, options = {})
70
+ actions = options.delete(:to)
71
+ rule = PermitRule.new(roles, options)
72
+ index_rule_by_actions @action_allow_rules, actions, rule
73
+ return rule
74
+ end
75
+
76
+ # Adds an deny rule for the given actions to the collection.
77
+ #
78
+ # @example Deny a person that is a member of a project from :show
79
+ # deny :person, :who => :is_member, :of => :project, :from => :show
80
+ # @example Deny a person with either of the named roles for a resource from writing.
81
+ # deny [:project_admin, :project_manager], :of => :project, :from => :write
82
+ #
83
+ # @param [Symbol, <Symbol>] roles the role(s) that the rule will apply to.
84
+ # @param [Hash] options the options used to build the rule.
85
+ # @option options [Symbol] :who the method to call on the target resource.
86
+ # @option options [Symbol] :that alias for +:who+
87
+ # @option options [Symbol] :of the name of the instance variable holding the target
88
+ # resource. If set to +:any+ then the match will apply to a person that has
89
+ # a matching role authorization for any resource. If not given, or set to
90
+ # +nil+, then the match will apply to a person that has a matching role
91
+ # authorization for a nil resource. :any/nil functionality only applies
92
+ # when using named roles. (see Permit::NamedRoles).
93
+ # @option options [Symbol] :on alias for +:of+
94
+ # @option options [Symbol, <Symbol>] :from the action(s) to deny access to if this
95
+ # rule matches. +:all+ may be given to indicate that access is denied to all
96
+ # actions if the rule matches. Actions will be expanded using the aliases
97
+ # defined in {Permit::Config.action_aliases}. The expansion operation is
98
+ # not recursive.
99
+ # @return [PermitRule] the rule that was created for the parameters.
100
+ # @raise [PermitConfigurationError] if +:from+ is not valid, or if the rule
101
+ # cannot be created.
102
+ def deny(roles, options = {})
103
+ actions = options.delete(:from)
104
+ rule = PermitRule.new(roles, options)
105
+ index_rule_by_actions @action_deny_rules, actions, rule
106
+ return rule
107
+ end
108
+
109
+ private
110
+ def index_rule_by_actions(action_rules, actions, rule)
111
+ determine_controlled_actions(actions).each do |a|
112
+ (action_rules[a] ||= []) << rule
113
+ end
114
+ end
115
+
116
+ def determine_controlled_actions(actions)
117
+ actions = permit_arrayify(actions).compact
118
+ raise PermitConfigurationError, "At least one action must be given to authorize access for." if actions.empty?
119
+ raise PermitConfigurationError, "If :all is specified for :to/:from then no other actions may be given." if (actions.include?(:all) && actions.size > 1)
120
+ expand_action_aliases actions
121
+ end
122
+
123
+ def expand_action_aliases(actions)
124
+ expanded_actions = actions.collect {|a| Permit::Config.action_aliases[a] || a}
125
+ expanded_actions.flatten.uniq
126
+ end
127
+
128
+ def has_action_rule_match?(type, rules, person, action, context_binding)
129
+ applicable_rules = (rules[action] || []) + (rules[:all] || [])
130
+ applicable_rules.each do |rule|
131
+ if rule.matches?(person, context_binding)
132
+ @logger.info "#{person.inspect} matched #{type.to_s} rule: #{rule.inspect}"
133
+ return true
134
+ end
135
+ end
136
+
137
+ return false
138
+ end
139
+ end
140
+
141
+ end