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.
- data/.gitignore +5 -0
- data/.yardopts +3 -0
- data/MIT-LICENSE +20 -0
- data/README.mkd +238 -0
- data/Rakefile +69 -0
- data/VERSION.yml +5 -0
- data/generators/permit/USAGE +40 -0
- data/generators/permit/permit_generator.rb +25 -0
- data/generators/permit/templates/authorization.rb +2 -0
- data/generators/permit/templates/initializer.rb +37 -0
- data/generators/permit/templates/migration.rb +28 -0
- data/generators/permit/templates/role.rb +2 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/models/association.rb +89 -0
- data/lib/models/authorizable.rb +31 -0
- data/lib/models/authorization.rb +54 -0
- data/lib/models/person.rb +148 -0
- data/lib/models/role.rb +59 -0
- data/lib/permit/controller.rb +132 -0
- data/lib/permit/permit_rule.rb +198 -0
- data/lib/permit/permit_rules.rb +141 -0
- data/lib/permit/support.rb +67 -0
- data/lib/permit.rb +134 -0
- data/permit.gemspec +91 -0
- data/rails/init.rb +7 -0
- data/spec/models/alternate_models_spec.rb +54 -0
- data/spec/models/authorizable_spec.rb +78 -0
- data/spec/models/authorization_spec.rb +77 -0
- data/spec/models/person_spec.rb +278 -0
- data/spec/models/role_spec.rb +121 -0
- data/spec/permit/controller_spec.rb +308 -0
- data/spec/permit/permit_rule_spec.rb +452 -0
- data/spec/permit/permit_rules_spec.rb +273 -0
- data/spec/permit_spec.rb +58 -0
- data/spec/spec_helper.rb +73 -0
- data/spec/support/helpers.rb +13 -0
- data/spec/support/models.rb +38 -0
- data/spec/support/permits_controller.rb +7 -0
- data/tasks/permit_tasks.rake +4 -0
- data/uninstall.rb +1 -0
- 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
|
data/lib/models/role.rb
ADDED
@@ -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
|