permit 0.9.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.
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