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
data/CHANGELOG CHANGED
@@ -1,3 +1,12 @@
1
+ * Simplified controller authorization with filter_resource_access [sb]
2
+
3
+ * Allow passing explicit context in addition to object in permitted_to? [Olly Lylo, sb]
4
+
5
+ * Change Supporter: suggest changes to authorization rules [sb]
6
+
7
+ * Added permitted_to!/? in model [Eike Carls]
8
+
9
+ * New test helper: should_(not_)_be_allowed_to(privilege, object_or_context) [sb]
1
10
 
2
11
  ** RELEASE 0.3 (April 20, 2009) **
3
12
 
data/README.rdoc CHANGED
@@ -79,12 +79,26 @@ generated yourself or at http://www.tzi.org/~sbartsch/declarative_authorization
79
79
 
80
80
  == Controller
81
81
 
82
- If authentication is in place, enabling user-specific access control may be
83
- as simple as one call to filter_access_to :all which simply requires the
84
- according privileges for present actions. E.g. the privilege index_users is
85
- required for action index. This works as a first default configuration
86
- for RESTful controllers, with these privileges easily handled in the
87
- authorization configuration, which will be described below.
82
+ If authentication is in place, there are two ways to enable user-specific
83
+ access control on controller actions. For resource controllers, which more
84
+ or less follow the CRUD pattern, +filter_resource_access+ is the simplest
85
+ approach. It sets up instance variables in before filters and calls
86
+ filter_access_to with the appropriate parameters to protect the CRUD methods.
87
+
88
+ class EmployeesController < ApplicationController
89
+ filter_resource_access
90
+ ...
91
+ end
92
+
93
+ See Authorization::AuthorizationInController::ClassMethods for options on
94
+ nested resources and custom member and collection actions.
95
+
96
+ If you prefer less magic or your controller has no resemblance with the resource
97
+ controllers, directly calling filter_access_to may be the better option. Examples
98
+ are given in the following. E.g. the privilege index users is required for
99
+ action index. This works as a first default configuration for RESTful
100
+ controllers, with these privileges easily handled in the authorization
101
+ configuration, which will be described below.
88
102
 
89
103
  class EmployeesController < ApplicationController
90
104
  filter_access_to :all
@@ -473,10 +487,12 @@ sbartsch at tzi.org
473
487
  = Contributors
474
488
 
475
489
  Thanks to
490
+ * Eike Carls
476
491
  * Erik Dahlstrand
477
492
  * Jeremy Friesen
478
493
  * Brian Langenfeld
479
494
  * Geoff Longman
495
+ * Olly Lylo
480
496
  * Mark Mansour
481
497
  * Mike Vincent
482
498
 
@@ -1,6 +1,8 @@
1
1
  if Authorization::activate_authorization_rules_browser?
2
2
 
3
- require File.join(File.dirname(__FILE__), %w{.. .. lib declarative_authorization authorization_rules_analyzer})
3
+ require File.join(File.dirname(__FILE__), %w{.. .. lib declarative_authorization development_support analyzer})
4
+ require File.join(File.dirname(__FILE__), %w{.. .. lib declarative_authorization development_support change_supporter})
5
+ require File.join(File.dirname(__FILE__), %w{.. .. lib declarative_authorization development_support development_support})
4
6
 
5
7
  begin
6
8
  # for nice auth_rules output:
@@ -28,29 +30,105 @@ class AuthorizationRulesController < ApplicationController
28
30
  end
29
31
  end
30
32
 
33
+ def change
34
+ @users = find_all_users
35
+ @users.sort! {|a, b| a.login <=> b.login }
36
+
37
+ @privileges = authorization_engine.auth_rules.collect {|rule| rule.privileges.to_a}.flatten.uniq
38
+ @privileges = @privileges.collect {|priv| Authorization::DevelopmentSupport::AnalyzerEngine::Privilege.for_sym(priv, authorization_engine).descendants.map(&:to_sym) }.flatten.uniq
39
+ @privileges.sort_by {|priv| priv.to_s}
40
+ @privilege = params[:privilege].to_sym rescue @privileges.first
41
+ @contexts = authorization_engine.auth_rules.collect {|rule| rule.contexts.to_a}.flatten.uniq
42
+ @context = params[:context].to_sym rescue @contexts.first
43
+
44
+ respond_to do |format|
45
+ format.html
46
+ format.js do
47
+ render :partial => 'change'
48
+ end
49
+ end
50
+ end
51
+
52
+ def suggest_change
53
+ users_permission = params[:user].inject({}) do |memo, (user_id, data)|
54
+ if data[:permission] != "undetermined"
55
+ begin
56
+ memo[find_user_by_id(user_id)] = (data[:permission] == 'yes')
57
+ rescue ActiveRecord::NotFound
58
+ end
59
+ end
60
+ memo
61
+ end
62
+
63
+ prohibited_actions = (params[:prohibited_action] || []).collect do |spec|
64
+ deserialize_changes(spec).flatten
65
+ end
66
+
67
+ users_keys = users_permission.keys
68
+ analyzer = Authorization::DevelopmentSupport::ChangeSupporter.new(authorization_engine)
69
+
70
+ privilege = params[:privilege].to_sym
71
+ context = params[:context].to_sym
72
+ @context = context
73
+ @approaches = analyzer.find_approaches_for(:users => users_keys, :prohibited_actions => prohibited_actions) do
74
+ users.each_with_index do |user, idx|
75
+ args = [privilege, {:context => context, :user => user}]
76
+ assert(users_permission[users_keys[idx]] ? permit?(*args) : !permit?(*args))
77
+ end
78
+ end
79
+
80
+ respond_to do |format|
81
+ format.js do
82
+ render :partial => 'suggestions'
83
+ end
84
+ end
85
+ end
86
+
31
87
  private
32
88
  def auth_to_dot (options = {})
33
89
  options = {
34
90
  :effective_role_privs => true,
35
91
  :privilege_hierarchy => false,
92
+ :stacked_roles => false,
36
93
  :only_relevant_contexts => true,
94
+ :only_relevant_roles => false,
37
95
  :filter_roles => nil,
38
96
  :filter_contexts => nil,
39
- :highlight_privilege => nil
97
+ :highlight_privilege => nil,
98
+ :changes => nil,
99
+ :users => nil
40
100
  }.merge(options)
41
101
 
102
+ @has_changes = options[:changes] && !options[:changes].empty?
42
103
  @highlight_privilege = options[:highlight_privilege]
43
- @roles = authorization_engine.roles
44
- @roles = @roles.select {|r| r == options[:filter_roles] } if options[:filter_roles]
45
- @role_hierarchy = authorization_engine.role_hierarchy
46
- @privilege_hierarchy = authorization_engine.privilege_hierarchy
104
+ @stacked_roles = options[:stacked_roles]
105
+
106
+ @users = options[:users]
107
+
108
+ engine = authorization_engine.clone
109
+ @changes = replay_changes(engine, @users, options[:changes]) if options[:changes]
110
+
111
+ options[:filter_roles] ||= @users.collect {|user| user.role_symbols}.flatten.uniq if options[:only_relevant_roles] and @users
112
+
113
+ filter_roles_flattened = nil
114
+ if options[:filter_roles]
115
+ filter_roles_flattened = options[:filter_roles].collect do |role_sym|
116
+ Authorization::DevelopmentSupport::AnalyzerEngine::Role.for_sym(role_sym, engine).
117
+ ancestors.map(&:to_sym) + [role_sym]
118
+ end.flatten.uniq
119
+ end
120
+
121
+ @roles = engine.roles
122
+ @roles = @roles.select {|r| filter_roles_flattened.include?(r) } if options[:filter_roles]
123
+ @role_hierarchy = engine.role_hierarchy
124
+ @privilege_hierarchy = engine.privilege_hierarchy
47
125
 
48
- @contexts = authorization_engine.auth_rules.
126
+ @contexts = engine.auth_rules.
49
127
  collect {|ar| ar.contexts.to_a}.flatten.uniq
50
128
  @contexts = @contexts.select {|c| c == options[:filter_contexts] } if options[:filter_contexts]
51
129
  @context_privs = {}
52
130
  @role_privs = {}
53
- authorization_engine.auth_rules.each do |auth_rule|
131
+ engine.auth_rules.each do |auth_rule|
54
132
  @role_privs[auth_rule.role] ||= []
55
133
  auth_rule.contexts.
56
134
  select {|c| options[:filter_contexts].nil? or c == options[:filter_contexts]}.
@@ -64,15 +142,23 @@ class AuthorizationRulesController < ApplicationController
64
142
 
65
143
  if options[:effective_role_privs]
66
144
  @roles.each do |role|
67
- @role_privs[role] ||= []
68
- (@role_hierarchy[role] || []).each do |lower_role|
69
- @role_privs[role].concat(@role_privs[lower_role]).uniq!
145
+ role = Authorization::DevelopmentSupport::AnalyzerEngine::Role.for_sym(role, engine)
146
+ @role_privs[role.to_sym] ||= []
147
+ role.ancestors.each do |lower_role|
148
+ @role_privs[role.to_sym].concat(@role_privs[lower_role.to_sym]).uniq!
70
149
  end
71
150
  end
72
151
  end
73
152
 
153
+ @roles.delete_if do |role|
154
+ role = Authorization::DevelopmentSupport::AnalyzerEngine::Role.for_sym(role, engine)
155
+ ([role] + role.ancestors).all? {|inner_role| @role_privs[inner_role.to_sym].blank? }
156
+ end
157
+
74
158
  if options[:only_relevant_contexts]
75
- @contexts.delete_if {|context| @roles.all? {|role| !@role_privs[role] || !@role_privs[role].any? {|info| info[0] == context}}}
159
+ @contexts.delete_if do |context|
160
+ @roles.all? {|role| !@role_privs[role] || !@role_privs[role].any? {|info| info[0] == context}}
161
+ end
76
162
  end
77
163
 
78
164
  if options[:privilege_hierarchy]
@@ -88,6 +174,20 @@ class AuthorizationRulesController < ApplicationController
88
174
 
89
175
  render_to_string :template => 'authorization_rules/graph.dot.erb', :layout => false
90
176
  end
177
+
178
+ def replay_changes (engine, users, changes)
179
+ changes.inject({}) do |memo, info|
180
+ case info[0]
181
+ when :add_privilege, :add_role
182
+ Authorization::DevelopmentSupport::AnalyzerEngine.apply_change(engine, info)
183
+ when :assign_role_to_user
184
+ user = users.find {|u| u.login == info[2]}
185
+ user.role_symbols << info[1] if user
186
+ end
187
+ (memo[info[0]] ||= Set.new) << info[1..-1]
188
+ memo
189
+ end
190
+ end
91
191
 
92
192
  def dot_to_svg (dot_data)
93
193
  gv = IO.popen("#{Authorization.dot_path} -q -Tsvg", "w+")
@@ -102,11 +202,32 @@ class AuthorizationRulesController < ApplicationController
102
202
  {
103
203
  :effective_role_privs => !params[:effective_role_privs].blank?,
104
204
  :privilege_hierarchy => !params[:privilege_hierarchy].blank?,
105
- :filter_roles => params[:filter_roles].blank? ? nil : params[:filter_roles].to_sym,
205
+ :stacked_roles => !params[:stacked_roles].blank?,
206
+ :only_relevant_roles => !params[:only_relevant_roles].blank?,
207
+ :filter_roles => params[:filter_roles].blank? ? nil : (params[:filter_roles].is_a?(Array) ? params[:filter_roles].map(&:to_sym) : [params[:filter_roles].to_sym]),
106
208
  :filter_contexts => params[:filter_contexts].blank? ? nil : params[:filter_contexts].to_sym,
107
- :highlight_privilege => params[:highlight_privilege].blank? ? nil : params[:highlight_privilege].to_sym
209
+ :highlight_privilege => params[:highlight_privilege].blank? ? nil : params[:highlight_privilege].to_sym,
210
+ :changes => deserialize_changes(params[:changes]),
211
+ :users => params[:user_ids] && params[:user_ids].collect {|user_id| find_user_by_id(user_id)}
108
212
  }
109
213
  end
214
+
215
+ def deserialize_changes (changes)
216
+ if changes
217
+ changes.split(';').collect do |info|
218
+ info.split(',').collect do |info_part|
219
+ info_part[0,1] == ':' ? info_part[1..-1].to_sym : info_part
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ def find_user_by_id (id)
226
+ User.find(id)
227
+ end
228
+ def find_all_users
229
+ User.all.select {|user| !user.login.blank?}
230
+ end
110
231
  end
111
232
 
112
233
  else
@@ -3,8 +3,8 @@ module AuthorizationRulesHelper
3
3
  regexps = {
4
4
  :constant => [/(:)(\w+)/],
5
5
  :proc => ['role', 'authorization', 'privileges'],
6
- :statement => ['has_permission_on', 'if_attribute', 'includes', 'privilege', 'to'],
7
- :operator => ['is', 'contains'],
6
+ :statement => ['has_permission_on', 'if_attribute', 'if_permitted_to', 'includes', 'privilege', 'to'],
7
+ :operator => ['is', 'contains', 'is_in', 'is_not', 'is_not_in', 'intersects'],
8
8
  :special => ['user', 'true', 'false'],
9
9
  :preproc => ['do', 'end', /()(=&gt;)/, /()(\{)/, /()(\})/, /()(\[)/, /()(\])/],
10
10
  :comment => [/()(#.*$)/]#,
@@ -23,16 +23,17 @@ module AuthorizationRulesHelper
23
23
  end
24
24
 
25
25
  def policy_analysis_hints (marked_up, policy_data)
26
- analyzer = Authorization::Analyzer.new(controller.authorization_engine)
26
+ analyzer = Authorization::DevelopmentSupport::Analyzer.new(controller.authorization_engine)
27
27
  analyzer.analyze(policy_data)
28
28
  marked_up_by_line = marked_up.split("\n")
29
29
  reports_by_line = analyzer.reports.inject({}) do |memo, report|
30
- memo[report.line] ||= []
31
- memo[report.line] << report
30
+ memo[report.line || 1] ||= []
31
+ memo[report.line || 1] << report
32
32
  memo
33
33
  end
34
34
  reports_by_line.each do |line, reports|
35
- note = %Q{<span class="note" title="#{reports.first.type}: #{reports.first.message}">[i]</span>}
35
+ text = reports.collect {|report| "#{report.type}: #{report.message}"} * " "
36
+ note = %Q{<span class="note" title="#{h text}">[i]</span>}
36
37
  marked_up_by_line[line - 1] = note + marked_up_by_line[line - 1]
37
38
  end
38
39
  marked_up_by_line * "\n"
@@ -45,6 +46,7 @@ module AuthorizationRulesHelper
45
46
 
46
47
  def navigation
47
48
  link_to("Rules", authorization_rules_path) << ' | ' <<
49
+ link_to("Change Supporter", change_authorization_rules_path) << ' | ' <<
48
50
  link_to("Graphical view", graph_authorization_rules_path) << ' | ' <<
49
51
  link_to("Usages", authorization_usages_path) #<< ' | ' <<
50
52
  # 'Edit | ' <<
@@ -52,20 +54,101 @@ module AuthorizationRulesHelper
52
54
  end
53
55
 
54
56
  def role_color (role, fill = false)
55
- fill_colors = %w{#ffdddd #ddffdd #ddddff #ffffdd #ffddff #ddffff}
56
- colors = %w{#dd0000 #00dd00 #0000dd #dddd00 #dd00dd #00dddd}
57
- @@role_colors ||= {}
58
- @@role_colors[role] ||= begin
59
- idx = @@role_colors.length % colors.length
60
- [colors[idx], fill_colors[idx]]
57
+ if @has_changes
58
+ if has_changed(:add_role, role)
59
+ fill ? '#ddffdd' : '#000000'
60
+ elsif has_changed(:remove_role, role)
61
+ fill ? '#ffdddd' : '#000000'
62
+ else
63
+ fill ? '#ddddff' : '#000000'
64
+ end
65
+ else
66
+ fill_colors = %w{#ffdddd #ddffdd #ddddff #ffffdd #ffddff #ddffff}
67
+ colors = %w{#dd0000 #00dd00 #0000dd #dddd00 #dd00dd #00dddd}
68
+ @@role_colors ||= {}
69
+ @@role_colors[role] ||= begin
70
+ idx = @@role_colors.length % colors.length
71
+ [colors[idx], fill_colors[idx]]
72
+ end
73
+ @@role_colors[role][fill ? 1 : 0]
61
74
  end
62
- @@role_colors[role][fill ? 1 : 0]
63
75
  end
64
76
 
65
77
  def role_fill_color (role)
66
78
  role_color(role, true)
67
79
  end
68
80
 
81
+ def privilege_color (privilege, context, role)
82
+ has_changed(:add_privilege, privilege, context, role) ? '#00dd00' :
83
+ (has_changed(:remove_privilege, privilege, context, role) ? '#dd0000' :
84
+ role_color(role))
85
+ end
86
+
87
+ def describe_step (step, options = {})
88
+ options = {:with_removal => false}.merge(options)
89
+
90
+ case step[0]
91
+ when :add_privilege
92
+ dont_assign = prohibit_link(step[0,3],
93
+ "Add privilege <strong>#{h step[1].to_sym.inspect} #{h step[2].to_sym.inspect}</strong> to any role",
94
+ "Don't suggest adding #{h step[1].to_sym.inspect} #{h step[2].to_sym.inspect}.", options)
95
+ "Add privilege <strong>#{h step[1].inspect} #{h step[2].inspect}</strong>#{dont_assign} to role <strong>#{h step[3].to_sym.inspect}</strong>"
96
+ when :remove_privilege
97
+ dont_remove = prohibit_link(step[0,3],
98
+ "Remove privilege <strong>#{h step[1].to_sym.inspect} #{h step[2].to_sym.inspect}</strong> from any role",
99
+ "Don't suggest removing #{h step[1].to_sym.inspect} #{h step[2].to_sym.inspect}.", options)
100
+ "Remove privilege <strong>#{h step[1].inspect} #{h step[2].inspect}</strong>#{dont_remove} from role <strong>#{h step[3].to_sym.inspect}</strong>"
101
+ when :add_role
102
+ "New role <strong>#{h step[1].to_sym.inspect}</strong>"
103
+ when :assign_role_to_user
104
+ dont_assign = prohibit_link(step[0,2],
105
+ "Assign role <strong>#{h step[1].to_sym.inspect}</strong> to any user",
106
+ "Don't suggest assigning #{h step[1].to_sym.inspect}.", options)
107
+ "Assign role <strong>#{h step[1].to_sym.inspect}</strong>#{dont_assign} to <strong>#{h readable_step_info(step[2])}</strong>"
108
+ when :remove_role_from_user
109
+ dont_remove = prohibit_link(step[0,2],
110
+ "Remove role <strong>#{h step[1].to_sym.inspect}</strong> from any user",
111
+ "Don't suggest removing #{h step[1].to_sym.inspect}.", options)
112
+ "Remove role <strong>#{h step[1].to_sym.inspect}</strong>#{dont_remove} from <strong>#{h readable_step_info(step[2])}</strong>"
113
+ else
114
+ step.collect {|info| readable_step_info(info) }.map {|str| h str } * ', '
115
+ end + prohibit_link(step, options[:with_removal] ? "#{escape_javascript(describe_step(step))}" : '',
116
+ "Don't suggest this action.", options)
117
+ end
118
+
119
+ def prohibit_link (step, text, title, options)
120
+ options[:with_removal] ?
121
+ ' ' + link_to_function("[x]", "prohibit_action('#{serialize_action(step)}', '#{text}')",
122
+ :class => 'unimportant', :title => title) :
123
+ ''
124
+ end
125
+
126
+ def readable_step_info (info)
127
+ case info
128
+ when Symbol then info.inspect
129
+ when User then info.login
130
+ else info.to_sym.inspect
131
+ end
132
+ end
133
+
134
+ def serialize_changes (approach)
135
+ changes = approach.changes.collect {|step| step.to_a.first.is_a?(Enumerable) ? step.to_a : [step.to_a]}
136
+ changes.collect {|multi_step| multi_step.collect {|step| serialize_action(step) }}.flatten * ';'
137
+ end
138
+
139
+ def serialize_action (step)
140
+ step.collect {|info| readable_step_info(info) } * ','
141
+ end
142
+
143
+ def serialize_relevant_roles (approach)
144
+ {:filter_roles => (Authorization::DevelopmentSupport::AnalyzerEngine.relevant_roles(approach.engine, approach.users).
145
+ map(&:to_sym) + [:new_role_for_change_analyzer]).uniq}.to_param
146
+ end
147
+
148
+ def has_changed (*args)
149
+ @changes && @changes[args[0]] && @changes[args[0]].include?(args[1..-1])
150
+ end
151
+
69
152
  def auth_usage_info_classes (auth_info)
70
153
  classes = []
71
154
  if auth_info[:controller_permissions]
@@ -0,0 +1,49 @@
1
+ <form>
2
+ <h2>1. Choose permission to change</h2>
3
+ <p class="action-options">
4
+ <label>Privilege</label>
5
+ <%= select_tag :privilege, options_for_select(@privileges.map(&:to_s).sort, @privilege.to_s) %>
6
+ <br/>
7
+ <label>On</label>
8
+ <%= select_tag :context, options_for_select(@contexts.map(&:to_s).sort, @context.to_s) %>
9
+ <br/>
10
+ <%= link_to_function "Current permissions", "show_current_permissions()", :class => 'unimportant' %>
11
+ </p>
12
+
13
+ <h2>2. Whose permission should be changed?</h2>
14
+ <table class="change-options">
15
+ <thead>
16
+ <tr>
17
+ <td>User</td>
18
+ <td>Current roles</td>
19
+ <td class="choose">?</td>
20
+ <td class="choose">Yes</td>
21
+ <td class="choose">No</td>
22
+ </tr>
23
+ </thead>
24
+ <tbody>
25
+ <% @users.each do |user| %>
26
+ <tr class="<%= controller.authorization_engine.permit?(@privilege, :context => @context, :user => user, :skip_attribute_test => true) ? 'permitted' : 'not-permitted' %>">
27
+ <td class="user_id"><%=h user.id %></td>
28
+ <td style="font-weight:bold"><%=h user.login %></td>
29
+ <td><%=h user.role_symbols * ', ' %></td>
30
+ <td><%= radio_button_tag "user[#{user.id}][permission]", "undetermined", true %></td>
31
+ <td class="yes"><%= radio_button_tag "user[#{user.id}][permission]", "yes" %></td>
32
+ <td class="no"><%= radio_button_tag "user[#{user.id}][permission]", "no" %></td>
33
+ </tr>
34
+ <% end %>
35
+ <tr>
36
+ <td colspan="5" style="text-align:right; padding-top:0.5em" class="unimportant">
37
+ <span style="background: #FFE599;">&nbsp; &nbsp;</span> Current permission
38
+ </td>
39
+ </tr>
40
+ </tbody>
41
+ </table>
42
+
43
+ <h2 style="display:none">Prohibited actions</h2>
44
+ <ul id="prohibited_actions"></ul>
45
+
46
+ <p class="submit">
47
+ <%= button_to_function "Suggest Changes", "suggest_changes()" %>
48
+ </p>
49
+ </form>