rack-api-key 0.0.2 → 0.0.3

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/README.md CHANGED
@@ -31,8 +31,10 @@ Or install it yourself as:
31
31
 
32
32
  ```ruby
33
33
  use RackApiKey, :api_key_proc => Proc.new { |val| ApiKey.find(val) },
34
- :rack_api_key => "account.api.key",
35
- :header_key => "HTTP_X_CUSTOM_API_HEADER"
34
+ :rack_api_key => "account.api.key",
35
+ :header_key => "HTTP_X_CUSTOM_API_HEADER",
36
+ :url_restriction => [/api/],
37
+ :url_exclusion => [/api\/status/]
36
38
  ```
37
39
 
38
40
  ### :header_key
@@ -61,6 +63,11 @@ authentication and some that might not. Or a combination of API endpoints and
61
63
  publicly facing webpages. Perhaps you've scoped all of your API endpoints to
62
64
  "/api", and the rest of the URL mappings or routes are supposed to be wide open.
63
65
 
66
+ ### :url_exclusion
67
+ This is an option to allow specific URLs to bypass rack-api-middleware authentication.
68
+ This works well when you require a single or few endpoints to not require
69
+ authentication. Perhaps you've scoped all of your API endpoints to "/api" but wish
70
+ to leave "/api/status" publically facing.
64
71
 
65
72
  ### unauthorized_api_key method
66
73
  This is a method that can be overridden with however you'd like to respond
@@ -2,94 +2,107 @@ require "rack-api-key/version"
2
2
 
3
3
  ##
4
4
  # RackApiKey is a middleware that relies on the client submitting requests
5
- # with a header named "X-API-KEY" storing their private API key as the value.
6
- # The middleware will then intercept the request, read the value from the named
7
- # header and call the given "proc" used for API key lookup. The API key lookup
8
- # should only return a value if there is an exact match for the value stored in
9
- # the named API key header.
10
- # If such a API key exists, the middleware will pass the request onward and also
5
+ # with a header named "X-API-KEY" storing their private API key as the value.
6
+ # The middleware will then intercept the request, read the value from the named
7
+ # header and call the given "proc" used for API key lookup. The API key lookup
8
+ # should only return a value if there is an exact match for the value stored in
9
+ # the named API key header.
10
+ #
11
+ # If such a API key exists, the middleware will pass the request onward and also
11
12
  # set a new value in the request representing the authenticated API key. Otherwise
12
13
  # the middleware will return a HTTP status of 401, and a plain text message
13
14
  # notifying the calling requestor that they are not authorized.
14
15
  class RackApiKey
15
16
 
16
- ##
17
- # ==== Options
18
- # * +:api_key_proc+ - **REQUIRED** A proc that is intended to lookup the API key in your datastore.
19
- # The given proc should take an argument, namely the value of the API key header.
20
- # There is no default value for this option and will raise a
21
- # NotImplementedError if left unspecified.
22
- # * +:rack_api_key+ - A way to override the key's name set in the request
23
- # on successful authentication. The default value is
24
- # "rack_api_key".
25
- # * +:header_key+ - A way to override the header's name used to store the API key.
26
- # The value given here should reflect how Rack interprets the
27
- # header. For example if the client passes "X-API-KEY" Rack
28
- # transforms interprets it as "HTTP_X_API_KEY". The default
29
- # value is "HTTP_X_API_KEY".
30
- # * +:url_restriction+ - A way to restrict specific URLs that should pass through
31
- # the rack-api-key middleware. In order to use pass an Array of Regex patterns.
32
- # If left unspecified all requests will pass through the rack-api-key
33
- # middleware.
34
- #
35
- # ==== Example
36
- # use RackApiKey,
37
- # :api_key_proc => Proc.new { |key| ApiKey.where(:key => key).first },
38
- # :rack_api_key => "authenticated.api.key",
39
- # :header_key => "HTTP_X_SECRET_API_KEY"
40
- def initialize(app, options = {})
41
- @app = app
42
- default_options = {
43
- :header_key => "HTTP_X_API_KEY",
44
- :rack_api_key => "rack_api_key",
45
- :api_key_proc => Proc.new { raise NotImplementedError.new("Caller must implement a way to lookup an API key.") },
46
- :url_restriction => []
47
- }
48
- @options = default_options.merge(options)
49
- end
50
-
51
- def call(env)
52
-
53
- if @options[:url_restriction].nil? || @options[:url_restriction].empty?
17
+ ##
18
+ # ==== Options
19
+ # * +:api_key_proc+ - **REQUIRED** A proc that is intended to lookup the API key in your datastore.
20
+ # The given proc should take an argument, namely the value of the API key header.
21
+ # There is no default value for this option and will raise a
22
+ # NotImplementedError if left unspecified.
23
+ #
24
+ # * +:rack_api_key+ - A way to override the key's name set in the request
25
+ # on successful authentication. The default value is
26
+ # "rack_api_key".
27
+ #
28
+ # * +:header_key+ - A way to override the header's name used to store the API key.
29
+ # The value given here should reflect how Rack interprets the
30
+ # header. For example if the client passes "X-API-KEY" Rack
31
+ # transforms interprets it as "HTTP_X_API_KEY". The default
32
+ # value is "HTTP_X_API_KEY".
33
+ #
34
+ # * +:url_restriction+ - A way to restrict specific URLs that should pass through
35
+ # the rack-api-key middleware. In order to use pass an Array of Regex patterns.
36
+ # If left unspecified all requests will pass through the rack-api-key
37
+ # middleware.
38
+ #
39
+ # * +:url_exclusion+ - A way to exclude specific URLs that should not pass through the
40
+ # the rack-api-middleware. In order to use, pass an Array of Regex patterns.
41
+ #
42
+ # ==== Example
43
+ # use RackApiKey,
44
+ # :api_key_proc => Proc.new { |key| ApiKey.where(:key => key).first },
45
+ # :rack_api_key => "authenticated.api.key",
46
+ # :header_key => "HTTP_X_SECRET_API_KEY",
47
+ # :url_restriction => [/api/],
48
+ # :url_exclusion => [/api\/status/]
49
+
50
+ def initialize(app, options = {})
51
+ @app = app
52
+ default_options = {
53
+ :header_key => 'HTTP_X_API_KEY',
54
+ :rack_api_key => 'rack_api_key',
55
+ :api_key_proc => Proc.new { raise NotImplementedError.new('Caller must implement a way to lookup an API key.') },
56
+ :url_restriction => [],
57
+ :url_exclusion => []
58
+ }
59
+ @options = default_options.merge(options)
60
+ end
61
+
62
+ def call(env)
63
+
64
+ if constraint?(:url_exclusion) && url_matches(:url_exclusion, env)
65
+
66
+ @app.call(env)
67
+
68
+ elsif constraint?(:url_restriction)
69
+
70
+ url_matches(:url_restriction, env) ? process_request(env) : @app.call(env)
71
+
72
+ else
73
+
54
74
  process_request(env)
55
- else
56
- request = Rack::Request.new(env)
57
- url_matches = @options[:url_restriction].select { |url_regex| request.fullpath.match(url_regex) }
58
- unless url_matches.empty?
59
- process_request(env)
60
- else
61
- @app.call(env)
62
- end
75
+
63
76
  end
64
77
 
65
78
  end
66
79
 
67
80
  ##
68
81
  # Sets the API key lookup value in the request. Intentionally left here
69
- # if anyone wants to change or override what does or does not get set
82
+ # if anyone wants to change or override what does or does not get set
70
83
  # in the request.
71
84
  def rack_api_key_request_setter(env, api_key_lookup_val)
72
- env[@options[:rack_api_key]] = api_key_lookup_val
85
+ env[@options[:rack_api_key]] = api_key_lookup_val
73
86
  end
74
87
 
75
88
  ##
76
89
  # Returns a 401 HTTP status code when an API key is not found or is not
77
90
  # authorized. Intentionally left here if anyone wants to override this
78
- # functionality, specifically change the format of the message or the
91
+ # functionality, specifically change the format of the message or the
79
92
  # media type.
80
- def unauthorized_api_key
81
- body_text = "The API key provided is not authorized."
93
+ def unauthorized_api_key
94
+ body_text = 'The API key provided is not authorized.'
82
95
  [401, {'Content-Type' => 'text/plain; charset=utf-8',
83
- 'Content-Length' => body_text.size.to_s}, [body_text]]
96
+ 'Content-Length' => body_text.size.to_s}, [body_text]]
84
97
  end
85
98
 
86
99
  ##
87
100
  # Checks if the API key header value is present and the API key
88
101
  # that was returned from the API key proc is present.
89
102
  # Intentionally left here is anyone wants to override this functionality.
90
- def valid_api_key?(api_header_val, api_key_lookup_val)
91
- !api_header_val.nil? && api_header_val != "" &&
92
- !api_key_lookup_val.nil? && api_key_lookup_val != ""
103
+ def valid_api_key?(api_header_val, api_key_lookup_val)
104
+ !api_header_val.nil? && api_header_val != '' &&
105
+ !api_key_lookup_val.nil? && api_key_lookup_val != ''
93
106
  end
94
107
 
95
108
  private
@@ -106,4 +119,13 @@ class RackApiKey
106
119
  end
107
120
  end
108
121
 
122
+ def constraint?(key)
123
+ !(@options[key].nil? || @options[key].empty?)
124
+ end
125
+
126
+ def url_matches(key, env)
127
+ path = Rack::Request.new(env).fullpath
128
+ @options[key].select { |url_regex| path.match(url_regex) }.empty? ? false : true
129
+ end
130
+
109
131
  end
@@ -1,3 +1,3 @@
1
1
  class RackApiKey
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -1,164 +1,211 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe RackApiKey do
4
- include Rack::Test::Methods
4
+ include Rack::Test::Methods
5
5
 
6
- class Account; end
6
+ class Account; end
7
7
 
8
- class ApiKey
8
+ class ApiKey
9
9
 
10
- attr_reader :value
10
+ attr_reader :value
11
11
 
12
- def initialize(value)
13
- @value = value
14
- end
12
+ def initialize(value)
13
+ @value = value
14
+ end
15
15
 
16
- def self.find(value)
17
- nil
18
- end
16
+ def self.find(value)
17
+ nil
18
+ end
19
19
 
20
- def account
21
- Account.new
22
- end
20
+ def account
21
+ Account.new
22
+ end
23
23
 
24
- end
24
+ end
25
25
 
26
- # simple test app for the middleware test
27
- def app
26
+ # simple test app for the middleware test
27
+ def app
28
28
 
29
- Rack::Builder.new do
29
+ Rack::Builder.new do
30
30
  map '/' do
31
- use RackApiKey, :api_key_proc => Proc.new { |val| ApiKey.find(val) }
32
- run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
33
- end
34
-
35
- map "/no-api-proc" do
36
- use RackApiKey
37
- run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
38
- end
39
-
40
- map "/all-options" do
41
- use RackApiKey,
42
- :api_key_proc => Proc.new { |val| ApiKey.find(val) },
43
- :rack_api_key => "account.api.key",
44
- :header_key => "HTTP_X_CUSTOM_API_HEADER"
45
- run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
46
- end
47
-
48
- map "/url-restricted" do
49
- use RackApiKey,
50
- :api_key_proc => Proc.new { |val| ApiKey.find(val) },
51
- :url_restriction => [/url-restricted\/foo/]
52
- run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
53
- end
54
-
55
- end
56
- end
57
-
58
- context "when using the predefined default middleware options" do
59
-
60
- it 'attempts to find the ApiKey with the header value' do
61
- ApiKey.should_receive(:find).with("SECRET API KEY")
62
- get "/", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
63
- end
64
-
65
- it 'responds with a 200 upon successful authorization' do
66
- ApiKey.stub(:find).and_return(ApiKey.new("SECRET API KEY"))
67
- get "/", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
68
- last_response.ok?.should be_true
69
- end
70
-
71
- it 'responds with a 401 when the ApiKey is not found' do
72
- ApiKey.stub(:find).and_return(nil)
73
- get "/", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
74
- last_response.status.should == 401
75
- end
76
-
77
- it 'responds with a 401 when the header is not set' do
78
- header("HTTP_X_API_KEY", nil)
79
- get "/"
80
- last_response.status.should == 401
81
- end
82
-
83
- it 'responds with a JSON formatted error message' do
84
- header("HTTP_X_API_KEY", nil)
85
- get "/"
86
- last_response.body.should == "The API key provided is not authorized."
87
- end
88
-
89
- it 'sets the api key in the env' do
90
- ApiKey.stub(:find).and_return(api_key = ApiKey.new("SECRET API KEY"))
91
- get "/", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
92
- last_request.env['rack_api_key'].should == api_key
93
- end
94
-
95
- end
96
-
97
- context "when the API key lookup proc is not provided" do
98
-
99
- it 'raises an error' do
100
- expect { get "/no-api-proc", {}, {} }.to raise_error(NotImplementedError, "Caller must implement a way to lookup an API key.")
101
- end
102
-
103
- end
104
-
105
- context "when all the options are specified" do
106
-
107
- it 'attempts to find the ApiKey with the header value' do
108
- ApiKey.should_receive(:find).with("SECRET API KEY")
109
- get "/all-options", {}, "HTTP_X_CUSTOM_API_HEADER" => "SECRET API KEY"
110
- end
111
-
112
- it 'responds with a 200 upon successful authorization' do
113
- ApiKey.stub(:find).and_return(ApiKey.new("SECRET API KEY"))
114
- get "/all-options", {}, "HTTP_X_CUSTOM_API_HEADER" => "SECRET API KEY"
115
- last_response.ok?.should be_true
116
- end
117
-
118
- it 'sets the api key in the env' do
119
- ApiKey.stub(:find).and_return(api_key = ApiKey.new("SECRET API KEY"))
120
- get "/all-options", {}, "HTTP_X_CUSTOM_API_HEADER" => "SECRET API KEY"
121
- last_request.env['account.api.key'].should == api_key
122
- end
123
-
124
- end
125
-
126
- context "when URL restriction is used" do
127
-
128
- context "and the requesting URL matches the restricted list" do
129
-
130
- it 'attempts to find the ApiKey with the header value' do
131
- ApiKey.should_receive(:find).with("SECRET API KEY")
132
- get "/url-restricted/foo", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
133
- end
134
-
135
- it 'responds with a 200 upon successful authorization' do
136
- ApiKey.stub(:find).and_return(ApiKey.new("SECRET API KEY"))
137
- get "/url-restricted/foo", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
138
- last_response.ok?.should be_true
139
- end
140
-
141
- it 'responds with a 401 when the header is not set' do
142
- header("HTTP_X_API_KEY", nil)
143
- get "/url-restricted/foo"
144
- last_response.status.should == 401
145
- end
146
-
147
- end
148
-
149
- context "and the requesting URL is not in the restricted list" do
150
-
151
- it 'does not attempt to find the ApiKey with the header value' do
152
- ApiKey.should_not_receive(:find).with("SECRET API KEY")
153
- get "/url-restricted/bar"
154
- end
31
+ use RackApiKey, :api_key_proc => Proc.new { |val| ApiKey.find(val) }
32
+ run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
33
+ end
34
+
35
+ map "/no-api-proc" do
36
+ use RackApiKey
37
+ run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
38
+ end
39
+
40
+ map "/all-options" do
41
+ use RackApiKey,
42
+ :api_key_proc => Proc.new { |val| ApiKey.find(val) },
43
+ :rack_api_key => "account.api.key",
44
+ :header_key => "HTTP_X_CUSTOM_API_HEADER"
45
+ run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
46
+ end
47
+
48
+ map "/url-restricted" do
49
+ use RackApiKey,
50
+ :api_key_proc => Proc.new { |val| ApiKey.find(val) },
51
+ :url_restriction => [/url-restricted\/foo/]
52
+ run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
53
+ end
54
+
55
+ map '/url-exclusion' do
56
+ use RackApiKey,
57
+ :api_key_proc => Proc.new { |val| ApiKey.find(val) },
58
+ :url_exclusion => [/url-exclusion\/foo/]
59
+ run lambda { |env| [200, { 'Content-Type' => 'text/html' }, 'Testing Middleware'] }
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+
66
+ context "when using the predefined default middleware options" do
67
+
68
+ it 'attempts to find the ApiKey with the header value' do
69
+ ApiKey.should_receive(:find).with("SECRET API KEY")
70
+ get "/", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
71
+ end
72
+
73
+ it 'responds with a 200 upon successful authorization' do
74
+ ApiKey.stub(:find).and_return(ApiKey.new("SECRET API KEY"))
75
+ get "/", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
76
+ last_response.ok?.should be_true
77
+ end
78
+
79
+ it 'responds with a 401 when the ApiKey is not found' do
80
+ ApiKey.stub(:find).and_return(nil)
81
+ get "/", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
82
+ last_response.status.should == 401
83
+ end
84
+
85
+ it 'responds with a 401 when the header is not set' do
86
+ header("HTTP_X_API_KEY", nil)
87
+ get "/"
88
+ last_response.status.should == 401
89
+ end
155
90
 
156
- it 'responds with a 200 without the header set' do
157
- get "/url-restricted/bar"
158
- last_response.ok?.should be_true
159
- end
160
-
161
- end
91
+ it 'responds with a JSON formatted error message' do
92
+ header("HTTP_X_API_KEY", nil)
93
+ get "/"
94
+ last_response.body.should == "The API key provided is not authorized."
95
+ end
162
96
 
163
- end
97
+ it 'sets the api key in the env' do
98
+ ApiKey.stub(:find).and_return(api_key = ApiKey.new("SECRET API KEY"))
99
+ get "/", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
100
+ last_request.env['rack_api_key'].should == api_key
101
+ end
102
+
103
+ end
104
+
105
+ context "when the API key lookup proc is not provided" do
106
+
107
+ it 'raises an error' do
108
+ expect { get "/no-api-proc", {}, {} }.to raise_error(NotImplementedError, "Caller must implement a way to lookup an API key.")
109
+ end
110
+
111
+ end
112
+
113
+ context "when all the options are specified" do
114
+
115
+ it 'attempts to find the ApiKey with the header value' do
116
+ ApiKey.should_receive(:find).with("SECRET API KEY")
117
+ get "/all-options", {}, "HTTP_X_CUSTOM_API_HEADER" => "SECRET API KEY"
118
+ end
119
+
120
+ it 'responds with a 200 upon successful authorization' do
121
+ ApiKey.stub(:find).and_return(ApiKey.new("SECRET API KEY"))
122
+ get "/all-options", {}, "HTTP_X_CUSTOM_API_HEADER" => "SECRET API KEY"
123
+ last_response.ok?.should be_true
124
+ end
125
+
126
+ it 'sets the api key in the env' do
127
+ ApiKey.stub(:find).and_return(api_key = ApiKey.new("SECRET API KEY"))
128
+ get "/all-options", {}, "HTTP_X_CUSTOM_API_HEADER" => "SECRET API KEY"
129
+ last_request.env['account.api.key'].should == api_key
130
+ end
131
+
132
+ end
133
+
134
+ context "when URL restriction is used" do
135
+
136
+ context "and the requesting URL matches the restricted list" do
137
+
138
+ it 'attempts to find the ApiKey with the header value' do
139
+ ApiKey.should_receive(:find).with("SECRET API KEY")
140
+ get "/url-restricted/foo", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
141
+ end
142
+
143
+ it 'responds with a 200 upon successful authorization' do
144
+ ApiKey.stub(:find).and_return(ApiKey.new("SECRET API KEY"))
145
+ get "/url-restricted/foo", {}, "HTTP_X_API_KEY" => "SECRET API KEY"
146
+ last_response.ok?.should be_true
147
+ end
148
+
149
+ it 'responds with a 401 when the header is not set' do
150
+ header("HTTP_X_API_KEY", nil)
151
+ get "/url-restricted/foo"
152
+ last_response.status.should == 401
153
+ end
154
+
155
+ end
156
+
157
+ context "and the requesting URL is not in the restricted list" do
158
+
159
+ it 'does not attempt to find the ApiKey with the header value' do
160
+ ApiKey.should_not_receive(:find).with("SECRET API KEY")
161
+ get "/url-restricted/bar"
162
+ end
163
+
164
+ it 'responds with a 200 without the header set' do
165
+ get "/url-restricted/bar"
166
+ last_response.ok?.should be_true
167
+ end
168
+
169
+ end
170
+
171
+ end
172
+
173
+ context 'when URL exclusion is used' do
174
+
175
+ context 'and the requesting URL matches the excluded list' do
176
+
177
+ it 'does not attempt to find the ApiKey with the header value' do
178
+ ApiKey.should_not_receive(:find)
179
+ get '/url-exclusion/foo'
180
+ end
181
+
182
+ it 'responds with a 200 without the header set' do
183
+ get '/url-exclusion/foo'
184
+ last_response.should be_ok
185
+ end
186
+
187
+ end
188
+
189
+ context 'and the requesting URL is not in the exclusion list' do
190
+
191
+ it 'attempts to find the ApiKey with the header value' do
192
+ ApiKey.should_receive(:find).with('SECRET API KEY')
193
+ get '/url-exclusion/bar', {}, 'HTTP_X_API_KEY' => 'SECRET API KEY'
194
+ end
195
+
196
+ it 'responds with a 200 upon successful authorization' do
197
+ ApiKey.stub(:find).and_return(ApiKey.new('SECRET API KEY'))
198
+ get '/url-exclusion/bar', {}, 'HTTP_X_API_KEY' => 'SECRET API KEY'
199
+ last_response.should be_ok
200
+ end
201
+
202
+ it 'responds with a 401 when the header is not set' do
203
+ header('HTTP_X_API_KEY', nil)
204
+ get '/url-exclusion/bar'
205
+ last_response.status.should eq 401
206
+ end
207
+
208
+ end
209
+
210
+ end
164
211
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-api-key
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-29 00:00:00.000000000 Z
12
+ date: 2013-05-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -107,7 +107,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
107
107
  version: '0'
108
108
  segments:
109
109
  - 0
110
- hash: 2860984967925191594
110
+ hash: -216640359850820027
111
111
  required_rubygems_version: !ruby/object:Gem::Requirement
112
112
  none: false
113
113
  requirements:
@@ -116,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
116
  version: '0'
117
117
  segments:
118
118
  - 0
119
- hash: 2860984967925191594
119
+ hash: -216640359850820027
120
120
  requirements: []
121
121
  rubyforge_project:
122
122
  rubygems_version: 1.8.25