stffn-declarative_authorization 0.3.0 → 0.3.1

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