stffn-declarative_authorization 0.3.0 → 0.3.1

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 (35) hide show
  1. data/CHANGELOG +9 -0
  2. data/README.rdoc +22 -6
  3. data/app/controllers/authorization_rules_controller.rb +135 -14
  4. data/app/helpers/authorization_rules_helper.rb +96 -13
  5. data/app/views/authorization_rules/_change.erb +49 -0
  6. data/app/views/authorization_rules/_show_graph.erb +37 -0
  7. data/app/views/authorization_rules/_suggestion.erb +9 -0
  8. data/app/views/authorization_rules/_suggestions.erb +24 -0
  9. data/app/views/authorization_rules/change.html.erb +124 -0
  10. data/app/views/authorization_rules/graph.dot.erb +23 -4
  11. data/app/views/authorization_rules/graph.html.erb +1 -0
  12. data/app/views/authorization_rules/index.html.erb +3 -2
  13. data/app/views/authorization_usages/index.html.erb +2 -11
  14. data/config/routes.rb +2 -1
  15. data/lib/declarative_authorization/authorization.rb +87 -35
  16. data/lib/declarative_authorization/development_support/analyzer.rb +252 -0
  17. data/lib/declarative_authorization/development_support/change_analyzer.rb +253 -0
  18. data/lib/declarative_authorization/development_support/change_supporter.rb +578 -0
  19. data/lib/declarative_authorization/development_support/development_support.rb +243 -0
  20. data/lib/declarative_authorization/helper.rb +6 -2
  21. data/lib/declarative_authorization/in_controller.rb +254 -26
  22. data/lib/declarative_authorization/in_model.rb +27 -2
  23. data/lib/declarative_authorization/maintenance.rb +22 -8
  24. data/lib/declarative_authorization/obligation_scope.rb +14 -9
  25. data/lib/declarative_authorization/reader.rb +10 -2
  26. data/test/authorization_test.rb +44 -0
  27. data/test/controller_filter_resource_access_test.rb +385 -0
  28. data/test/controller_test.rb +14 -6
  29. data/test/helper_test.rb +21 -0
  30. data/test/maintenance_test.rb +26 -0
  31. data/test/model_test.rb +28 -0
  32. data/test/test_helper.rb +14 -1
  33. metadata +15 -5
  34. data/lib/declarative_authorization/authorization_rules_analyzer.rb +0 -138
  35. data/test/authorization_rules_analyzer_test.rb +0 -123
@@ -0,0 +1,37 @@
1
+ <% javascript_tag do %>
2
+ function show_graph (privilege, context, user_ids) {
3
+ var params = {
4
+ privilege_hierarchy: 1,
5
+ highlight_privilege: privilege,
6
+ filter_contexts: context
7
+ };
8
+ if (user_ids)
9
+ params['user_ids[]'] = user_ids;
10
+ show_graph_with_params('graph', params);
11
+ }
12
+
13
+ var graph_params = {};
14
+ function show_graph_with_params (graph_id, params) {
15
+ graph_params[graph_id] = params;
16
+ base_url = "<%= graph_authorization_rules_path('svg') %>";
17
+ $(graph_id).data = base_url + '?' + Object.toQueryString(params);
18
+ $(graph_id).up().show();
19
+ }
20
+
21
+ function update_graph_params (graph_id, params) {
22
+ show_graph_with_params(graph_id,
23
+ $H(graph_params[graph_id] || {}).merge(params).toObject());
24
+ }
25
+
26
+ function toggle_graph_params (graph_id) {
27
+ var opts = {}
28
+ $A(arguments).slice(1).each(function (param) {
29
+ opts[param] = graph_params[graph_id][param] ? null : '1';
30
+ });
31
+ update_graph_params(graph_id, opts)
32
+ }
33
+ <% end %>
34
+ <div id="graph-container" style="display:none">
35
+ <%= link_to_function "Hide", "$('graph-container').hide()" %><br/>
36
+ <object id="graph" data="" type="image/svg+xml" style="max-width:100%"/>
37
+ </div>
@@ -0,0 +1,9 @@
1
+ <ul>
2
+ <% suggestion.changes.each do |action| %>
3
+ <% (action.to_a[0].is_a?(Enumerable) ? action.to_a : [action.to_a]).each do |step| %>
4
+ <li>
5
+ <%= describe_step(step.to_a, :with_removal => true) %>
6
+ </li>
7
+ <% end %>
8
+ <% end %>
9
+ </ul>
@@ -0,0 +1,24 @@
1
+ <h2>Suggestions</h2>
2
+
3
+ <% if @approaches.length > 0 %>
4
+ <% if @approaches.first.changes.empty? %>
5
+ <p>No changes necessary.</p>
6
+ <% else %>
7
+ <p>Suggested changes (<%= link_to_function "show", "show_suggest_graph('#{serialize_changes(@approaches.first)}', '#{serialize_relevant_roles(@approaches.first)}', '#{@context}', relevant_user_ids())" %>):</p>
8
+ <%= render "suggestion", :object => @approaches.first %>
9
+ <% end %>
10
+ <% else %>
11
+ <p><strong>No approach found.</strong></p>
12
+ <% end %>
13
+
14
+ <% if @approaches.length > 1 %>
15
+ <p <%= !params[:show_all] ? '' : 'style="display: none"' %>><a href="#" onclick="$(this).up().hide();$('more-suggestions').show();return false">Show other <%= pluralize(@approaches.length - 1, 'approach') %></a></p>
16
+ <div id="more-suggestions" <%= params[:show_all] ? '' : 'style="display: none"' %>>
17
+ <% @approaches[1..-1].each_with_index do |approach, index| %>
18
+ <p>
19
+ <%= (index + 2).ordinalize %> best approach (<%= pluralize(approach.changes.length, 'step') %>, <%= link_to_function "show", "show_suggest_graph('#{serialize_changes(approach)}', '#{serialize_relevant_roles(approach)}', '#{@context}', relevant_user_ids())" %>)
20
+ <%= render "suggestion", :object => approach %>
21
+ </p>
22
+ <% end %>
23
+ </div>
24
+ <% end %>
@@ -0,0 +1,124 @@
1
+ <h1>Suggestions on Authorization Rules Change</h1>
2
+ <p><%= navigation %></p>
3
+ <div style="display:none" id="suggest-graph-container">
4
+ <%= link_to_function "Hide", "$(this).up().hide()", :class => 'important' %>
5
+ <%= link_to_function "Toggle stacked roles", "toggle_graph_params('suggest-graph', 'stacked_roles');" %>
6
+ <%= link_to_function "Toggle only users' roles", "toggle_graph_params('suggest-graph', 'only_relevant_roles');" %><br/>
7
+ <object id="suggest-graph" data="" type="image/svg+xml" style="max-width: 98%;max-height: 95%"/>
8
+ </div>
9
+ <%= render 'show_graph' %>
10
+
11
+ <style type="text/css">
12
+ .action-options label { display: block; float: left; width: 7em; padding-bottom: 0.5em }
13
+ .action-options select { float: left; }
14
+ .action-options br { clear: both; }
15
+ .action-options { margin-bottom: 2em }
16
+ .change-options { margin-bottom: 1em }
17
+ .change-options td.user_id { display: none }
18
+ .change-options td { padding-right: 1em; padding-left: 0.5em }
19
+ .change-options thead td { border-bottom: 1px solid lightgrey }
20
+ .change-options thead td.choose { text-align: center; font-weight: bold }
21
+ .change-options tr.permitted td.yes,
22
+ .change-options tr.not-permitted td.no { background: #FFE599 }
23
+ .submit { margin-top: 0 }
24
+ .submit input { font-weight: bold; font-size: 120% }
25
+ #suggest-result {
26
+ position: absolute; left: 60%; right: 10px;
27
+ border-left: 2px solid grey;
28
+ padding-left: 1em;
29
+ z-index: 10;
30
+ }
31
+ #suggest-graph-container {
32
+ background: white; border:1px solid #ccc;
33
+ position:fixed; z-index: 20;
34
+ left:10%; bottom: 10%; right: 10%; top: 10%
35
+ }
36
+ #graph-container {
37
+ background: white; margin: 1em; border:1px solid #ccc;
38
+ max-width:50%; position:fixed; z-index: 20; right:0;
39
+ }
40
+ .unimportant, .remove { color: grey }
41
+ .remove { cursor: pointer }
42
+ </style>
43
+ <% javascript_tag do %>
44
+ function suggest_changes (refresh) {
45
+ if (!refresh)
46
+ $("suggest-result").innerHTML = "Searching...";
47
+ $("suggest-result").show();
48
+ new Ajax.Updater({success: 'suggest-result'}, '<%= suggest_change_authorization_rules_path %>', {
49
+ method: 'get',
50
+ onFailure: function(request) {
51
+ $("suggest-result").innerHTML = "Error while searching."
52
+ },
53
+ parameters: $H($('change').down('form').serialize(true)).merge(refresh ? {show_all: "true"} : {}).toQueryString()
54
+ });
55
+ if (!refresh)
56
+ location.hash = 'suggest-result';
57
+ }
58
+
59
+ function show_suggest_graph (changes, filter_roles_params, filter_context, user_ids) {
60
+ var params = {changes: changes, highlight_privilege: $F('privilege')};
61
+ if (filter_context)
62
+ params['filter_contexts'] = filter_context;
63
+ if (user_ids)
64
+ params['user_ids[]'] = user_ids;
65
+ show_graph_with_params('suggest-graph', params);
66
+ }
67
+
68
+ document.observe('dom:loaded', function() {
69
+ install_change_observers();
70
+ });
71
+
72
+ function install_change_observers () {
73
+ $$('#change select').each(function (el) {
74
+ el.observe('change', function (event) {
75
+ new Ajax.Updater({success: 'change'}, '<%= url_for %>', {
76
+ parameters: { context: $F('context'), privilege: $F('privilege') },
77
+ method: 'get',
78
+ onComplete: function () {
79
+ install_change_observers();
80
+ if ($('graph-container').visible())
81
+ show_current_permissions();
82
+ }
83
+ });
84
+ });
85
+ });
86
+ $('prohibited_actions').observe('click', function (event) {
87
+ var target = event.findElement();
88
+ if (target.hasClassName('remove')) {
89
+ target.up().remove();
90
+ if ($('prohibited_actions').childElements().length == 0)
91
+ $('prohibited_actions').previous().hide();
92
+ suggest_changes();
93
+ }
94
+ });
95
+ }
96
+
97
+ function show_current_permissions () {
98
+ show_graph($F('privilege'), $F('context')/*, relevant_user_ids()*/);
99
+ }
100
+
101
+ function relevant_user_ids () {
102
+ return $$('#change .user_id').collect(function (el) {
103
+ return el.innerHTML;
104
+ }).reject(function (id) {
105
+ return $('user_' + id + '_permission_undetermined').checked;
106
+ });
107
+ }
108
+
109
+ var prohibited_action_template =
110
+ new Template('<li>#{description} <span class="remove">[x]</span><input type="hidden" name="prohibited_action[]" value="#{action}"></li>');
111
+ function prohibit_action (action, description) {
112
+ $('prohibited_actions').previous().show();
113
+ $('prohibited_actions').insert(
114
+ {bottom: prohibited_action_template.evaluate({action: action, description: description}) }
115
+ );
116
+ suggest_changes(true);
117
+ }
118
+ <% end %>
119
+
120
+ <div id="suggest-result" style="display:none"></div>
121
+
122
+ <div id="change">
123
+ <%= render 'change' %>
124
+ </div>
@@ -7,20 +7,39 @@ digraph rules {
7
7
  ranksep = "0.3"
8
8
  //concentrate = true
9
9
  rankdir = TB
10
+
11
+ <% unless @users.blank? %>
12
+ {
13
+ rank = source
14
+ node [shape=polygon,style=filled,fillcolor="#eeeeee"]
15
+ <% @users.each do |user| %>
16
+ "<%= user.login %>"
17
+ <% end %>
18
+ }
19
+ <% end %>
20
+
10
21
  {
11
22
  node [shape=ellipse,style=filled]
12
- //rank = source
23
+ <%= @stacked_roles ? '' : "rank = same" %>
13
24
  <% @roles.each do |role| %>
14
25
  "<%= role.inspect %>" [fillcolor="<%= role_fill_color(role) %>"]
15
26
  // ,URL="javascript:set_filter({roles: '<%= role %>'})"
16
27
  <% end %>
17
28
  <% @roles.each do |role| %>
18
- <% (@role_hierarchy[role] || []).each do |lower_role| %>
19
- "<%= role.inspect %>" -> "<%= lower_role.inspect %>" [constraint=false,arrowhead=empty]
29
+ <% (@role_hierarchy[role] || []).select {|lower_role| @roles.include?(lower_role)}.each do |lower_role| %>
30
+ "<%= role.inspect %>" -> "<%= lower_role.inspect %>" [arrowhead=empty]
20
31
  <% end %>
21
32
  <% end %>
22
33
  }
23
34
 
35
+ <% unless @users.blank? %>
36
+ <% @users.each do |user| %>
37
+ <% user.role_symbols.select {|role| @roles.include?(role)}.each do |role| %>
38
+ "<%= user.login %>" -> "<%= role.inspect %>" [color="<%= has_changed(:assign_role_to_user, role, user.login) ? '#00dd00' : (has_changed(:remove_role_from_user, role, user.login) ? '#dd0000' : '#000000') %>"]
39
+ <% end %>
40
+ <% end %>
41
+ <% end %>
42
+
24
43
  <% @contexts.each do |context| %>
25
44
  subgraph cluster_<%= context %> {
26
45
  label = "<%= context.inspect %>"
@@ -43,7 +62,7 @@ digraph rules {
43
62
 
44
63
  <% @roles.each do |role| %>
45
64
  <% (@role_privs[role] || []).each do |context, privilege, unconditionally, attribute_string| %>
46
- "<%= role.inspect %>" -> <%= privilege %>_<%= context %> [color="<%= role_color(role) %>", minlen=3<%= ", arrowhead=opendot, URL=\"javascript:\", edgetooltip=\"#{attribute_string.gsub('"','')}\"" unless unconditionally %>]
65
+ "<%= role.inspect %>" -> <%= privilege %>_<%= context %> [color="<%= privilege_color(privilege, context, role) %>", minlen=3<%= ", arrowhead=opendot, URL=\"javascript:\", edgetooltip=\"#{attribute_string.gsub('"','')}\"" unless unconditionally %>]
47
66
  <% end %>
48
67
  <% end %>
49
68
  }
@@ -30,6 +30,7 @@
30
30
  <%= select_tag "filter_contexts", options_for_select([["All contexts",'']] + controller.authorization_engine.auth_rules.collect {|ar| ar.contexts.to_a}.flatten.uniq.map(&:to_s).sort), :onchange => 'update_graph(this.form)' %>
31
31
  <%= check_box_tag "effective_role_privs", "1", false, :onclick => 'update_graph(this.form)' %> <%= label_tag "effective_role_privs", "Effective privileges" %>
32
32
  <%= check_box_tag "privilege_hierarchy", "1", false, :onclick => 'update_graph(this.form)' %> <%= label_tag "privilege_hierarchy", "Show full privilege hierarchy" %>
33
+ <%= check_box_tag "stacked_roles", "1", false, :onclick => 'update_graph(this.form)' %> <%= label_tag "stacked_roles", "Stacked roles" %>
33
34
  <% end %>
34
35
  </p>
35
36
  <div style="margin: 1em;border:1px solid #ccc;max-width:95%">
@@ -9,8 +9,9 @@
9
9
  pre .proc {color: #0a0;}
10
10
  pre .privilege, pre .context {font-weight: bold}
11
11
  pre .preproc, pre .comment, pre .comment span {color: grey !important;}
12
- pre .note {color: #a00; position:absolute; cursor: help }
12
+ pre .note {color: #a00; position:absolute; cursor: help; left: 15px }
13
+ pre.with-notes {padding-left: 35px}
13
14
  </style>
14
- <pre>
15
+ <pre class="with-notes">
15
16
  <%= policy_analysis_hints(syntax_highlight(h(@auth_rules_script)), @auth_rules_script) %>
16
17
  </pre>
@@ -1,7 +1,5 @@
1
1
  <h1>Authorization Usage</h1>
2
- <div style="margin: 1em;border:1px solid #ccc;max-width:50%;position:fixed;right:0;display:none">
3
- <object id="graph" data="<%= url_for :format => 'svg' %>" type="image/svg+xml" style="max-width:100%"/>
4
- </div>
2
+ <%= render 'authorization_rules/show_graph' %>
5
3
  <p>Filter rules in actions by controller:</p>
6
4
  <p><%= navigation %></p>
7
5
  <style type="text/css">
@@ -13,15 +11,8 @@
13
11
  /*.auth-usages tr.catch-all td.privilege,*/
14
12
  .auth-usages tr.default-privilege td.privilege,
15
13
  .auth-usages tr.default-context td.context { color: #888888 }
14
+ #graph-container {margin: 1em; border:1px solid #ccc; max-width:50%; position:fixed; right:0;}
16
15
  </style>
17
- <% javascript_tag do %>
18
- function show_graph (privilege, context) {
19
- base_url = "<%= graph_authorization_rules_path('svg') %>";
20
- $('graph').data = base_url + '?privilege_hierarchy=1&highlight_privilege=' +
21
- privilege + '&filter_contexts=' + context;
22
- $('graph').up().show();
23
- }
24
- <% end %>
25
16
  <table class="auth-usages">
26
17
  <% @auth_usages_by_controller.keys.sort {|c1, c2| c1.name <=> c2.name}.each do |controller| %>
27
18
  <% default_context = controller.controller_name.to_sym rescue nil %>
data/config/routes.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  ActionController::Routing::Routes.draw do |map|
2
2
  if Authorization::activate_authorization_rules_browser?
3
- map.resources :authorization_rules, :only => :index, :collection => {:graph => :get}
3
+ map.resources :authorization_rules, :only => [:index],
4
+ :collection => {:graph => :get, :change => :get, :suggest_change => :get}
4
5
  map.resources :authorization_usages, :only => :index
5
6
  end
6
7
  end
@@ -58,7 +58,8 @@ module Authorization
58
58
  #
59
59
  class Engine
60
60
  attr_reader :roles, :role_titles, :role_descriptions, :privileges,
61
- :privilege_hierarchy, :auth_rules, :role_hierarchy, :rev_priv_hierarchy
61
+ :privilege_hierarchy, :auth_rules, :role_hierarchy, :rev_priv_hierarchy,
62
+ :rev_role_hierarchy
62
63
 
63
64
  # If +reader+ is not given, a new one is created with the default
64
65
  # authorization configuration of +AUTH_DSL_FILE+. If given, may be either
@@ -82,6 +83,7 @@ module Authorization
82
83
 
83
84
  @role_titles = reader.auth_rules_reader.role_titles
84
85
  @role_descriptions = reader.auth_rules_reader.role_descriptions
86
+ @reader = reader
85
87
 
86
88
  # {[priv, ctx] => [priv, ...]}
87
89
  @rev_priv_hierarchy = {}
@@ -91,6 +93,20 @@ module Authorization
91
93
  @rev_priv_hierarchy[val] << key
92
94
  end
93
95
  end
96
+ @rev_role_hierarchy = {}
97
+ @role_hierarchy.each do |higher_role, lower_roles|
98
+ lower_roles.each do |role|
99
+ (@rev_role_hierarchy[role] ||= []) << higher_role
100
+ end
101
+ end
102
+ end
103
+
104
+ def initialize_copy (from) # :nodoc:
105
+ [
106
+ :privileges, :privilege_hierarchy, :roles, :role_hierarchy, :role_titles,
107
+ :role_descriptions, :rev_priv_hierarchy, :rev_role_hierarchy
108
+ ].each {|attr| instance_variable_set(:"@#{attr}", from.send(attr).clone) }
109
+ @auth_rules = from.auth_rules.collect {|rule| rule.clone}
94
110
  end
95
111
 
96
112
  # Returns true if privilege is met by the current user. Raises
@@ -143,7 +159,7 @@ module Authorization
143
159
 
144
160
  # find a authorization rule that matches for at least one of the roles and
145
161
  # at least one of the given privileges
146
- attr_validator = AttributeValidator.new(self, user, options[:object])
162
+ attr_validator = AttributeValidator.new(self, user, options[:object], privilege, options[:context])
147
163
  rules = matching_auth_rules(roles, privileges, options[:context])
148
164
  if rules.empty?
149
165
  raise NotAuthorized, "No matching rules found for #{privilege} for #{user.inspect} " +
@@ -152,21 +168,8 @@ module Authorization
152
168
  end
153
169
 
154
170
  # Test each rule in turn to see whether any one of them is satisfied.
155
- grant_permission = rules.any? do |rule|
156
- begin
157
- options[:skip_attribute_test] or
158
- rule.attributes.empty? or
159
- rule.attributes.send(rule.join_operator == :and ? :all? : :any?) do |attr|
160
- begin
161
- attr.validate?( attr_validator )
162
- rescue NilAttributeValueError => e
163
- nil # Bumping up against a nil attribute value flunks the rule.
164
- end
165
- end
166
- end
167
- end
168
- unless grant_permission
169
- raise AttributeAuthorizationError, "#{privilege} not allowed for #{user.inspect} on #{options[:object].inspect}."
171
+ unless rules.any? {|rule| rule.validate?(attr_validator, options[:skip_attribute_test])}
172
+ raise AttributeAuthorizationError, "#{privilege} not allowed for #{user.inspect} on #{(options[:object] || options[:context]).inspect}."
170
173
  end
171
174
  true
172
175
  end
@@ -201,17 +204,9 @@ module Authorization
201
204
  def obligations (privilege, options = {})
202
205
  options = {:context => nil}.merge(options)
203
206
  user, roles, privileges = user_roles_privleges_from_options(privilege, options)
204
- attr_validator = AttributeValidator.new(self, user, nil, options[:context])
207
+ attr_validator = AttributeValidator.new(self, user, nil, privilege, options[:context])
205
208
  matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
206
- obligations = rule.attributes.collect {|attr| attr.obligation(attr_validator) }
207
- if rule.join_operator == :and and !obligations.empty?
208
- merged_obligation = obligations.first
209
- obligations[1..-1].each do |obligation|
210
- merged_obligation = merged_obligation.deep_merge(obligation)
211
- end
212
- obligations = [merged_obligation]
213
- end
214
- obligations.empty? ? [{}] : obligations
209
+ rule.obligations(attr_validator)
215
210
  end.flatten
216
211
  end
217
212
 
@@ -263,11 +258,12 @@ module Authorization
263
258
  end
264
259
 
265
260
  class AttributeValidator # :nodoc:
266
- attr_reader :user, :object, :engine, :context
267
- def initialize (engine, user, object = nil, context = nil)
261
+ attr_reader :user, :object, :engine, :context, :privilege
262
+ def initialize (engine, user, object = nil, privilege = nil, context = nil)
268
263
  @engine = engine
269
264
  @user = user
270
265
  @object = object
266
+ @privilege = privilege
271
267
  @context = context
272
268
  end
273
269
 
@@ -281,15 +277,16 @@ module Authorization
281
277
  def user_roles_privleges_from_options(privilege, options)
282
278
  options = {
283
279
  :user => nil,
284
- :context => nil
280
+ :context => nil,
281
+ :user_roles => nil
285
282
  }.merge(options)
286
283
  user = options[:user] || Authorization.current_user
287
284
  privileges = privilege.is_a?(Array) ? privilege : [privilege]
288
285
 
289
- raise AuthorizationUsageError, "No user object given (#{user.inspect})" \
290
- unless user
286
+ raise AuthorizationUsageError, "No user object given (#{user.inspect}) or " +
287
+ "set through Authorization.current_user" unless user
291
288
 
292
- roles = flatten_roles(roles_for(user))
289
+ roles = options[:user_roles] || flatten_roles(roles_for(user))
293
290
  privileges = flatten_privileges privileges, options[:context]
294
291
  [user, roles, privileges]
295
292
  end
@@ -329,14 +326,24 @@ module Authorization
329
326
  end
330
327
 
331
328
  class AuthorizationRule
332
- attr_reader :attributes, :contexts, :role, :privileges, :join_operator
329
+ attr_reader :attributes, :contexts, :role, :privileges, :join_operator,
330
+ :source_file, :source_line
333
331
 
334
- def initialize (role, privileges = [], contexts = nil, join_operator = :or)
332
+ def initialize (role, privileges = [], contexts = nil, join_operator = :or,
333
+ options = {})
335
334
  @role = role
336
335
  @privileges = Set.new(privileges)
337
336
  @contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
338
337
  @join_operator = join_operator
339
338
  @attributes = []
339
+ @source_file = options[:source_file]
340
+ @source_line = options[:source_line]
341
+ end
342
+
343
+ def initialize_copy (from)
344
+ @privileges = @privileges.clone
345
+ @contexts = @contexts.clone
346
+ @attributes = @attributes.collect {|attribute| attribute.clone }
340
347
  end
341
348
 
342
349
  def append_privileges (privs)
@@ -353,6 +360,29 @@ module Authorization
353
360
  not (@privileges & privs).empty?
354
361
  end
355
362
 
363
+ def validate? (attr_validator, skip_attribute = false)
364
+ skip_attribute or @attributes.empty? or
365
+ @attributes.send(@join_operator == :and ? :all? : :any?) do |attr|
366
+ begin
367
+ attr.validate?(attr_validator)
368
+ rescue NilAttributeValueError => e
369
+ nil # Bumping up against a nil attribute value flunks the rule.
370
+ end
371
+ end
372
+ end
373
+
374
+ def obligations (attr_validator)
375
+ obligations = @attributes.collect {|attr| attr.obligation(attr_validator) }.flatten
376
+ if @join_operator == :and and !obligations.empty?
377
+ merged_obligation = obligations.first
378
+ obligations[1..-1].each do |obligation|
379
+ merged_obligation = merged_obligation.deep_merge(obligation)
380
+ end
381
+ obligations = [merged_obligation]
382
+ end
383
+ obligations.empty? ? [{}] : obligations
384
+ end
385
+
356
386
  def to_long_s
357
387
  attributes.collect {|attr| attr.to_long_s } * "; "
358
388
  end
@@ -365,6 +395,10 @@ module Authorization
365
395
  def initialize (conditions_hash)
366
396
  @conditions_hash = conditions_hash
367
397
  end
398
+
399
+ def initialize_copy (from)
400
+ @conditions_hash = deep_hash_clone(@conditions_hash)
401
+ end
368
402
 
369
403
  def validate? (attr_validator, object = nil, hash = nil)
370
404
  object ||= attr_validator.object
@@ -480,6 +514,20 @@ module Authorization
480
514
  "#{object.inspect} for validating attribute: #{e}"
481
515
  end
482
516
  end
517
+
518
+ def deep_hash_clone (hash)
519
+ hash.inject({}) do |memo, (key, val)|
520
+ memo[key] = case val
521
+ when Hash
522
+ deep_hash_clone(val)
523
+ when NilClass, Symbol
524
+ val
525
+ else
526
+ val.clone
527
+ end
528
+ memo
529
+ end
530
+ end
483
531
  end
484
532
 
485
533
  # An attribute condition that uses existing rules to decide validation
@@ -493,6 +541,10 @@ module Authorization
493
541
  @attr_hash = attr_or_hash
494
542
  end
495
543
 
544
+ def initialize_copy (from)
545
+ @attr_hash = deep_hash_clone(@attr_hash) if @attr_hash.is_a?(Hash)
546
+ end
547
+
496
548
  def validate? (attr_validator, object = nil, hash_or_attr = nil)
497
549
  object ||= attr_validator.object
498
550
  hash_or_attr ||= @attr_hash