uhees-declarative_authorization 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/CHANGELOG +77 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +490 -0
  4. data/Rakefile +43 -0
  5. data/app/controllers/authorization_rules_controller.rb +235 -0
  6. data/app/controllers/authorization_usages_controller.rb +23 -0
  7. data/app/helpers/authorization_rules_helper.rb +183 -0
  8. data/app/views/authorization_rules/_change.erb +49 -0
  9. data/app/views/authorization_rules/_show_graph.erb +37 -0
  10. data/app/views/authorization_rules/_suggestion.erb +9 -0
  11. data/app/views/authorization_rules/_suggestions.erb +24 -0
  12. data/app/views/authorization_rules/change.html.erb +124 -0
  13. data/app/views/authorization_rules/graph.dot.erb +68 -0
  14. data/app/views/authorization_rules/graph.html.erb +40 -0
  15. data/app/views/authorization_rules/index.html.erb +17 -0
  16. data/app/views/authorization_usages/index.html.erb +36 -0
  17. data/authorization_rules.dist.rb +20 -0
  18. data/config/routes.rb +7 -0
  19. data/garlic_example.rb +20 -0
  20. data/init.rb +5 -0
  21. data/lib/declarative_authorization.rb +15 -0
  22. data/lib/declarative_authorization/authorization.rb +630 -0
  23. data/lib/declarative_authorization/development_support/analyzer.rb +252 -0
  24. data/lib/declarative_authorization/development_support/change_analyzer.rb +253 -0
  25. data/lib/declarative_authorization/development_support/change_supporter.rb +578 -0
  26. data/lib/declarative_authorization/development_support/development_support.rb +243 -0
  27. data/lib/declarative_authorization/helper.rb +60 -0
  28. data/lib/declarative_authorization/in_controller.rb +367 -0
  29. data/lib/declarative_authorization/in_model.rb +150 -0
  30. data/lib/declarative_authorization/maintenance.rb +188 -0
  31. data/lib/declarative_authorization/obligation_scope.rb +297 -0
  32. data/lib/declarative_authorization/rails_legacy.rb +14 -0
  33. data/lib/declarative_authorization/reader.rb +438 -0
  34. data/test/authorization_test.rb +823 -0
  35. data/test/controller_test.rb +418 -0
  36. data/test/dsl_reader_test.rb +157 -0
  37. data/test/helper_test.rb +154 -0
  38. data/test/maintenance_test.rb +41 -0
  39. data/test/model_test.rb +1171 -0
  40. data/test/schema.sql +53 -0
  41. data/test/test_helper.rb +103 -0
  42. metadata +104 -0
data/Rakefile ADDED
@@ -0,0 +1,43 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the authorization plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the authorization plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'Authorization'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.options << '--charset' << 'utf-8'
21
+ rdoc.rdoc_files.include('README.rdoc')
22
+ rdoc.rdoc_files.include('CHANGELOG')
23
+ rdoc.rdoc_files.include('lib/**/*.rb')
24
+ end
25
+
26
+ # load up garlic if it's here
27
+ if File.directory?(File.join(File.dirname(__FILE__), 'garlic'))
28
+ require File.join(File.dirname(__FILE__), 'garlic/lib/garlic_tasks')
29
+ require File.join(File.dirname(__FILE__), 'garlic')
30
+ end
31
+
32
+ desc "clone the garlic repo (for running ci tasks)"
33
+ task :get_garlic do
34
+ sh "git clone git://github.com/ianwhite/garlic.git garlic"
35
+ end
36
+
37
+ desc "Expand filelist in src gemspec"
38
+ task :build_gemspec do
39
+ gemspec_data = File.read("declarative_authorization.gemspec.src")
40
+ gemspec_data.gsub!(/\.files = (.*)/) {|m| ".files = #{eval($1).inspect}"}
41
+ File.open("declarative_authorization.gemspec", "w") {|f| f.write(gemspec_data)}
42
+ end
43
+
@@ -0,0 +1,235 @@
1
+ if Authorization::activate_authorization_rules_browser?
2
+
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})
6
+
7
+ begin
8
+ # for nice auth_rules output:
9
+ require "parse_tree"
10
+ require "parse_tree_extensions"
11
+ require "ruby2ruby"
12
+ rescue LoadError; end
13
+
14
+ class AuthorizationRulesController < ApplicationController
15
+ unloadable
16
+
17
+ filter_access_to :all, :require => :read
18
+ def index
19
+ respond_to do |format|
20
+ format.html do
21
+ @auth_rules_script = File.read("#{RAILS_ROOT}/config/authorization_rules.rb")
22
+ end
23
+ end
24
+ end
25
+
26
+ def graph
27
+ if params[:format] == "svg"
28
+ render :text => dot_to_svg(auth_to_dot(graph_options)),
29
+ :content_type => "image/svg+xml"
30
+ end
31
+ end
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
+
87
+ private
88
+ def auth_to_dot (options = {})
89
+ options = {
90
+ :effective_role_privs => true,
91
+ :privilege_hierarchy => false,
92
+ :stacked_roles => false,
93
+ :only_relevant_contexts => true,
94
+ :only_relevant_roles => false,
95
+ :filter_roles => nil,
96
+ :filter_contexts => nil,
97
+ :highlight_privilege => nil,
98
+ :changes => nil,
99
+ :users => nil
100
+ }.merge(options)
101
+
102
+ @has_changes = options[:changes] && !options[:changes].empty?
103
+ @highlight_privilege = options[:highlight_privilege]
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
125
+
126
+ @contexts = engine.auth_rules.
127
+ collect {|ar| ar.contexts.to_a}.flatten.uniq
128
+ @contexts = @contexts.select {|c| c == options[:filter_contexts] } if options[:filter_contexts]
129
+ @context_privs = {}
130
+ @role_privs = {}
131
+ engine.auth_rules.each do |auth_rule|
132
+ @role_privs[auth_rule.role] ||= []
133
+ auth_rule.contexts.
134
+ select {|c| options[:filter_contexts].nil? or c == options[:filter_contexts]}.
135
+ each do |context|
136
+ @context_privs[context] ||= []
137
+ @context_privs[context] += auth_rule.privileges.to_a
138
+ @context_privs[context].uniq!
139
+ @role_privs[auth_rule.role] += auth_rule.privileges.collect {|p| [context, p, auth_rule.attributes.empty?, auth_rule.to_long_s]}
140
+ end
141
+ end
142
+
143
+ if options[:effective_role_privs]
144
+ @roles.each do |role|
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!
149
+ end
150
+ end
151
+ end
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
+
158
+ if options[:only_relevant_contexts]
159
+ @contexts.delete_if do |context|
160
+ @roles.all? {|role| !@role_privs[role] || !@role_privs[role].any? {|info| info[0] == context}}
161
+ end
162
+ end
163
+
164
+ if options[:privilege_hierarchy]
165
+ @context_privs.each do |context, privs|
166
+ privs.each do |priv|
167
+ context_lower_privs = (@privilege_hierarchy[priv] || []).
168
+ select {|p,c| c.nil? or c == context}.
169
+ collect {|p,c| p}
170
+ privs.concat(context_lower_privs).uniq!
171
+ end
172
+ end
173
+ end
174
+
175
+ render_to_string :template => 'authorization_rules/graph.dot.erb', :layout => false
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
191
+
192
+ def dot_to_svg (dot_data)
193
+ gv = IO.popen("#{Authorization.dot_path} -q -Tsvg", "w+")
194
+ gv.puts dot_data
195
+ gv.close_write
196
+ gv.read
197
+ rescue IOError, Errno::EPIPE => e
198
+ raise Exception, "Error in call to graphviz: #{e}"
199
+ end
200
+
201
+ def graph_options
202
+ {
203
+ :effective_role_privs => !params[:effective_role_privs].blank?,
204
+ :privilege_hierarchy => !params[:privilege_hierarchy].blank?,
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]),
208
+ :filter_contexts => params[:filter_contexts].blank? ? nil : params[:filter_contexts].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)}
212
+ }
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
231
+ end
232
+
233
+ else
234
+ class AuthorizationRulesController < ApplicationController; end
235
+ end # activate_authorization_rules_browser?
@@ -0,0 +1,23 @@
1
+ if Authorization::activate_authorization_rules_browser?
2
+
3
+ require File.join(File.dirname(__FILE__), %w{.. .. lib declarative_authorization maintenance})
4
+
5
+ class AuthorizationUsagesController < ApplicationController
6
+ unloadable
7
+
8
+ helper :authorization_rules
9
+ filter_access_to :all, :require => :read
10
+ # TODO set context?
11
+
12
+ def index
13
+ respond_to do |format|
14
+ format.html do
15
+ @auth_usages_by_controller = Authorization::Maintenance::Usage.usages_by_controller
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ else
22
+ class AuthorizationUsagesController < ApplicationController; end
23
+ end # activate_authorization_rules_browser?
@@ -0,0 +1,183 @@
1
+ module AuthorizationRulesHelper
2
+ def syntax_highlight (rules)
3
+ regexps = {
4
+ :constant => [/(:)(\w+)/],
5
+ :proc => ['role', 'authorization', 'privileges'],
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
+ :special => ['user', 'true', 'false'],
9
+ :preproc => ['do', 'end', /()(=&gt;)/, /()(\{)/, /()(\})/, /()(\[)/, /()(\])/],
10
+ :comment => [/()(#.*$)/]#,
11
+ #:privilege => [:read],
12
+ #:context => [:conferences]
13
+ }
14
+ regexps.each do |name, res|
15
+ res.each do |re|
16
+ rules.gsub!(
17
+ re.is_a?(String) ? Regexp.new("(^|[^:])\\b(#{Regexp.escape(re)})\\b") :
18
+ (re.is_a?(Symbol) ? Regexp.new("()(:#{Regexp.escape(re.to_s)})\\b") : re),
19
+ "\\1<span class=\"#{name}\">\\2</span>")
20
+ end
21
+ end
22
+ rules
23
+ end
24
+
25
+ def policy_analysis_hints (marked_up, policy_data)
26
+ analyzer = Authorization::DevelopmentSupport::Analyzer.new(controller.authorization_engine)
27
+ analyzer.analyze(policy_data)
28
+ marked_up_by_line = marked_up.split("\n")
29
+ reports_by_line = analyzer.reports.inject({}) do |memo, report|
30
+ memo[report.line || 1] ||= []
31
+ memo[report.line || 1] << report
32
+ memo
33
+ end
34
+ reports_by_line.each do |line, reports|
35
+ text = reports.collect {|report| "#{report.type}: #{report.message}"} * " "
36
+ note = %Q{<span class="note" title="#{h text}">[i]</span>}
37
+ marked_up_by_line[line - 1] = note + marked_up_by_line[line - 1]
38
+ end
39
+ marked_up_by_line * "\n"
40
+ end
41
+
42
+ def link_to_graph (title, options = {})
43
+ type = options[:type] || ''
44
+ link_to_function title, "$$('object')[0].data = '#{url_for :action => 'index', :format => 'svg', :type => type}'"
45
+ end
46
+
47
+ def navigation
48
+ link_to("Rules", authorization_rules_path) << ' | ' <<
49
+ link_to("Change Supporter", change_authorization_rules_path) << ' | ' <<
50
+ link_to("Graphical view", graph_authorization_rules_path) << ' | ' <<
51
+ link_to("Usages", authorization_usages_path) #<< ' | ' <<
52
+ # 'Edit | ' <<
53
+ # link_to("XACML export", :action => 'index', :format => 'xacml')
54
+ end
55
+
56
+ def role_color (role, fill = false)
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]
74
+ end
75
+ end
76
+
77
+ def role_fill_color (role)
78
+ role_color(role, true)
79
+ end
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
+
152
+ def auth_usage_info_classes (auth_info)
153
+ classes = []
154
+ if auth_info[:controller_permissions]
155
+ if auth_info[:controller_permissions][0]
156
+ classes << "catch-all" if auth_info[:controller_permissions][0].actions.include?(:all)
157
+ classes << "default-privilege" unless auth_info[:controller_permissions][0].privilege
158
+ classes << "default-context" unless auth_info[:controller_permissions][0].context
159
+ classes << "no-attribute-check" unless auth_info[:controller_permissions][0].attribute_check
160
+ end
161
+ else
162
+ classes << "unprotected"
163
+ end
164
+ classes * " "
165
+ end
166
+
167
+ def auth_usage_info_title (auth_info)
168
+ titles = []
169
+ if auth_usage_info_classes(auth_info) =~ /unprotected/
170
+ titles << "No filter_access_to call protects this action"
171
+ end
172
+ if auth_usage_info_classes(auth_info) =~ /no-attribute-check/
173
+ titles << "Action is not protected with attribute check"
174
+ end
175
+ if auth_usage_info_classes(auth_info) =~ /default-privilege/
176
+ titles << "Privilege set automatically from action name by :all rule"
177
+ end
178
+ if auth_usage_info_classes(auth_info) =~ /default-context/
179
+ titles << "Context set automatically from controller name by filter_access_to call without :context option"
180
+ end
181
+ titles * ". "
182
+ end
183
+ end