uhees-declarative_authorization 0.3.1 → 0.3.2.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,9 @@
1
+ * Change Support: Suggestion grouping, sort by affected users [sb]
2
+
3
+ * Changed context derived from objects to #class.name.tableize to fix STI [sb]
4
+
5
+ * Simplified controller authorization with filter_resource_access [sb]
6
+
1
7
  * Allow passing explicit context in addition to object in permitted_to? [Olly Lylo, sb]
2
8
 
3
9
  * Change Supporter: suggest changes to authorization rules [sb]
@@ -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
@@ -456,7 +470,7 @@ and Ubuntu) and has only been tested under Linux.
456
470
 
457
471
  = Help and Contact
458
472
 
459
- We have an issue tracker[http://stffn.lighthouseapp.com/projects/20733-declarative_authorization]
473
+ We have an issue tracker[http://github.com/stffn/declarative_authorization/issues]
460
474
  for bugs and feature requests as well as a
461
475
  Google Group[http://groups.google.com/group/declarative_authorization] for
462
476
  discussions on the usage of the plugin. You are very welcome to contribute.
@@ -475,11 +489,14 @@ sbartsch at tzi.org
475
489
  Thanks to
476
490
  * Eike Carls
477
491
  * Erik Dahlstrand
492
+ * Jeroen van Dijk
478
493
  * Jeremy Friesen
479
494
  * Brian Langenfeld
495
+ * Georg Ledermann
480
496
  * Geoff Longman
481
497
  * Olly Lylo
482
498
  * Mark Mansour
499
+ * Thomas Maurer
483
500
  * Mike Vincent
484
501
 
485
502
 
@@ -35,7 +35,10 @@ class AuthorizationRulesController < ApplicationController
35
35
  @users.sort! {|a, b| a.login <=> b.login }
36
36
 
37
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
38
+ @privileges = @privileges.collect do |priv|
39
+ priv = Authorization::DevelopmentSupport::AnalyzerEngine::Privilege.for_sym(priv, authorization_engine)
40
+ (priv.descendants + priv.ancestors).map(&:to_sym)
41
+ end.flatten.uniq
39
42
  @privileges.sort_by {|priv| priv.to_s}
40
43
  @privilege = params[:privilege].to_sym rescue @privileges.first
41
44
  @contexts = authorization_engine.auth_rules.collect {|rule| rule.contexts.to_a}.flatten.uniq
@@ -64,19 +67,40 @@ class AuthorizationRulesController < ApplicationController
64
67
  deserialize_changes(spec).flatten
65
68
  end
66
69
 
67
- users_keys = users_permission.keys
68
70
  analyzer = Authorization::DevelopmentSupport::ChangeSupporter.new(authorization_engine)
69
71
 
70
72
  privilege = params[:privilege].to_sym
71
73
  context = params[:context].to_sym
74
+ all_users = User.all
72
75
  @context = context
73
- @approaches = analyzer.find_approaches_for(:users => users_keys, :prohibited_actions => prohibited_actions) do
76
+ @approaches = analyzer.find_approaches_for(:users => all_users, :prohibited_actions => prohibited_actions) do
74
77
  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))
78
+ unless users_permission[all_users[idx]].nil?
79
+ args = [privilege, {:context => context, :user => user}]
80
+ assert(users_permission[all_users[idx]] ? permit?(*args) : !permit?(*args))
81
+ end
77
82
  end
78
83
  end
79
84
 
85
+ @affected_users = @approaches.each_with_object({}) do |approach, memo|
86
+ memo[approach] = approach.affected_users(authorization_engine, all_users, privilege, context).length
87
+ end
88
+ max_affected_users = @affected_users.values.max
89
+ if params[:affected_users]
90
+ @approaches = @approaches.sort_by do |approach|
91
+ affected_users_count = @affected_users[approach]
92
+ if params[:affected_users] == "many"
93
+ #approach.weight.to_f / [affected_users_count, 0.1].min
94
+ approach.weight + (max_affected_users - affected_users_count) * 10
95
+ else
96
+ #approach.weight * affected_users_count
97
+ approach.weight + affected_users_count * 10
98
+ end
99
+ end
100
+ end
101
+
102
+ @grouped_approaches = analyzer.group_approaches(@approaches)
103
+
80
104
  respond_to do |format|
81
105
  format.js do
82
106
  render :partial => 'suggestions'
@@ -46,7 +46,7 @@ module AuthorizationRulesHelper
46
46
 
47
47
  def navigation
48
48
  link_to("Rules", authorization_rules_path) << ' | ' <<
49
- link_to("Change Supporter", change_authorization_rules_path) << ' | ' <<
49
+ link_to("Change Support", change_authorization_rules_path) << ' | ' <<
50
50
  link_to("Graphical view", graph_authorization_rules_path) << ' | ' <<
51
51
  link_to("Usages", authorization_usages_path) #<< ' | ' <<
52
52
  # 'Edit | ' <<
@@ -118,8 +118,8 @@ module AuthorizationRulesHelper
118
118
 
119
119
  def prohibit_link (step, text, title, options)
120
120
  options[:with_removal] ?
121
- ' ' + link_to_function("[x]", "prohibit_action('#{serialize_action(step)}', '#{text}')",
122
- :class => 'unimportant', :title => title) :
121
+ link_to_function("[x]", "prohibit_action('#{serialize_action(step)}', '#{text}')",
122
+ :class => 'prohibit', :title => title) :
123
123
  ''
124
124
  end
125
125
 
@@ -149,6 +149,10 @@ module AuthorizationRulesHelper
149
149
  @changes && @changes[args[0]] && @changes[args[0]].include?(args[1..-1])
150
150
  end
151
151
 
152
+ def affected_users_count (approach)
153
+ @affected_users[approach]
154
+ end
155
+
152
156
  def auth_usage_info_classes (auth_info)
153
157
  classes = []
154
158
  if auth_info[:controller_permissions]
@@ -1,5 +1,5 @@
1
1
  <form>
2
- <h2>1. Choose permission to change</h2>
2
+ <h2>Which permission to change?</h2>
3
3
  <p class="action-options">
4
4
  <label>Privilege</label>
5
5
  <%= select_tag :privilege, options_for_select(@privileges.map(&:to_s).sort, @privilege.to_s) %>
@@ -7,10 +7,19 @@
7
7
  <label>On</label>
8
8
  <%= select_tag :context, options_for_select(@contexts.map(&:to_s).sort, @context.to_s) %>
9
9
  <br/>
10
- <%= link_to_function "Current permissions", "show_current_permissions()", :class => 'unimportant' %>
10
+ <label></label>
11
+ <%= link_to_function "Show current permissions", "show_current_permissions()", :class => 'unimportant' %>
12
+ <br/><br/>
13
+ How many users should be <strong>affected</strong>?
14
+ <br/>
15
+ <label></label>
16
+ <%= radio_button_tag :affected_users, :few, params[:affected_users] == 'few' %>
17
+ <label class="inline">A <strong>few</strong> users</label>
18
+ <%= radio_button_tag :affected_users, :many, params[:affected_users] == 'many' %>
19
+ <label class="inline"><strong>Many</strong> users</label>
11
20
  </p>
12
21
 
13
- <h2>2. Whose permission should be changed?</h2>
22
+ <h2>Whose permission should be changed?</h2>
14
23
  <table class="change-options">
15
24
  <thead>
16
25
  <tr>
@@ -32,6 +32,6 @@
32
32
  }
33
33
  <% end %>
34
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%"/>
35
+ <%= link_to_function "Hide", "$('graph-container').hide()", :class => 'important' %><br/>
36
+ <object id="graph" data="" type="image/svg+xml" style="max-width:100%;margin-top: 0.5em"/>
37
37
  </div>
@@ -4,21 +4,45 @@
4
4
  <% if @approaches.first.changes.empty? %>
5
5
  <p>No changes necessary.</p>
6
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 %>
7
+ <p class="unimportant">
8
+ <%= pluralize(@approaches.length, 'approach') %> in
9
+ <%= pluralize(@grouped_approaches.length, 'group') %>
10
+ <%= params[:affected_users] ? "– #{params[:affected_users] == 'few' ? "fewer" : "most"} affected users first" : "" %>
11
+ </p>
12
+ <ul>
13
+ <% @grouped_approaches.each_with_index do |grouped_approach, group_index| %>
14
+ <% ([grouped_approach.approach] + grouped_approach.similar_approaches).each_with_index do |approach, index| %>
15
+ <li <%= (group_index < 3 || params[:show_all]) && index == 0 ? '' : 'style="display: none"' %> class="<%= index == 0 ? 'primary' : "secondary" %> group-<%= group_index %>">
16
+ <!--<span class="ord"><%= (index + 1) %></span>-->
17
+ <span class="unimportant">Affected users</span> <strong><%= affected_users_count(approach) %></strong> &nbsp;
18
+ <span class="unimportant">Complexity</span> <strong><%= approach.weight %></strong> &nbsp;
19
+ <%= link_to_function "Diagram", "show_suggest_graph('#{serialize_changes(approach)}', '#{serialize_relevant_roles(approach)}', '#{@context}', relevant_user_ids())", :class => "show-approach" %>
20
+ <ul>
21
+ <% approach.changes.each do |action| %>
22
+ <% (action.to_a[0].is_a?(Enumerable) ? action.to_a : [action.to_a]).each do |step| %>
23
+ <li>
24
+ <%= describe_step(step.to_a, :with_removal => true) %>
25
+ </li>
26
+ <% end %>
27
+ <% end %>
28
+ </ul>
29
+ <% if index == 0 %>
30
+ <% if grouped_approach.similar_approaches.empty? %>
31
+ <span class="unimportant show-others-in-group">No further suggestions in this group</span>
32
+ <% else %>
33
+ <%= link_to_function "Show further #{pluralize grouped_approach.similar_approaches.length, "suggestion"} in this group", "show_group_approaches(#{group_index})", :class => "show-others-in-group" %>
34
+ <% end %>
35
+ <% end %>
36
+ <%= link_to_function "Hide further suggestions", "hide_group_approaches(#{group_index})" if index > 0 and index == grouped_approach.similar_approaches.length %>
37
+ </li>
38
+ <% if index == 0 and group_index == 2 and @grouped_approaches.length > 3 %>
39
+ <li <%= !params[:show_all] ? '' : 'style="display: none"' %>><%= link_to_function "Show further #{pluralize(@grouped_approaches.length - 3, 'group')}", "show_all_groups(); $(this).up().hide()" %></li>
40
+ <% end %>
41
+ <% end %>
42
+ <% end %>
43
+ </ul>
44
+
9
45
  <% end %>
10
46
  <% else %>
11
47
  <p><strong>No approach found.</strong></p>
12
48
  <% 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 %>
@@ -4,12 +4,13 @@
4
4
  <%= link_to_function "Hide", "$(this).up().hide()", :class => 'important' %>
5
5
  <%= link_to_function "Toggle stacked roles", "toggle_graph_params('suggest-graph', 'stacked_roles');" %>
6
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%"/>
7
+ <object id="suggest-graph" data="" type="image/svg+xml" style="max-width: 98%;max-height: 95%;margin-top: 0.5em"/>
8
8
  </div>
9
9
  <%= render 'show_graph' %>
10
10
 
11
11
  <style type="text/css">
12
12
  .action-options label { display: block; float: left; width: 7em; padding-bottom: 0.5em }
13
+ .action-options label.inline { display: inline; float: none }
13
14
  .action-options select { float: left; }
14
15
  .action-options br { clear: both; }
15
16
  .action-options { margin-bottom: 2em }
@@ -23,21 +24,33 @@
23
24
  .submit { margin-top: 0 }
24
25
  .submit input { font-weight: bold; font-size: 120% }
25
26
  #suggest-result {
26
- position: absolute; left: 60%; right: 10px;
27
- border-left: 2px solid grey;
28
- padding-left: 1em;
27
+ position: absolute; left: 55%; right: 10px;
28
+ border-left: 1px solid grey;
29
+ padding-left: 2em;
29
30
  z-index: 10;
30
31
  }
32
+ #suggest-result>ul { padding-left: 0 }
33
+ #suggest-result>ul>li { list-style: none; margin-bottom: 1em }
34
+ #suggest-result>ul>li.secondary { padding-left: 2em }
35
+ #suggest-result li { padding-left: 0 }
36
+ #suggest-result ul ul { padding-left: 2em }
37
+ #suggest-result .ord { float: left; display: block; width: 2em; font-weight: bold; color: grey }
38
+ .show-approach, .show-others-in-group { visibility: hidden }
39
+ .prohibit { text-decoration: none; visibility: hidden }
40
+ li:hover .show-approach, li:hover .prohibit,
41
+ li:hover .show-others-in-group { visibility: visible }
31
42
  #suggest-graph-container {
32
- background: white; border:1px solid #ccc;
43
+ background: white; border:1px solid grey;
33
44
  position:fixed; z-index: 20;
34
- left:10%; bottom: 10%; right: 10%; top: 10%
45
+ left:10%; bottom: 10%; right: 10%; top: 10%;
46
+ padding: 0.5em
35
47
  }
36
48
  #graph-container {
37
- background: white; margin: 1em; border:1px solid #ccc;
49
+ background: white; margin: 1em; border:1px solid grey; padding: 0.5em;
38
50
  max-width:50%; position:fixed; z-index: 20; right:0;
39
51
  }
40
- .unimportant, .remove { color: grey }
52
+ .unimportant, .remove, .prohibit { color: grey }
53
+ .important { font-weight: bold }
41
54
  .remove { cursor: pointer }
42
55
  </style>
43
56
  <% javascript_tag do %>
@@ -72,8 +85,9 @@
72
85
  function install_change_observers () {
73
86
  $$('#change select').each(function (el) {
74
87
  el.observe('change', function (event) {
88
+ var form = $('change').down('form');
75
89
  new Ajax.Updater({success: 'change'}, '<%= url_for %>', {
76
- parameters: { context: $F('context'), privilege: $F('privilege') },
90
+ parameters: Form.serializeElements(form.select('select').concat(form.getInputs('radio', 'affected_users')), true),
77
91
  method: 'get',
78
92
  onComplete: function () {
79
93
  install_change_observers();
@@ -98,6 +112,20 @@
98
112
  show_graph($F('privilege'), $F('context')/*, relevant_user_ids()*/);
99
113
  }
100
114
 
115
+ function show_all_groups () {
116
+ $$('#suggest-result>ul>li.primary').invoke("show");
117
+ }
118
+
119
+ function show_group_approaches (group_no) {
120
+ $$('#suggest-result>ul>li.secondary.group-' + group_no).invoke("show");
121
+ $$('#suggest-result>ul>li.primary.group-' + group_no + ' a.show-others-in-group').invoke("hide");
122
+ }
123
+
124
+ function hide_group_approaches (group_no) {
125
+ $$('#suggest-result>ul>li.secondary.group-' + group_no).invoke("hide");
126
+ $$('#suggest-result>ul>li.primary.group-' + group_no + ' a.show-others-in-group').invoke("show");
127
+ }
128
+
101
129
  function relevant_user_ids () {
102
130
  return $$('#change .user_id').collect(function (el) {
103
131
  return el.innerHTML;
@@ -33,10 +33,10 @@ module Authorization
33
33
  Thread.current["current_user"] = user
34
34
  end
35
35
 
36
- @@ignore_access_control = false
37
36
  # For use in test cases only
38
37
  def self.ignore_access_control (state = nil) # :nodoc:
39
- false
38
+ Thread.current["ignore_access_control"] = state unless state.nil?
39
+ Thread.current["ignore_access_control"] || false
40
40
  end
41
41
 
42
42
  def self.activate_authorization_rules_browser? # :nodoc:
@@ -117,10 +117,9 @@ module Authorization
117
117
  # Options:
118
118
  # [:+context+]
119
119
  # The context part of the privilege.
120
- # Defaults either to the +table_name+ of the given :+object+, if given.
121
- # That is, either :+users+ for :+object+ of type User.
122
- # Raises AuthorizationUsageError if
123
- # context is missing and not to be infered.
120
+ # Defaults either to the tableized +class_name+ of the given :+object+, if given.
121
+ # That is, :+users+ for :+object+ of type User.
122
+ # Raises AuthorizationUsageError if context is missing and not to be infered.
124
123
  # [:+object+] An context object to test attribute checks against.
125
124
  # [:+skip_attribute_test+]
126
125
  # Skips those attribute checks in the
@@ -153,7 +152,11 @@ module Authorization
153
152
  options[:object] = options[:object].new
154
153
  end
155
154
 
156
- options[:context] ||= options[:object] && options[:object].class.table_name.to_sym rescue NoMethodError
155
+ options[:context] ||= options[:object] && (
156
+ options[:object].class.respond_to?(:decl_auth_context) ?
157
+ options[:object].class.decl_auth_context :
158
+ options[:object].class.name.tableize.to_sym
159
+ ) rescue NoMethodError
157
160
 
158
161
  user, roles, privileges = user_roles_privleges_from_options(privilege, options)
159
162
 
@@ -100,7 +100,7 @@ module Authorization
100
100
  SMALL_ROLES_RATIO = 0.2
101
101
 
102
102
  def analyze_policy
103
- small_roles.count > 1 and small_roles.count.to_f / roles.count.to_f > SMALL_ROLES_RATIO
103
+ small_roles.length > 1 and small_roles.length.to_f / roles.length.to_f > SMALL_ROLES_RATIO
104
104
  end
105
105
 
106
106
  def message (object)
@@ -109,7 +109,7 @@ module Authorization
109
109
 
110
110
  private
111
111
  def small_roles
112
- roles.select {|role| role.rules.count < SMALL_ROLE_RULES_COUNT }
112
+ roles.select {|role| role.rules.length < SMALL_ROLE_RULES_COUNT }
113
113
  end
114
114
  end
115
115
 
@@ -8,7 +8,6 @@ module Authorization
8
8
  # * Objective function:
9
9
  # * affected user count,
10
10
  # * as specific as possible (roles, privileges)
11
- # -> counter-productive?
12
11
  # * as little changes as necessary
13
12
  # * Modify role, privilege hierarchy
14
13
  # * Merge, split roles
@@ -18,7 +17,7 @@ module Authorization
18
17
  # * group similar candidates: only show abstract methods?
19
18
  # * restructure GUI layout: more room for analyzing suggestions
20
19
  # * changelog, previous tests, etc.
21
- # * different permissions in tests
20
+ # * multiple permissions in tests
22
21
  # * Evaluation of approaches with Analyzer algorithms
23
22
  # * Authorization constraints
24
23
  #
@@ -41,6 +40,11 @@ module Authorization
41
40
  #
42
41
  class ChangeSupporter < AbstractAnalyzer
43
42
 
43
+ # Returns a list of possible approaches for changes to the current
44
+ # authorization rules that achieve a given goal. The goal is given as
45
+ # permission tests in the block. The instance method +users+ is available
46
+ # when the block is executed to refer to the then-current users, whose
47
+ # roles might have changed as one suggestion.
44
48
  def find_approaches_for (options, &tests)
45
49
  @prohibited_actions = (options[:prohibited_actions] || []).to_set
46
50
 
@@ -63,10 +67,29 @@ module Authorization
63
67
  end
64
68
 
65
69
  # remove subsets
66
-
67
70
  suggestions.sort!
68
71
  end
69
72
 
73
+ # Returns an array of GroupedApproaches for the given array of approaches.
74
+ # Only groups directly adjacent approaches
75
+ def group_approaches (approaches)
76
+ approaches.each_with_object([]) do |approach, grouped|
77
+ if grouped.last and grouped.last.approach.similar_to(approach)
78
+ grouped.last.similar_approaches << approach
79
+ else
80
+ grouped << GroupedApproach.new(approach)
81
+ end
82
+ end
83
+ end
84
+
85
+ class GroupedApproach
86
+ attr_accessor :approach, :similar_approaches
87
+ def initialize (approach)
88
+ @approach = approach
89
+ @similar_approaches = []
90
+ end
91
+ end
92
+
70
93
  class ApproachChecker
71
94
  attr_reader :users, :failed_tests
72
95
 
@@ -119,6 +142,15 @@ module Authorization
119
142
  res
120
143
  end
121
144
 
145
+ def affected_users (original_engine, original_users, privilege, context)
146
+ (0...@users.length).select do |i|
147
+ original_engine.permit?(privilege, :context => context,
148
+ :skip_attribute_test => true, :user => original_users[i]) !=
149
+ @engine.permit?(privilege, :context => context,
150
+ :skip_attribute_test => true, :user => @users[i])
151
+ end.collect {|i| original_users[i]}
152
+ end
153
+
122
154
  def initialize_copy (other)
123
155
  @engine = @engine.clone
124
156
  @users = @users.clone
@@ -171,7 +203,17 @@ module Authorization
171
203
  end
172
204
 
173
205
  def sort_value
174
- changes.sum(&:weight) + @failed_tests.length
206
+ weight + @failed_tests.length
207
+ end
208
+
209
+ def weight
210
+ changes.sum(&:weight)
211
+ end
212
+
213
+ def similar_to (other)
214
+ other.weight == weight and
215
+ other.changes.map {|change| change.class.name}.sort ==
216
+ changes.map {|change| change.class.name}.sort
175
217
  end
176
218
 
177
219
  def inspect