rack-api-key 0.0.1 → 0.0.2

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
@@ -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