rack-api-key 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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