protector 0.0.2 → 0.0.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 098c77da509ed0a347c0a22df9dd6c6a8d34289a
4
- data.tar.gz: b9ae1d35809445399fda713c3f0276ac91114659
3
+ metadata.gz: 5196d6460c7c9550f4be2cef9135916d900dcdc0
4
+ data.tar.gz: 08886a665e9f57db43dd49d064c23b569e01106b
5
5
  SHA512:
6
- metadata.gz: d0cc601d414b036f8eb6db9381c9f30a93bc1bdb5bd92a946165f32ea97f9229be9fa76210a7b98ec878da9b0476f6dacffb6bf7e24200c496bf8e6106a6b4bf
7
- data.tar.gz: 9994bc1d0e0a3a93f81f4ee07fabf7cf1faf7c02c958952ec8cd80a6452349c20cd8fc7a79bf51665da2cbccdef179ce85963cc4ab0528b039b6b21a735b85bc
6
+ metadata.gz: 87cd3b93a9b8b3ca0b86d034234385111cd8aa9c793e6440908fc6362a6160e21a7d6010b6c89018a4245a44e1cb043c0086a023cf88ce50e501be7a21cece95
7
+ data.tar.gz: 2fffcf64bc2669bfb3292e432e52408d1a9207263085a6cac74dde4200882dc645523c93320449ffe9320f76aa94389caf3ce2c7c993ddfc56333e2426d1d5c8
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ before_install:
2
+ - gem install bundler
3
+ rvm:
4
+ - 1.9.3
5
+ - jruby-19mode
6
+ - 2.0.0
data/Appraisals ADDED
@@ -0,0 +1,9 @@
1
+ appraise "AR_3.2" do
2
+ gem "activerecord", "3.2", require: "active_record"
3
+ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby, github: "jruby/activerecord-jdbc-adapter"
4
+ end
5
+
6
+ appraise "AR_4" do
7
+ gem "activerecord", "4.0.0.rc1", require: "active_record"
8
+ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby, github: "jruby/activerecord-jdbc-adapter"
9
+ end
data/Gemfile CHANGED
@@ -7,4 +7,7 @@ gem 'rspec'
7
7
  gem 'guard'
8
8
  gem 'guard-rspec'
9
9
 
10
- gem 'activerecord', '>= 3.0'
10
+ gem 'appraisal'
11
+
12
+ gem 'sqlite3', platform: :ruby
13
+ gem 'jdbc-sqlite3', platform: :jruby
data/README.md CHANGED
@@ -1,6 +1,125 @@
1
1
  # Protector
2
2
 
3
- TODO: Write a gem description
3
+ [![Build Status](https://travis-ci.org/inossidabile/protector.png?branch=master)](https://travis-ci.org/inossidabile/protector)
4
+ [![Code Climate](https://codeclimate.com/github/inossidabile/protector.png)](https://codeclimate.com/github/inossidabile/protector)
5
+
6
+ Protector is a Ruby ORM extension for managing security restrictions on a field level. The gem favors white-listing over black-listing (everything is disallowed by default), convention over configuration and is duck-type compatible with most of existing code.
7
+
8
+ Currently Protector supports the following ORM adapters:
9
+
10
+ * [ActiveRecord](http://guides.rubyonrails.org/active_record_querying.html) (>= 3.2)
11
+
12
+ We are working hard to extend the list with:
13
+
14
+ * [Sequel](http://sequel.rubyforge.org/)
15
+ * [DataMapper](http://datamapper.org/)
16
+ * [Mongoid](http://mongoid.org/en/mongoid/index.html)
17
+
18
+ ## Basics
19
+
20
+ DSL of Protector is a Ruby block (or several) describing ACL separated into contexts (authorized user is a very typical example of a context). Each time the context of model changes, DSL blocks reevaluate internally to get an actual ACL that is then utilized internally to cut restricted actions.
21
+
22
+ Protector follows nondestructive blocking strategy. It returns `nil` when the forbidden field is requested and only checks creation (modification) capability during persisting. Even more: the latter is implemented as a model validation so it will seamlessly integrate into your typical workflow.
23
+
24
+ This example is based on ActiveRecord but the code is mostly identical for any supported adapter.
25
+
26
+ ```ruby
27
+ class Article < ActiveRecord::Base # Fields: title, text, user_id, hidden
28
+ protect do |user| # `user` is a context of security
29
+
30
+ unless user.admin?
31
+ scope { where(hidden: false) } # Non-admins can only read insecure data
32
+
33
+ can :view # Allow to read any field
34
+ if user.nil? # User is unknown and therefore not authenticated
35
+ cannot :view, :text # Guests can't read the text
36
+ end
37
+
38
+ can :create, %w(title text) # Non-admins can't set `hidden` flag
39
+ can :create, user_id: lamda{|x| # ... and should correctly fill
40
+ x == user.id # ... the `user_id` association
41
+ }
42
+
43
+ # In this setup non-admins can not destroy or update existing records.
44
+ else
45
+ scope { all } # Admins can retrieve anything
46
+
47
+ can :view # ... and view anything
48
+ can :create # ... and create anything
49
+ can :update # ... and update anything
50
+ can :destroy # ... and they can delete
51
+ end
52
+ end
53
+ end
54
+ ```
55
+
56
+ Now that we have ACL described we can enable it as easy as:
57
+
58
+ ```ruby
59
+ article.restrict!(current_user) # Assuming article is an instance of Article
60
+ ```
61
+
62
+ To make model unsafe again call:
63
+
64
+ ```ruby
65
+ article.unrestrict!
66
+ ```
67
+
68
+ **Both methods are chainable!**
69
+
70
+ ## Scopes
71
+
72
+ Besides the `can` and `cannot` directives Protector also handles relations visibility. In the previous sample the following block is responsible to make hidden articles actually hide:
73
+
74
+ ```ruby
75
+ scope { where(hidden: false) } # Non-admins can only read unsecure data
76
+ ````
77
+
78
+ Make sure to write the block content of the `scope` directive in the notation of your ORM library.
79
+
80
+ To finally utilize this function use the same `restrict!` method on a level of Class or Relation. Like this:
81
+
82
+ ```ruby
83
+ Article.restrict!(current_user).where(...)
84
+ # OR
85
+ Article.where(...).restrict!(current_user)
86
+ ```
87
+
88
+ Note that you don't need to explicitly restrict models you get from a restricted scope – they born restricted.
89
+
90
+ ## Self-aware conditions
91
+
92
+ Sometimes an access decision depends on the object we restrict. `protect` block accepts second argument to fulfill these cases. Keep in mind however that it's not always accessible: we don't have any instance for the restriction of relation and therefore `nil` is passed.
93
+
94
+ The following example extends Article to allow users edit their own posts:
95
+
96
+ ```ruby
97
+ class Article < ActiveRecord::Base # Fields: title, text, user_id, hidden
98
+ protect do |user, article|
99
+ if user
100
+ if article.try(:user_id) == user.id # Checks belonging keeping possible nil in mind
101
+ can :update, %w(title text) # Allow authors to modify posts
102
+ end
103
+ end
104
+ end
105
+ end
106
+ ```
107
+
108
+ ## Associations
109
+
110
+ Protector is aware of associations. All the associations retrieved from restricted instance will automatically be restricted to the same context. Therefore you don't have to do anything special – it will respect proper scopes out of the box.
111
+
112
+ The access to `belongs_to` kind of association depends on corresponding foreign key readability.
113
+
114
+ ## Ideology
115
+
116
+ Protector is a successor to [Heimdallr](https://github.com/inossidabile/heimdallr). The latter being a proof-of-concept appeared to be way too paranoid and incompatible with the rest of the world. Protector re-implements same idea keeping the Ruby way:
117
+
118
+ * it works inside of the model instead of wrapping it into a proxy: that's why it's compatible with every other extension you use
119
+ * it secures persistence and not object properties: you can modify any properties you want but it's not going to let you save what you can not save
120
+ * it respects the differentiation between business-logic layer and SQL layer: protection is validation so any method that skips validation will also avoid the security check
121
+
122
+ **The last thing is really important to understand. No matter if you can read a field or not, methods like `.pluck` are still capable of reading any of your fields and if you tell your model to skip validation it will also skip an ACL check.**
4
123
 
5
124
  ## Installation
6
125
 
@@ -16,9 +135,13 @@ Or install it yourself as:
16
135
 
17
136
  $ gem install protector
18
137
 
19
- ## Usage
138
+ As long as you load Protector after an ORM library it is supposed to activate itself automatically. Otherwise you can enable required adapter manually:
139
+
140
+ ```ruby
141
+ Protector::Adapters::ActiveRecord.activate!
142
+ ```
20
143
 
21
- TODO: Write usage instructions here
144
+ Where "ActiveRecord" is the adapter you are about to use. It can be "Sequel", "DataMapper", "Mongoid".
22
145
 
23
146
  ## Contributing
24
147
 
data/Rakefile CHANGED
@@ -1,8 +1,13 @@
1
1
  require 'bundler/setup'
2
2
  require 'bundler/gem_tasks'
3
3
  require 'rspec/core/rake_task'
4
+ require 'appraisal'
4
5
 
5
6
  RSpec::Core::RakeTask.new(:spec)
6
7
 
7
- desc "Default: run the unit tests."
8
- task :default => :spec
8
+ task :default => :all
9
+
10
+ desc 'Test the plugin under all supported Rails versions.'
11
+ task :all => ["appraisal:install"] do |t|
12
+ exec('rake appraisal spec')
13
+ end
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake"
6
+ gem "pry"
7
+ gem "rspec"
8
+ gem "guard"
9
+ gem "guard-rspec"
10
+ gem "appraisal"
11
+ gem "sqlite3", :platform=>:ruby
12
+ gem "jdbc-sqlite3", :platform=>:jruby
13
+ gem "activerecord", "3.2", :require=>"active_record"
14
+ gem "activerecord-jdbcsqlite3-adapter", :platform=>:jruby, :github=>"jruby/activerecord-jdbc-adapter"
15
+
16
+ gemspec :path=>"../"
@@ -0,0 +1,104 @@
1
+ GIT
2
+ remote: git://github.com/jruby/activerecord-jdbc-adapter.git
3
+ revision: b1cb2cb59496a7c3ae22799f88c8c4789e6a8cce
4
+ specs:
5
+ activerecord-jdbcsqlite3-adapter (1.3.0.DEV)
6
+ activerecord-jdbc-adapter (~> 1.3.0.DEV)
7
+ jdbc-sqlite3 (~> 3.7.2)
8
+
9
+ PATH
10
+ remote: /Users/inossidabile/Repos/protector
11
+ specs:
12
+ protector (0.0.3)
13
+ activesupport
14
+ i18n
15
+
16
+ GEM
17
+ remote: https://rubygems.org/
18
+ specs:
19
+ activemodel (3.2.0)
20
+ activesupport (= 3.2.0)
21
+ builder (~> 3.0.0)
22
+ activerecord (3.2.0)
23
+ activemodel (= 3.2.0)
24
+ activesupport (= 3.2.0)
25
+ arel (~> 3.0.0)
26
+ tzinfo (~> 0.3.29)
27
+ activerecord-jdbc-adapter (1.3.0.beta1)
28
+ activesupport (3.2.0)
29
+ i18n (~> 0.6)
30
+ multi_json (~> 1.0)
31
+ appraisal (0.5.2)
32
+ bundler
33
+ rake
34
+ arel (3.0.2)
35
+ builder (3.0.4)
36
+ coderay (1.0.9)
37
+ diff-lcs (1.2.4)
38
+ ffi (1.8.1)
39
+ ffi (1.8.1-java)
40
+ formatador (0.2.4)
41
+ guard (1.8.0)
42
+ formatador (>= 0.2.4)
43
+ listen (>= 1.0.0)
44
+ lumberjack (>= 1.0.2)
45
+ pry (>= 0.9.10)
46
+ thor (>= 0.14.6)
47
+ guard-rspec (3.0.0)
48
+ guard (>= 1.8)
49
+ rspec (~> 2.13)
50
+ i18n (0.6.4)
51
+ jdbc-sqlite3 (3.7.2)
52
+ listen (1.1.3)
53
+ rb-fsevent (>= 0.9.3)
54
+ rb-inotify (>= 0.9)
55
+ rb-kqueue (>= 0.2)
56
+ lumberjack (1.0.3)
57
+ method_source (0.8.1)
58
+ multi_json (1.7.3)
59
+ pry (0.9.12.2)
60
+ coderay (~> 1.0.5)
61
+ method_source (~> 0.8)
62
+ slop (~> 3.4)
63
+ pry (0.9.12.2-java)
64
+ coderay (~> 1.0.5)
65
+ method_source (~> 0.8)
66
+ slop (~> 3.4)
67
+ spoon (~> 0.0)
68
+ rake (10.0.4)
69
+ rb-fsevent (0.9.3)
70
+ rb-inotify (0.9.0)
71
+ ffi (>= 0.5.0)
72
+ rb-kqueue (0.2.0)
73
+ ffi (>= 0.5.0)
74
+ rspec (2.13.0)
75
+ rspec-core (~> 2.13.0)
76
+ rspec-expectations (~> 2.13.0)
77
+ rspec-mocks (~> 2.13.0)
78
+ rspec-core (2.13.1)
79
+ rspec-expectations (2.13.0)
80
+ diff-lcs (>= 1.1.3, < 2.0)
81
+ rspec-mocks (2.13.1)
82
+ slop (3.4.5)
83
+ spoon (0.0.4)
84
+ ffi
85
+ sqlite3 (1.3.7)
86
+ thor (0.18.1)
87
+ tzinfo (0.3.37)
88
+
89
+ PLATFORMS
90
+ java
91
+ ruby
92
+
93
+ DEPENDENCIES
94
+ activerecord (= 3.2)
95
+ activerecord-jdbcsqlite3-adapter!
96
+ appraisal
97
+ guard
98
+ guard-rspec
99
+ jdbc-sqlite3
100
+ protector!
101
+ pry
102
+ rake
103
+ rspec
104
+ sqlite3
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake"
6
+ gem "pry"
7
+ gem "rspec"
8
+ gem "guard"
9
+ gem "guard-rspec"
10
+ gem "appraisal"
11
+ gem "sqlite3", :platform=>:ruby
12
+ gem "jdbc-sqlite3", :platform=>:jruby
13
+ gem "activerecord", "4.0.0.rc1", :require=>"active_record"
14
+ gem "activerecord-jdbcsqlite3-adapter", :platform=>:jruby, :github=>"jruby/activerecord-jdbc-adapter"
15
+
16
+ gemspec :path=>"../"
@@ -0,0 +1,113 @@
1
+ GIT
2
+ remote: git://github.com/jruby/activerecord-jdbc-adapter.git
3
+ revision: b1cb2cb59496a7c3ae22799f88c8c4789e6a8cce
4
+ specs:
5
+ activerecord-jdbcsqlite3-adapter (1.3.0.DEV)
6
+ activerecord-jdbc-adapter (~> 1.3.0.DEV)
7
+ jdbc-sqlite3 (~> 3.7.2)
8
+
9
+ PATH
10
+ remote: /Users/inossidabile/Repos/protector
11
+ specs:
12
+ protector (0.0.3)
13
+ activesupport
14
+ i18n
15
+
16
+ GEM
17
+ remote: https://rubygems.org/
18
+ specs:
19
+ activemodel (4.0.0.rc1)
20
+ activesupport (= 4.0.0.rc1)
21
+ builder (~> 3.1.0)
22
+ activerecord (4.0.0.rc1)
23
+ activemodel (= 4.0.0.rc1)
24
+ activerecord-deprecated_finders (~> 1.0.2)
25
+ activesupport (= 4.0.0.rc1)
26
+ arel (~> 4.0.0)
27
+ activerecord-deprecated_finders (1.0.2)
28
+ activerecord-jdbc-adapter (1.3.0.beta1)
29
+ activesupport (4.0.0.rc1)
30
+ i18n (~> 0.6, >= 0.6.4)
31
+ minitest (~> 4.2)
32
+ multi_json (~> 1.3)
33
+ thread_safe (~> 0.1)
34
+ tzinfo (~> 0.3.37)
35
+ appraisal (0.5.2)
36
+ bundler
37
+ rake
38
+ arel (4.0.0)
39
+ atomic (1.1.9)
40
+ atomic (1.1.9-java)
41
+ builder (3.1.4)
42
+ coderay (1.0.9)
43
+ diff-lcs (1.2.4)
44
+ ffi (1.8.1)
45
+ ffi (1.8.1-java)
46
+ formatador (0.2.4)
47
+ guard (1.8.0)
48
+ formatador (>= 0.2.4)
49
+ listen (>= 1.0.0)
50
+ lumberjack (>= 1.0.2)
51
+ pry (>= 0.9.10)
52
+ thor (>= 0.14.6)
53
+ guard-rspec (3.0.0)
54
+ guard (>= 1.8)
55
+ rspec (~> 2.13)
56
+ i18n (0.6.4)
57
+ jdbc-sqlite3 (3.7.2)
58
+ listen (1.1.3)
59
+ rb-fsevent (>= 0.9.3)
60
+ rb-inotify (>= 0.9)
61
+ rb-kqueue (>= 0.2)
62
+ lumberjack (1.0.3)
63
+ method_source (0.8.1)
64
+ minitest (4.7.4)
65
+ multi_json (1.7.3)
66
+ pry (0.9.12.2)
67
+ coderay (~> 1.0.5)
68
+ method_source (~> 0.8)
69
+ slop (~> 3.4)
70
+ pry (0.9.12.2-java)
71
+ coderay (~> 1.0.5)
72
+ method_source (~> 0.8)
73
+ slop (~> 3.4)
74
+ spoon (~> 0.0)
75
+ rake (10.0.4)
76
+ rb-fsevent (0.9.3)
77
+ rb-inotify (0.9.0)
78
+ ffi (>= 0.5.0)
79
+ rb-kqueue (0.2.0)
80
+ ffi (>= 0.5.0)
81
+ rspec (2.13.0)
82
+ rspec-core (~> 2.13.0)
83
+ rspec-expectations (~> 2.13.0)
84
+ rspec-mocks (~> 2.13.0)
85
+ rspec-core (2.13.1)
86
+ rspec-expectations (2.13.0)
87
+ diff-lcs (>= 1.1.3, < 2.0)
88
+ rspec-mocks (2.13.1)
89
+ slop (3.4.5)
90
+ spoon (0.0.4)
91
+ ffi
92
+ sqlite3 (1.3.7)
93
+ thor (0.18.1)
94
+ thread_safe (0.1.0)
95
+ atomic
96
+ tzinfo (0.3.37)
97
+
98
+ PLATFORMS
99
+ java
100
+ ruby
101
+
102
+ DEPENDENCIES
103
+ activerecord (= 4.0.0.rc1)
104
+ activerecord-jdbcsqlite3-adapter!
105
+ appraisal
106
+ guard
107
+ guard-rspec
108
+ jdbc-sqlite3
109
+ protector!
110
+ pry
111
+ rake
112
+ rspec
113
+ sqlite3
@@ -0,0 +1,29 @@
1
+ module Protector
2
+ module Adapters
3
+ module ActiveRecord
4
+ module Association
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ alias_method_chain :scope, :protector
9
+ end
10
+
11
+ def scope_with_protector(*args)
12
+ scope_without_protector(*args).restrict!(owner.protector_subject)
13
+ end
14
+ end
15
+
16
+ module PreloaderAssociation
17
+ extend ActiveSupport::Concern
18
+
19
+ included do
20
+ alias_method_chain :scope, :protector
21
+ end
22
+
23
+ def scope_with_protector
24
+ scope_without_protector
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,100 @@
1
+ module Protector
2
+ module Adapters
3
+ module ActiveRecord
4
+ module Base
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include Protector::DSL::Base
9
+ include Protector::DSL::Entry
10
+
11
+ validate(on: :create) do
12
+ return unless @protector_subject
13
+ errors[:base] << I18n.t('protector.invalid') unless creatable?
14
+ end
15
+
16
+ validate(on: :update) do
17
+ return unless @protector_subject
18
+ errors[:base] << I18n.t('protector.invalid') unless updatable?
19
+ end
20
+
21
+ before_destroy do
22
+ return true unless @protector_subject
23
+ destroyable?
24
+ end
25
+
26
+ if Gem::Version.new(::ActiveRecord::VERSION::STRING) < Gem::Version.new('4.0.0.rc1')
27
+ def self.restrict!(subject)
28
+ scoped.restrict!(subject)
29
+ end
30
+ else
31
+ def self.restrict!(subject)
32
+ all.restrict!(subject)
33
+ end
34
+ end
35
+
36
+ def [](name)
37
+ if !@protector_subject || name == self.class.primary_key || protector_meta.readable?(name)
38
+ read_attribute(name)
39
+ else
40
+ nil
41
+ end
42
+ end
43
+ end
44
+
45
+ module ClassMethods
46
+ def define_method_attribute(name)
47
+ super
48
+
49
+ unless (primary_key == name || (primary_key.is_a?(Array) && primary_key.include?(name)))
50
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
51
+ alias_method #{"#{name}_unprotected".inspect}, #{name.inspect}
52
+
53
+ def #{name}
54
+ if !@protector_subject || protector_meta.readable?(#{name.inspect})
55
+ #{name}_unprotected
56
+ else
57
+ nil
58
+ end
59
+ end
60
+ STR
61
+ end
62
+ end
63
+ end
64
+
65
+ def protector_meta
66
+ unless @protector_subject
67
+ raise "Unprotected entity detected: use `restrict` method to protect it."
68
+ end
69
+
70
+ self.class.protector_meta.evaluate(
71
+ self.class,
72
+ self.class.column_names,
73
+ @protector_subject,
74
+ self
75
+ )
76
+ end
77
+
78
+ def visible?
79
+ protector_meta.relation.where(
80
+ self.class.primary_key => id
81
+ ).any?
82
+ end
83
+
84
+ def creatable?
85
+ fields = HashWithIndifferentAccess[changed.map{|x| [x, read_attribute(x)]}]
86
+ protector_meta.creatable?(fields)
87
+ end
88
+
89
+ def updatable?
90
+ fields = HashWithIndifferentAccess[changed.map{|x| [x, read_attribute(x)]}]
91
+ protector_meta.updatable?(fields)
92
+ end
93
+
94
+ def destroyable?
95
+ protector_meta.destroyable?
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,49 @@
1
+ module Protector
2
+ module Adapters
3
+ module ActiveRecord
4
+ module Relation
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include Protector::DSL::Base
9
+
10
+ alias_method_chain :exec_queries, :protector
11
+ end
12
+
13
+ def protector_meta
14
+ @klass.protector_meta.evaluate(@klass, @klass.column_names, @protector_subject)
15
+ end
16
+
17
+ def unscoped
18
+ super.restrict!(@protector_subject)
19
+ end
20
+
21
+ def count(*args)
22
+ super || 0
23
+ end
24
+
25
+ def sum(*args)
26
+ super || 0
27
+ end
28
+
29
+ def calculate(*args)
30
+ return super unless @protector_subject
31
+ merge(protector_meta.relation).unrestrict!.calculate *args
32
+ end
33
+
34
+ def exists?(*args)
35
+ return super unless @protector_subject
36
+ merge(protector_meta.relation).unrestrict!.exists? *args
37
+ end
38
+
39
+ def exec_queries_with_protector(*args)
40
+ return exec_queries_without_protector unless @protector_subject
41
+
42
+ subject = @protector_subject
43
+ relation = merge(protector_meta.relation).unrestrict!
44
+ @records = relation.send(:exec_queries).each{|r| r.restrict!(subject)}
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end