declarative_authorization 0.4.1 → 0.5
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.
- data/CHANGELOG +15 -0
- data/README.rdoc +8 -7
- data/Rakefile +0 -8
- data/app/controllers/authorization_rules_controller.rb +1 -1
- data/app/helpers/authorization_rules_helper.rb +3 -3
- data/config/routes.rb +5 -2
- data/lib/declarative_authorization.rb +2 -0
- data/lib/declarative_authorization/authorization.rb +20 -14
- data/lib/declarative_authorization/in_controller.rb +2 -5
- data/lib/declarative_authorization/in_model.rb +13 -10
- data/lib/declarative_authorization/maintenance.rb +4 -4
- data/lib/declarative_authorization/obligation_scope.rb +51 -13
- data/lib/declarative_authorization/rails_legacy.rb +9 -1
- data/lib/declarative_authorization/railsengine.rb +6 -0
- data/lib/declarative_authorization/reader.rb +61 -7
- data/lib/tasks/authorization_tasks.rake +82 -0
- data/test/authorization_test.rb +108 -0
- data/test/controller_test.rb +4 -3
- data/test/dsl_reader_test.rb +21 -0
- data/test/helper_test.rb +1 -0
- data/test/model_test.rb +229 -91
- data/test/test_helper.rb +44 -13
- metadata +23 -9
data/CHANGELOG
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
|
2
|
+
** RELEASE 0.5 (July 21, 2010) **
|
3
|
+
|
4
|
+
* Ruby 1.9.2 compatibility [sb]
|
5
|
+
|
6
|
+
* Comparisons in authorization roles: lt, lte, gt, gte [aepstein,hollownest]
|
7
|
+
|
8
|
+
* DSL optimization: allow array being passed to to
|
9
|
+
|
10
|
+
* Omnipotent roles [timcharper]
|
11
|
+
|
12
|
+
* Meaningful error in case of missing authorization rules file [timcharper]
|
13
|
+
|
14
|
+
* Rails 3 support [sb]
|
15
|
+
|
1
16
|
* Support shallow nested resources [jjb]
|
2
17
|
|
3
18
|
* Allow multiple authorization rules files [kaichen]
|
data/README.rdoc
CHANGED
@@ -277,8 +277,9 @@ Privilege hierarchies may be context-specific, e.g. applicable to :employees.
|
|
277
277
|
privilege :manage, :employees, :includes => :increase_salary
|
278
278
|
end
|
279
279
|
|
280
|
-
For more complex use cases, authorizations need to be based on attributes.
|
281
|
-
|
280
|
+
For more complex use cases, authorizations need to be based on attributes. Note
|
281
|
+
that you then also need to set :attribute_check => true in controllers for filter_access_to.
|
282
|
+
E.g. if a branch admin should manage only employees of his branch (see
|
282
283
|
Authorization::Reader in the API docs for a full list of available operators):
|
283
284
|
|
284
285
|
authorization do
|
@@ -379,7 +380,7 @@ Then,
|
|
379
380
|
|
380
381
|
== Providing the Plugin's Requirements
|
381
382
|
The requirements are
|
382
|
-
* Rails >= 2.
|
383
|
+
* Rails >= 2.2, including 3 and Ruby >= 1.8.6, including 1.9
|
383
384
|
* An authentication mechanism
|
384
385
|
* A user object returned by Controller#current_user
|
385
386
|
* An array of role symbols returned by User#role_symbols
|
@@ -490,10 +491,10 @@ sbartsch at tzi.org
|
|
490
491
|
|
491
492
|
= Contributors
|
492
493
|
|
493
|
-
Thanks to John Joseph Bachir, Eike Carls, Kai Chen, Erik Dahlstrand,
|
494
|
-
|
495
|
-
Georg Ledermann, Geoff Longman, Olly Lylo, Mark Mansour,
|
496
|
-
Mike Vincent
|
494
|
+
Thanks to John Joseph Bachir, Eike Carls, Kai Chen, Erik Dahlstrand, Jeroen van Dijk,
|
495
|
+
Alexander Dobriakov, Sebastian Dyck, Ari Epstein, Jeremy Friesen, Tim Harper, hollownest,
|
496
|
+
Daniel Kristensen, Brian Langenfeld, Georg Ledermann, Geoff Longman, Olly Lylo, Mark Mansour,
|
497
|
+
Thomas Maurer, TJ Singleton, Mike Vincent
|
497
498
|
|
498
499
|
|
499
500
|
= Licence
|
data/Rakefile
CHANGED
@@ -33,11 +33,3 @@ desc "clone the garlic repo (for running ci tasks)"
|
|
33
33
|
task :get_garlic do
|
34
34
|
sh "git clone git://github.com/ianwhite/garlic.git garlic"
|
35
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
|
-
|
@@ -18,7 +18,7 @@ class AuthorizationRulesController < ApplicationController
|
|
18
18
|
def index
|
19
19
|
respond_to do |format|
|
20
20
|
format.html do
|
21
|
-
@auth_rules_script = File.read("#{
|
21
|
+
@auth_rules_script = File.read("#{::Rails.root}/config/authorization_rules.rb")
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
@@ -36,14 +36,14 @@ module AuthorizationRulesHelper
|
|
36
36
|
note = %Q{<span class="note" title="#{h text}">[i]</span>}
|
37
37
|
marked_up_by_line[line - 1] = note + marked_up_by_line[line - 1]
|
38
38
|
end
|
39
|
-
marked_up_by_line * "\n"
|
39
|
+
(marked_up_by_line * "\n").html_safe
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
def link_to_graph (title, options = {})
|
43
43
|
type = options[:type] || ''
|
44
44
|
link_to_function title, "$$('object')[0].data = '#{url_for :action => 'index', :format => 'svg', :type => type}'"
|
45
45
|
end
|
46
|
-
|
46
|
+
|
47
47
|
def navigation
|
48
48
|
link_to("Rules", authorization_rules_path) << ' | ' <<
|
49
49
|
link_to("Change Support", change_authorization_rules_path) << ' | ' <<
|
data/config/routes.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
-
ActionController::Routing::Routes
|
1
|
+
# Rails 3 depreciates ActionController::Routing::Routes
|
2
|
+
routes = (Rails.respond_to?(:application) ? Rails.application.routes : ActionController::Routing::Routes)
|
3
|
+
|
4
|
+
routes.draw do |map|
|
2
5
|
if Authorization::activate_authorization_rules_browser?
|
3
|
-
map.resources :authorization_rules, :only => [:index],
|
6
|
+
map.resources :authorization_rules, :only => [:index],
|
4
7
|
:collection => {:graph => :get, :change => :get, :suggest_change => :get}
|
5
8
|
map.resources :authorization_usages, :only => :index
|
6
9
|
end
|
@@ -9,6 +9,8 @@ if Rails::VERSION::STRING < min_rails_version
|
|
9
9
|
raise "declarative_authorization requires Rails #{min_rails_version}. You are using #{Rails::VERSION::STRING}."
|
10
10
|
end
|
11
11
|
|
12
|
+
require File.join(%w{declarative_authorization railsengine}) if defined?(::Rails::Engine)
|
13
|
+
|
12
14
|
ActionController::Base.send :include, Authorization::AuthorizationInController
|
13
15
|
ActionController::Base.helper Authorization::AuthorizationHelper
|
14
16
|
|
@@ -20,7 +20,7 @@ module Authorization
|
|
20
20
|
# The exception is raised to ensure that the entire rule is invalidated.
|
21
21
|
class NilAttributeValueError < AuthorizationError; end
|
22
22
|
|
23
|
-
AUTH_DSL_FILES = ["
|
23
|
+
AUTH_DSL_FILES = [(Rails.root || Pathname.new('')).join("config", "authorization_rules.rb").to_s] unless defined? AUTH_DSL_FILES
|
24
24
|
|
25
25
|
# Controller-independent method for retrieving the current user.
|
26
26
|
# Needed for model security where the current controller is not available.
|
@@ -40,7 +40,7 @@ module Authorization
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def self.activate_authorization_rules_browser? # :nodoc:
|
43
|
-
::
|
43
|
+
::Rails.env.development?
|
44
44
|
end
|
45
45
|
|
46
46
|
@@dot_path = "dot"
|
@@ -57,7 +57,7 @@ module Authorization
|
|
57
57
|
# a certain privilege is granted for the current user.
|
58
58
|
#
|
59
59
|
class Engine
|
60
|
-
attr_reader :roles, :role_titles, :role_descriptions, :privileges,
|
60
|
+
attr_reader :roles, :omnipotent_roles, :role_titles, :role_descriptions, :privileges,
|
61
61
|
:privilege_hierarchy, :auth_rules, :role_hierarchy, :rev_priv_hierarchy,
|
62
62
|
:rev_role_hierarchy
|
63
63
|
|
@@ -65,20 +65,14 @@ module Authorization
|
|
65
65
|
# authorization configuration of +AUTH_DSL_FILES+. If given, may be either
|
66
66
|
# a Reader object or a path to a configuration file.
|
67
67
|
def initialize (reader = nil)
|
68
|
-
|
69
|
-
|
70
|
-
reader = Reader::DSLReader.load(AUTH_DSL_FILES)
|
71
|
-
rescue SystemCallError
|
72
|
-
reader = Reader::DSLReader.new
|
73
|
-
end
|
74
|
-
elsif reader.is_a?(String)
|
75
|
-
reader = Reader::DSLReader.load(reader)
|
76
|
-
end
|
68
|
+
reader = Reader::DSLReader.factory(reader || AUTH_DSL_FILES)
|
69
|
+
|
77
70
|
@privileges = reader.privileges_reader.privileges
|
78
71
|
# {priv => [[priv, ctx],...]}
|
79
72
|
@privilege_hierarchy = reader.privileges_reader.privilege_hierarchy
|
80
73
|
@auth_rules = reader.auth_rules_reader.auth_rules
|
81
74
|
@roles = reader.auth_rules_reader.roles
|
75
|
+
@omnipotent_roles = reader.auth_rules_reader.omnipotent_roles
|
82
76
|
@role_hierarchy = reader.auth_rules_reader.role_hierarchy
|
83
77
|
|
84
78
|
@role_titles = reader.auth_rules_reader.role_titles
|
@@ -160,6 +154,8 @@ module Authorization
|
|
160
154
|
|
161
155
|
user, roles, privileges = user_roles_privleges_from_options(privilege, options)
|
162
156
|
|
157
|
+
return true if roles.is_a?(Array) and not (roles & @omnipotent_roles).empty?
|
158
|
+
|
163
159
|
# find a authorization rule that matches for at least one of the roles and
|
164
160
|
# at least one of the given privileges
|
165
161
|
attr_validator = AttributeValidator.new(self, user, options[:object], privilege, options[:context])
|
@@ -477,6 +473,14 @@ module Authorization
|
|
477
473
|
"subclass of Enumerable as value, got: #{attr_value.inspect} " +
|
478
474
|
"is_not_in #{evaluated.inspect}: #{e}"
|
479
475
|
end
|
476
|
+
when :lt
|
477
|
+
attr_value && attr_value < evaluated
|
478
|
+
when :lte
|
479
|
+
attr_value && attr_value <= evaluated
|
480
|
+
when :gt
|
481
|
+
attr_value && attr_value > evaluated
|
482
|
+
when :gte
|
483
|
+
attr_value && attr_value >= evaluated
|
480
484
|
else
|
481
485
|
raise AuthorizationError, "Unknown operator #{value[0]}"
|
482
486
|
end
|
@@ -521,8 +525,9 @@ module Authorization
|
|
521
525
|
begin
|
522
526
|
object.send(attr)
|
523
527
|
rescue ArgumentError, NoMethodError => e
|
524
|
-
raise AuthorizationUsageError, "Error
|
525
|
-
|
528
|
+
raise AuthorizationUsageError, "Error occurred while validating attribute ##{attr} on #{object.inspect}: #{e}.\n" +
|
529
|
+
"Please check your authorization rules and ensure the attribute is correctly spelled and \n" +
|
530
|
+
"corresponds to a method on the model you are authorizing for."
|
526
531
|
end
|
527
532
|
end
|
528
533
|
|
@@ -679,3 +684,4 @@ module Authorization
|
|
679
684
|
end
|
680
685
|
end
|
681
686
|
end
|
687
|
+
|
@@ -112,7 +112,7 @@ module Authorization
|
|
112
112
|
else
|
113
113
|
!DEFAULT_DENY
|
114
114
|
end
|
115
|
-
rescue
|
115
|
+
rescue NotAuthorized => e
|
116
116
|
auth_exception = e
|
117
117
|
end
|
118
118
|
|
@@ -274,10 +274,7 @@ module Authorization
|
|
274
274
|
context = options[:context]
|
275
275
|
actions = args.flatten
|
276
276
|
|
277
|
-
|
278
|
-
unless filter_chain.any? {|filter| filter.method == :filter_access_filter}
|
279
|
-
before_filter :filter_access_filter
|
280
|
-
end
|
277
|
+
before_filter :filter_access_filter
|
281
278
|
|
282
279
|
filter_access_permissions.each do |perm|
|
283
280
|
perm.remove_actions(actions)
|
@@ -69,11 +69,12 @@ module Authorization
|
|
69
69
|
}.merge(options)
|
70
70
|
engine = options[:engine] || Authorization::Engine.instance
|
71
71
|
|
72
|
-
|
72
|
+
obligation_scope = ObligationScope.new( options[:model], {} )
|
73
73
|
engine.obligations( privileges, :user => options[:user], :context => options[:context] ).each do |obligation|
|
74
|
-
|
74
|
+
obligation_scope.parse!( obligation )
|
75
75
|
end
|
76
|
-
|
76
|
+
|
77
|
+
obligation_scope.scope
|
77
78
|
end
|
78
79
|
|
79
80
|
# Named scope for limiting query results according to the authorization
|
@@ -132,15 +133,17 @@ module Authorization
|
|
132
133
|
:object => object, :context => options[:context])
|
133
134
|
end
|
134
135
|
end
|
135
|
-
|
136
|
-
# after_find is only called if after_find is implemented
|
137
|
-
after_find do |object|
|
138
|
-
Authorization::Engine.instance.permit!(:read, :object => object,
|
139
|
-
:context => options[:context])
|
140
|
-
end
|
141
136
|
|
142
137
|
if options[:include_read]
|
143
|
-
|
138
|
+
# after_find is only called if after_find is implemented
|
139
|
+
after_find do |object|
|
140
|
+
Authorization::Engine.instance.permit!(:read, :object => object,
|
141
|
+
:context => options[:context])
|
142
|
+
end
|
143
|
+
|
144
|
+
if Rails.version < "3"
|
145
|
+
def after_find; end
|
146
|
+
end
|
144
147
|
end
|
145
148
|
|
146
149
|
def self.using_access_control?
|
@@ -55,9 +55,9 @@ module Authorization
|
|
55
55
|
def self.usages_by_controller
|
56
56
|
# load each application controller
|
57
57
|
begin
|
58
|
-
Dir.foreach(File.join(
|
58
|
+
Dir.foreach(File.join(::Rails.root, %w{app controllers})) do |entry|
|
59
59
|
if entry =~ /^\w+_controller\.rb$/
|
60
|
-
require File.join(
|
60
|
+
require File.join(::Rails.root, %w{app controllers}, entry)
|
61
61
|
end
|
62
62
|
end
|
63
63
|
rescue Errno::ENOENT
|
@@ -78,7 +78,7 @@ module Authorization
|
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
-
actions = controller.public_instance_methods(false) - controller.hidden_actions
|
81
|
+
actions = controller.public_instance_methods(false) - controller.hidden_actions.to_a
|
82
82
|
memo[controller] = actions.inject({}) do |actions_memo, action|
|
83
83
|
action_sym = action.to_sym
|
84
84
|
actions_memo[action_sym] =
|
@@ -171,7 +171,7 @@ module Authorization
|
|
171
171
|
|
172
172
|
def request_with (user, method, xhr, action, params = {},
|
173
173
|
session = {}, flash = {})
|
174
|
-
session = session.merge({:user => user, :user_id => user.id})
|
174
|
+
session = session.merge({:user => user, :user_id => user && user.id})
|
175
175
|
with_user(user) do
|
176
176
|
if xhr
|
177
177
|
xhr method, action, params, session, flash
|
@@ -33,6 +33,7 @@ module Authorization
|
|
33
33
|
# [ :attr, :is, <user.id> ]
|
34
34
|
# ]+
|
35
35
|
#
|
36
|
+
# TODO update doc for Relations:
|
36
37
|
# After successfully parsing an obligation, all of the stored paths and conditions are converted
|
37
38
|
# into scope options (stored in +proxy_options+ as +:joins+ and +:conditions+). The resulting
|
38
39
|
# scope may then be used to find all scoped objects for which at least one of the parsed
|
@@ -41,8 +42,25 @@ module Authorization
|
|
41
42
|
# +@proxy_options[:joins] = { :bar => { :baz => :foo } }
|
42
43
|
# @proxy_options[:conditions] = [ 'foos_bazzes.attr = :foos_bazzes__id_0', { :foos_bazzes__id_0 => 1 } ]+
|
43
44
|
#
|
44
|
-
class ObligationScope < ActiveRecord::NamedScope::Scope
|
45
|
-
|
45
|
+
class ObligationScope < (Rails.version < "3" ? ActiveRecord::NamedScope::Scope : ActiveRecord::Relation)
|
46
|
+
def initialize (model, options)
|
47
|
+
@finder_options = {}
|
48
|
+
if Rails.version < "3"
|
49
|
+
super(model, options)
|
50
|
+
else
|
51
|
+
super(model, model.table_name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def scope
|
56
|
+
if Rails.version < "3"
|
57
|
+
self
|
58
|
+
else
|
59
|
+
# for Rails < 3: scope, after setting proxy_options
|
60
|
+
self.klass.scoped(@finder_options)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
46
64
|
# Consumes the given obligation, converting it into scope join and condition options.
|
47
65
|
def parse!( obligation )
|
48
66
|
@current_obligation = obligation
|
@@ -79,6 +97,18 @@ module Authorization
|
|
79
97
|
raise "invalid obligation path #{[past_steps, steps].inspect}"
|
80
98
|
end
|
81
99
|
end
|
100
|
+
|
101
|
+
def top_level_model
|
102
|
+
if Rails.version < "3"
|
103
|
+
@proxy_scope
|
104
|
+
else
|
105
|
+
self.klass
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def finder_options
|
110
|
+
Rails.version < "3" ? @proxy_options : @finder_options
|
111
|
+
end
|
82
112
|
|
83
113
|
# At the end of every association path, we expect to see a comparison of some kind; for
|
84
114
|
# example, +:attr => [ :is, :value ]+.
|
@@ -135,7 +165,7 @@ module Authorization
|
|
135
165
|
def map_reflection_for( path )
|
136
166
|
raise "reflection for #{path.inspect} already exists" unless reflections[path].nil?
|
137
167
|
|
138
|
-
reflection = path.empty? ?
|
168
|
+
reflection = path.empty? ? top_level_model : begin
|
139
169
|
parent = reflection_for( path[0..-2] )
|
140
170
|
if !parent.respond_to?(:proxy_reflection) and parent.respond_to?(:klass)
|
141
171
|
parent.klass.reflect_on_association( path.last )
|
@@ -151,7 +181,8 @@ module Authorization
|
|
151
181
|
map_table_alias_for( path ) # Claim a table alias for the path.
|
152
182
|
|
153
183
|
# Claim alias for join table
|
154
|
-
|
184
|
+
# TODO change how this is checked
|
185
|
+
if !reflection.respond_to?(:proxy_reflection) and !reflection.respond_to?(:proxy_scope) and reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
155
186
|
join_table_path = path[0..-2] + [reflection.options[:through]]
|
156
187
|
reflection_for(join_table_path, true)
|
157
188
|
end
|
@@ -209,7 +240,7 @@ module Authorization
|
|
209
240
|
conditions.each do |path, expressions|
|
210
241
|
model = model_for( path )
|
211
242
|
table_alias = table_alias_for(path)
|
212
|
-
parent_model = (path.length > 1 ? model_for(path[0..-2]) :
|
243
|
+
parent_model = (path.length > 1 ? model_for(path[0..-2]) : top_level_model)
|
213
244
|
expressions.each do |expression|
|
214
245
|
attribute, operator, value = expression
|
215
246
|
# prevent unnecessary joins:
|
@@ -227,7 +258,8 @@ module Authorization
|
|
227
258
|
end
|
228
259
|
bindvar = "#{attribute_table_alias}__#{attribute_name}_#{obligation_index}".to_sym
|
229
260
|
|
230
|
-
sql_attribute = "#{connection.quote_table_name(attribute_table_alias)}
|
261
|
+
sql_attribute = "#{parent_model.connection.quote_table_name(attribute_table_alias)}." +
|
262
|
+
"#{parent_model.connection.quote_table_name(attribute_name)}"
|
231
263
|
if value.nil? and [:is, :is_not].include?(operator)
|
232
264
|
obligation_conds << "#{sql_attribute} IS #{[:contains, :is].include?(operator) ? '' : 'NOT '}NULL"
|
233
265
|
else
|
@@ -236,6 +268,10 @@ module Authorization
|
|
236
268
|
when :does_not_contain, :is_not then "<> :#{bindvar}"
|
237
269
|
when :is_in, :intersects_with then "IN (:#{bindvar})"
|
238
270
|
when :is_not_in then "NOT IN (:#{bindvar})"
|
271
|
+
when :lt then "< :#{bindvar}"
|
272
|
+
when :lte then "<= :#{bindvar}"
|
273
|
+
when :gt then "> :#{bindvar}"
|
274
|
+
when :gte then ">= :#{bindvar}"
|
239
275
|
else raise AuthorizationUsageError, "Unknown operator: #{operator}"
|
240
276
|
end
|
241
277
|
obligation_conds << "#{sql_attribute} #{attribute_operator}"
|
@@ -247,7 +283,8 @@ module Authorization
|
|
247
283
|
conds << "(#{obligation_conds.join(' AND ')})"
|
248
284
|
end
|
249
285
|
(delete_paths - used_paths).each {|path| reflections.delete(path)}
|
250
|
-
|
286
|
+
|
287
|
+
finder_options[:conditions] = [ conds.join( " OR " ), binds ]
|
251
288
|
end
|
252
289
|
|
253
290
|
def attribute_value (value)
|
@@ -259,7 +296,7 @@ module Authorization
|
|
259
296
|
# Parses all of the defined obligation joins and defines the scope's :joins or :includes option.
|
260
297
|
# TODO: Support non-linear association paths. Right now, we just break down the longest path parsed.
|
261
298
|
def rebuild_join_options!
|
262
|
-
joins = (
|
299
|
+
joins = (finder_options[:joins] || []) + (finder_options[:includes] || [])
|
263
300
|
|
264
301
|
reflections.keys.each do |path|
|
265
302
|
next if path.empty? or @join_table_joins.include?(path)
|
@@ -283,11 +320,11 @@ module Authorization
|
|
283
320
|
when 0 then
|
284
321
|
# No obligation conditions means we don't have to mess with joins or includes at all.
|
285
322
|
when 1 then
|
286
|
-
|
287
|
-
|
323
|
+
finder_options[:joins] = joins
|
324
|
+
finder_options.delete( :include )
|
288
325
|
else
|
289
|
-
|
290
|
-
|
326
|
+
finder_options.delete( :joins )
|
327
|
+
finder_options[:include] = joins
|
291
328
|
end
|
292
329
|
end
|
293
330
|
|
@@ -313,4 +350,5 @@ module Authorization
|
|
313
350
|
end
|
314
351
|
end
|
315
352
|
end
|
316
|
-
end
|
353
|
+
end
|
354
|
+
|