permit 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|