rack-api-key 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # RackApiKey
2
2
 
3
+ [![Build Status](https://travis-ci.org/techwhizbang/rack-api-key.png)](https://travis-ci.org/techwhizbang/rack-api-key)
4
+
3
5
  RackApiKey is a middleware that relies on the client submitting requests
4
- with a header named "X-API-KEY" storing their private API key as the value.
6
+ with a header named "X-API-KEY" storing their private API key as the value.
5
7
  The middleware will then intercept the request, read the value from the named
6
8
  header and call the given "proc" used for API key lookup. The API key lookup
7
9
  should only return a value if there is an exact match for the value stored in
8
10
  the named API key header.
9
- If such a API key exists, the middleware will pass the request onward and also
11
+ If such an API key exists, the middleware will pass the request onward and also
10
12
  set a new value in the request representing the authenticated API key. Otherwise
11
13
  the middleware will return a HTTP status of 401, and a plain text message
12
14
  notifying the calling requestor that they are not authorized.
@@ -28,18 +30,97 @@ Or install it yourself as:
28
30
  ## Usage
29
31
 
30
32
  ```ruby
31
- Rack::Builder.new do
32
- map '/' do
33
- use RackApiKey, :api_key_proc => Proc.new { |val| ApiKey.find(val) }
34
- run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
35
- end
36
-
37
- map "/all-options" do
38
- use RackApiKey,
39
- :api_key_proc => Proc.new { |val| ApiKey.find(val) },
40
- :rack_api_key => "account.api.key",
41
- :header_key => "HTTP_X_CUSTOM_API_HEADER"
42
- run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
43
- end
44
- end
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"
36
+ ```
37
+
38
+ ### :header_key
39
+ It's important to note that internally Rack actually mutates any given headers
40
+ and prefixes them with HTTP and subsequently underscores them. For example if an
41
+ API client passed "X-API-KEY" in the header, Rack would interpret that header
42
+ as "HTTP_X_API_KEY". "HTTP_X_API_KEY" is the default header. If you want to use
43
+ a different header you can specify it in the :header_key options Hash.
44
+
45
+ ### :api_key_proc
46
+ This is required, there is no default behavior, and the middleware will not work
47
+ properly unless you specify a Proc that takes one argument.
48
+ The value the Proc receives will be the value set in the API header key.
49
+ Use anything you like to determine if the header value
50
+ is valid. If the value is invalid, the Proc should return nil, otherwise return
51
+ a value that will ultimately be set in the Rack env.
52
+
53
+ ### :rack_api_key
54
+ This is the key that will be set in the Rack env with the return value of the
55
+ :api_key_proc.
56
+
57
+ ### :url_restriction
58
+ This is an option that can restrict the rack-api-middleware to specific URLs.
59
+ This works well when you have a mixture of API endpoints that require
60
+ authentication and some that might not. Or a combination of API endpoints and
61
+ publicly facing webpages. Perhaps you've scoped all of your API endpoints to
62
+ "/api", and the rest of the URL mappings or routes are supposed to be wide open.
63
+
64
+
65
+ ### unauthorized_api_key method
66
+ This is a method that can be overridden with however you'd like to respond
67
+ when a request with an invalid or unauthorized API key is encountered. The default
68
+ behavior responds with a 401 plain/text message. I find it especially useful to
69
+ override this method and switch the response to JSON format.
70
+
71
+ ### valid_api_key? method
72
+ This is another method that can be overridden if there are additional checks
73
+ and validations beyond the ones already provided. For instance the API key
74
+ may exist, but for some reason it was temporarily disabled. You could add a check
75
+ for that here.
76
+
77
+ ### rack_api_key_request_setter method
78
+ The default behavior of this method will take the return value of the API key
79
+ proc and set it to the Rack env with the ke specified by :rack_api_key. You
80
+ may override this method if you prefer setting something else in the Rack env
81
+ or perhaps nothing at all.
82
+
83
+ ## Examples
84
+
85
+ ```ruby
86
+ # Overridden to use the default behavior plus check if the api key is enabled.
87
+ def valid_api_key?(api_header_val, api_key_lookup_val)
88
+ super && api_key_lookup_val.enabled?
89
+ end
90
+ ```
91
+
92
+ ```ruby
93
+ # Overridden to respond in JSON format.
94
+ def unauthorized_api_key
95
+ body_text = {"error" => "blah blah blah"}.to_json
96
+ [401, {'Content-Type' => 'application/json; charset=utf-8',
97
+ 'Content-Length' => body_text.size.to_s},
98
+ [body_text]]
99
+ end
100
+ ```
101
+
102
+ ```ruby
103
+ # Overridden to set the Account attached to the API key instead.
104
+ def rack_api_key_request_setter(env, api_key_lookup_val)
105
+ env[@options[:rack_api_key]] = api_key_lookup_val.account
106
+ end
107
+ ```
108
+
109
+ ```ruby
110
+ Rack::Builder.new do
111
+ map '/' do
112
+ use RackApiKey,
113
+ :api_key_proc => Proc.new { |val| ApiKey.find(val) },
114
+ :url_restriction => [/api/]
115
+ run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
116
+ end
117
+
118
+ map "/all-options" do
119
+ use RackApiKey,
120
+ :api_key_proc => Proc.new { |val| ApiKey.find(val) },
121
+ :rack_api_key => "account.api.key",
122
+ :header_key => "HTTP_X_CUSTOM_API_HEADER"
123
+ run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
124
+ end
125
+ end
45
126
  ```
@@ -1,3 +1,3 @@
1
1
  class RackApiKey
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/rack-api-key.rb CHANGED
@@ -23,7 +23,14 @@ class RackApiKey
23
23
  # on successful authentication. The default value is
24
24
  # "rack_api_key".
25
25
  # * +:header_key+ - A way to override the header's name used to store the API key.
26
- # The default value is "HTTP_X_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.
27
34
  #
28
35
  # ==== Example
29
36
  # use RackApiKey,
@@ -35,21 +42,26 @@ class RackApiKey
35
42
  default_options = {
36
43
  :header_key => "HTTP_X_API_KEY",
37
44
  :rack_api_key => "rack_api_key",
38
- :api_key_proc => Proc.new { raise NotImplementedError.new("Caller must implement a way to lookup an API key.") }
45
+ :api_key_proc => Proc.new { raise NotImplementedError.new("Caller must implement a way to lookup an API key.") },
46
+ :url_restriction => []
39
47
  }
40
48
  @options = default_options.merge(options)
41
49
  end
42
50
 
43
51
  def call(env)
44
- api_header_val = env[@options[:header_key]]
45
- api_key_lookup_val = @options[:api_key_proc].call(api_header_val)
46
-
47
- if valid_api_key?(api_header_val, api_key_lookup_val)
48
- rack_api_key_request_setter(env, api_key_lookup_val)
49
- @app.call(env)
50
- else
51
- unauthorized_api_key
52
+
53
+ if @options[:url_restriction].nil? || @options[:url_restriction].empty?
54
+ 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
52
63
  end
64
+
53
65
  end
54
66
 
55
67
  ##
@@ -80,4 +92,18 @@ class RackApiKey
80
92
  !api_key_lookup_val.nil? && api_key_lookup_val != ""
81
93
  end
82
94
 
95
+ private
96
+
97
+ def process_request(env)
98
+ api_header_val = env[@options[:header_key]]
99
+ api_key_lookup_val = @options[:api_key_proc].call(api_header_val)
100
+
101
+ if valid_api_key?(api_header_val, api_key_lookup_val)
102
+ rack_api_key_request_setter(env, api_key_lookup_val)
103
+ @app.call(env)
104
+ else
105
+ unauthorized_api_key
106
+ end
107
+ end
108
+
83
109
  end
@@ -25,6 +25,7 @@ describe RackApiKey do
25
25
 
26
26
  # simple test app for the middleware test
27
27
  def app
28
+
28
29
  Rack::Builder.new do
29
30
  map '/' do
30
31
  use RackApiKey, :api_key_proc => Proc.new { |val| ApiKey.find(val) }
@@ -43,6 +44,13 @@ describe RackApiKey do
43
44
  :header_key => "HTTP_X_CUSTOM_API_HEADER"
44
45
  run lambda { |env| [200, {"Content-Type" => "text/html"}, "Testing Middleware"] }
45
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
46
54
 
47
55
  end
48
56
  end
@@ -114,4 +122,43 @@ describe RackApiKey do
114
122
  end
115
123
 
116
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
155
+
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
162
+
163
+ end
117
164
  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.1
4
+ version: 0.0.2
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-04 00:00:00.000000000 Z
12
+ date: 2013-04-29 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: 3957032321281409751
110
+ hash: 2860984967925191594
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: 3957032321281409751
119
+ hash: 2860984967925191594
120
120
  requirements: []
121
121
  rubyforge_project:
122
122
  rubygems_version: 1.8.25