lockbox_middleware 1.2.0
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/LICENSE +24 -0
- data/README.rdoc +48 -0
- data/lib/lockbox_cache.rb +48 -0
- data/lib/lockbox_middleware.rb +156 -0
- data/spec/lib/lockbox_middleware_spec.rb +229 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/mocha.rb +13 -0
- metadata +110 -0
data/LICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Copyright (c) 2010, Democratic National Committee
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
* Redistributions of source code must retain the above copyright
|
7
|
+
notice, this list of conditions and the following disclaimer.
|
8
|
+
* Redistributions in binary form must reproduce the above copyright
|
9
|
+
notice, this list of conditions and the following disclaimer in the
|
10
|
+
documentation and/or other materials provided with the distribution.
|
11
|
+
* Neither the name of the Democratic National Committee nor the
|
12
|
+
names of its contributors may be used to endorse or promote products
|
13
|
+
derived from this software without specific prior written permission.
|
14
|
+
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
16
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
17
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
19
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
20
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
21
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
22
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
23
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
24
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
= LockBox
|
2
|
+
|
3
|
+
LockBox is a centralized API authentication service written by the DNC Innovation Lab. It lets your API
|
4
|
+
users share a single identity across multiple services.
|
5
|
+
It is licensed under the New BSD License (see the LICENSE file for details).
|
6
|
+
|
7
|
+
It is a Ruby on Rails application on the server side, and Rack middleware on the client side
|
8
|
+
(which means it integrates nicely with any modern Ruby web framework). As of v1.2.0, there is an
|
9
|
+
unfortunate Rails dependency in the middleware gem. Hopefully we'll get rid of that soon.
|
10
|
+
|
11
|
+
Lockbox handles things like rate limiting, API key signup and management, and supports HMAC
|
12
|
+
authentication as well as plain-text key exchange. We are working on replacing HMAC with OAuth 2.0.
|
13
|
+
|
14
|
+
== Configuration
|
15
|
+
|
16
|
+
LockBox needs a configuration file named "lockbox.yml" in order to work. In a Rack app (incl. Rails),
|
17
|
+
this file should be placed in app_root/config/lockbox.yml.
|
18
|
+
|
19
|
+
You should define (for each of your environments), the base_uri of your app and the relative paths you want
|
20
|
+
to protect with LockBox.
|
21
|
+
|
22
|
+
Here's an example lockbox.yml:
|
23
|
+
|
24
|
+
production:
|
25
|
+
base_uri: http://lockbox.foo.org
|
26
|
+
development:
|
27
|
+
base_uri: http://localhost:3001
|
28
|
+
cucumber:
|
29
|
+
base_uri: http://localhost:3001
|
30
|
+
test:
|
31
|
+
base_uri: http://localhost:3001
|
32
|
+
all:
|
33
|
+
protect_paths:
|
34
|
+
- ^/api/
|
35
|
+
|
36
|
+
== Download
|
37
|
+
|
38
|
+
Github: http://github.com/dnclabs/lockbox/tree/master
|
39
|
+
|
40
|
+
== Authors
|
41
|
+
|
42
|
+
- Nathan Woodhull
|
43
|
+
- Chris Gill
|
44
|
+
- Brian Cardarella
|
45
|
+
- Wes Morgan
|
46
|
+
|
47
|
+
Copyright 2010 Democratic National Committee,
|
48
|
+
All Rights Reserved.
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module LockBoxCache
|
4
|
+
class Cache
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :@cache, :write, :read, :delete
|
7
|
+
|
8
|
+
class RailsCache
|
9
|
+
def write(key, value)
|
10
|
+
Rails.cache.write(key, value)
|
11
|
+
end
|
12
|
+
|
13
|
+
def read(key)
|
14
|
+
Rails.cache.read(key)
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete(key)
|
18
|
+
Rails.cache.delete(key)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class HashCache
|
23
|
+
def initialize
|
24
|
+
@store = Hash.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def write(key, value)
|
28
|
+
@store[key] = value
|
29
|
+
end
|
30
|
+
|
31
|
+
def read(key)
|
32
|
+
@store[key]
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(key)
|
36
|
+
@store.delete(key)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize
|
41
|
+
if defined?(Rails)
|
42
|
+
@cache = RailsCache.new
|
43
|
+
else
|
44
|
+
@cache = HashCache.new
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'lockbox_cache'
|
3
|
+
require 'auth-hmac'
|
4
|
+
|
5
|
+
class LockBox
|
6
|
+
include HTTParty
|
7
|
+
include LockBoxCache
|
8
|
+
|
9
|
+
def self.config
|
10
|
+
if defined?(Rails)
|
11
|
+
root_dir = Rails.root
|
12
|
+
else
|
13
|
+
root_dir = '.'
|
14
|
+
end
|
15
|
+
yaml_config = YAML.load_file(File.join(root_dir,'config','lockbox.yml'))
|
16
|
+
return_config = {}
|
17
|
+
environment = Rails.env if defined? Rails
|
18
|
+
environment ||= ENV['RACK_ENV']
|
19
|
+
environment ||= 'test'
|
20
|
+
if !environment.nil?
|
21
|
+
if !yaml_config['all'].nil?
|
22
|
+
return_config = yaml_config['all']
|
23
|
+
return_config.merge!(yaml_config[environment])
|
24
|
+
else
|
25
|
+
return_config = yaml_config[environment]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
return_config
|
29
|
+
end
|
30
|
+
|
31
|
+
base_uri config['base_uri']
|
32
|
+
|
33
|
+
def initialize(app)
|
34
|
+
@app = app
|
35
|
+
@cache = LockBoxCache::Cache.new
|
36
|
+
end
|
37
|
+
|
38
|
+
def call(env)
|
39
|
+
dup.call!(env)
|
40
|
+
end
|
41
|
+
|
42
|
+
def protected_paths
|
43
|
+
self.class.config['protect_paths'].map do |path|
|
44
|
+
Regexp.new(path)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def call!(env)
|
49
|
+
#attempt to authenticate any requests to /api
|
50
|
+
request = Rack::Request.new(env)
|
51
|
+
path_protected = false
|
52
|
+
protected_paths.each do |path|
|
53
|
+
if env['PATH_INFO'] =~ path
|
54
|
+
path_protected = true
|
55
|
+
authorized = false
|
56
|
+
key = request['key']
|
57
|
+
if key.blank?
|
58
|
+
key = 'hmac'
|
59
|
+
end
|
60
|
+
|
61
|
+
auth = auth_response(key,env)
|
62
|
+
authorized = auth[:authorized]
|
63
|
+
auth_headers = auth[:headers]
|
64
|
+
|
65
|
+
if authorized
|
66
|
+
app_response = @app.call(env)
|
67
|
+
app_headers = app_response[1]
|
68
|
+
response_headers = app_headers.merge(auth_headers)
|
69
|
+
return [app_response[0], response_headers, app_response[2]]
|
70
|
+
else
|
71
|
+
message = "Access Denied"
|
72
|
+
return [401, {'Content-Type' => 'text/plain', 'Content-Length' => "#{message.length}"}, message]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
unless path_protected
|
77
|
+
#pass everything else straight through to app
|
78
|
+
return @app.call(env)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def auth_response(api_key, env={})
|
83
|
+
if api_key != 'hmac'
|
84
|
+
cached_auth = auth_cache(api_key)
|
85
|
+
if !cached_auth.nil?
|
86
|
+
# currently we don't cache forward headers
|
87
|
+
return {:authorized => cached_auth, :headers => {}}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
auth_response = self.class.get("/authentication/#{api_key}", {:headers => auth_headers(env)})
|
91
|
+
authorized = (auth_response.code == 200)
|
92
|
+
cache_response_if_allowed(api_key, auth_response) if authorized
|
93
|
+
{:authorized => authorized, :headers => response_headers(auth_response)}
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def cache_response_if_allowed(api_key, auth_response)
|
99
|
+
cache_control = auth_response.headers['Cache-Control'].split(/,\s*/)
|
100
|
+
cache_max_age = 0
|
101
|
+
cache_public = false
|
102
|
+
cache_control.each do |c|
|
103
|
+
if c =~ /^max-age=\s*(\d+)$/
|
104
|
+
cache_max_age = $1.to_i
|
105
|
+
elsif c == 'public'
|
106
|
+
cache_public = true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
caching_allowed = (cache_max_age > 0 && cache_public)
|
110
|
+
expiration = cache_max_age.seconds.since(Time.now)
|
111
|
+
cache_auth(api_key,expiration) if caching_allowed
|
112
|
+
end
|
113
|
+
|
114
|
+
def response_headers(auth_response)
|
115
|
+
headers = {}
|
116
|
+
auth_response.headers.each_pair do |h,v|
|
117
|
+
if h =~ /^X-RateLimit-/
|
118
|
+
headers[h] = v
|
119
|
+
elsif h =~ /^X-LockBox-/
|
120
|
+
headers[h] = v
|
121
|
+
end
|
122
|
+
end
|
123
|
+
headers
|
124
|
+
end
|
125
|
+
|
126
|
+
def auth_headers(env)
|
127
|
+
headers = {}
|
128
|
+
headers['Referer'] = "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}#{env['PATH_INFO']}"
|
129
|
+
headers['Referer'] << "?#{env['QUERY_STRING']}" unless env['QUERY_STRING'].blank?
|
130
|
+
{'Content-Type' => 'Content-Type', 'Content-MD5' => 'Content-MD5', 'Date' => 'HTTP_DATE', 'Method' => 'REQUEST_METHOD', 'Authorization' => 'HTTP_AUTHORIZATION'}.each_pair do |h,e|
|
131
|
+
headers["X-Referer-#{h}"] = env[e] unless env[e].blank?
|
132
|
+
end
|
133
|
+
headers
|
134
|
+
end
|
135
|
+
|
136
|
+
def cache_key(api_key)
|
137
|
+
"lockbox_#{api_key}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def auth_cache(api_key)
|
141
|
+
expiration = @cache.read(cache_key(api_key))
|
142
|
+
return nil if expiration.nil?
|
143
|
+
expiration = Time.at(expiration)
|
144
|
+
if expiration <= Time.now
|
145
|
+
@cache.delete(cache_key(api_key))
|
146
|
+
nil
|
147
|
+
elsif expiration > Time.now
|
148
|
+
true
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def cache_auth(api_key,expiration)
|
153
|
+
@cache.write(cache_key(api_key),expiration.to_i)
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rack/test'
|
3
|
+
require 'lockbox_middleware'
|
4
|
+
|
5
|
+
describe 'LockBox' do
|
6
|
+
include Rack::Test::Methods
|
7
|
+
|
8
|
+
def app
|
9
|
+
# Initialize our LockBox middleware with an "app" that just always returns 200, if it gets .called
|
10
|
+
LockBox.new(Proc.new {|env| [200,{},"successfully hit rails app"]})
|
11
|
+
end
|
12
|
+
|
13
|
+
def safely_edit_config_file(settings, env=nil)
|
14
|
+
env ||= Rails.env if defined?(Rails)
|
15
|
+
env ||= ENV['RACK_ENV']
|
16
|
+
env ||= 'test'
|
17
|
+
@config_file = File.join(File.dirname(__FILE__),'..','..','config','lockbox.yml')
|
18
|
+
@tmp_config_file = "#{@config_file}.testing"
|
19
|
+
FileUtils.cp(@config_file, @tmp_config_file)
|
20
|
+
config = YAML.load_file(@config_file)
|
21
|
+
settings.each_pair do |setting,value|
|
22
|
+
config[env][setting.to_s] = value
|
23
|
+
end
|
24
|
+
File.open( @config_file, 'w' ) do |out|
|
25
|
+
YAML.dump( config, out )
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "setting the base_uri" do
|
30
|
+
let(:base_uri) { "http://localhost:3001" }
|
31
|
+
|
32
|
+
it "should use the base_uri specified in the config" do
|
33
|
+
safely_edit_config_file({:base_uri => base_uri})
|
34
|
+
LockBox.base_uri.should == base_uri
|
35
|
+
end
|
36
|
+
|
37
|
+
after :each do
|
38
|
+
if @tmp_config_file && @config_file
|
39
|
+
FileUtils.mv(@tmp_config_file, @config_file)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "setting the protected paths" do
|
45
|
+
let(:path1) { "^/api/" }
|
46
|
+
let(:path2) { "^/foo/bar/" }
|
47
|
+
let(:path3) { "/lookup/?$" }
|
48
|
+
|
49
|
+
before :each do
|
50
|
+
safely_edit_config_file({:protect_paths => [path1, path2, path3]}, 'all')
|
51
|
+
successful_response = mock("MockResponse")
|
52
|
+
successful_response.stubs(:code).returns(200)
|
53
|
+
successful_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
|
54
|
+
LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(successful_response)
|
55
|
+
bad_response = mock("MockResponse")
|
56
|
+
bad_response.stubs(:code).returns(401)
|
57
|
+
bad_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
|
58
|
+
LockBox.stubs(:get).with("/authentication/invalid", any_parameters).returns(bad_response)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should protect path1" do
|
62
|
+
get "/api/foo?key=invalid"
|
63
|
+
last_response.status.should == 401
|
64
|
+
get "/api/foo?key=123456"
|
65
|
+
last_response.status.should == 200
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should protect path2" do
|
69
|
+
get "/foo/bar/baz?key=invalid"
|
70
|
+
last_response.status.should == 401
|
71
|
+
get "/foo/bar/baz?key=123456"
|
72
|
+
last_response.status.should == 200
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should protect path3" do
|
76
|
+
get "/polling_place/lookup?key=invalid"
|
77
|
+
last_response.status.should == 401
|
78
|
+
get "/polling_place/lookup?key=123456"
|
79
|
+
last_response.status.should == 200
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should not protect other paths" do
|
83
|
+
get "/bar/baz"
|
84
|
+
last_response.status.should == 200
|
85
|
+
last_response.body.should == "successfully hit rails app"
|
86
|
+
end
|
87
|
+
|
88
|
+
after :each do
|
89
|
+
if @tmp_config_file && @config_file
|
90
|
+
FileUtils.mv(@tmp_config_file, @config_file)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "hitting API actions" do
|
96
|
+
before :each do
|
97
|
+
@max_age = 3600
|
98
|
+
successful_response = mock("MockResponse")
|
99
|
+
successful_response.stubs(:code).returns(200)
|
100
|
+
successful_response.stubs(:headers).returns({'Cache-Control' => "public,max-age=#{@max_age},must-revalidate"})
|
101
|
+
LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(successful_response)
|
102
|
+
bad_response = mock("MockResponse")
|
103
|
+
bad_response.stubs(:code).returns(401)
|
104
|
+
bad_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
|
105
|
+
LockBox.stubs(:get).with("/authentication/blah", any_parameters).returns(bad_response)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should return 401 for a request that starts with /api with invalid api key" do
|
109
|
+
get "/api/some_controller/some_action?key=blah"
|
110
|
+
last_response.status.should == 401
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should return 200 for a request that starts with /api and has api key" do
|
114
|
+
get "/api/some_controller/some_action?key=123456"
|
115
|
+
last_response.status.should == 200
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should cache lockbox responses for max-age when Cache-Control allows it" do
|
119
|
+
get "/api/some_controller/some_action?key=123456"
|
120
|
+
last_response.status.should == 200
|
121
|
+
bad_response = mock("MockResponse")
|
122
|
+
bad_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
|
123
|
+
bad_response.stubs(:code).returns(401)
|
124
|
+
LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(bad_response)
|
125
|
+
get "/api/some_controller/some_action?key=123456"
|
126
|
+
last_response.status.should == 200
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should expire cached lockbox responses when max-age seconds have passed" do
|
130
|
+
get "/api/some_controller/some_action?key=123456"
|
131
|
+
last_response.status.should == 200
|
132
|
+
bad_response = mock("MockResponse")
|
133
|
+
bad_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
|
134
|
+
bad_response.stubs(:code).returns(401)
|
135
|
+
LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(bad_response)
|
136
|
+
expired_time = @max_age.seconds.since(Time.now)
|
137
|
+
Time.stubs(:now).returns(expired_time)
|
138
|
+
get "/api/some_controller/some_action?key=123456"
|
139
|
+
last_response.status.should == 401
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should not cache lockbox responses when Cache-Control does not allow it" do
|
143
|
+
successful_response = mock("MockResponse")
|
144
|
+
successful_response.stubs(:code).returns(200)
|
145
|
+
successful_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
|
146
|
+
LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(successful_response)
|
147
|
+
get "/api/some_controller/some_action?key=123456"
|
148
|
+
last_response.status.should == 200
|
149
|
+
bad_response = mock("MockResponse")
|
150
|
+
bad_response.stubs(:code).returns(401)
|
151
|
+
bad_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
|
152
|
+
LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(bad_response)
|
153
|
+
get "/api/some_controller/some_action?key=123456"
|
154
|
+
last_response.status.should == 401
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should pass along the rate limit headers to the client if they exist" do
|
158
|
+
successful_response = mock("MockResponse")
|
159
|
+
successful_response.stubs(:code).returns(200)
|
160
|
+
headers = {
|
161
|
+
'X-RateLimit-Limit' => '100',
|
162
|
+
'X-RateLimit-Remaining' => '99',
|
163
|
+
'X-RateLimit-Reset' => 1.hour.from_now.to_i.to_s
|
164
|
+
}
|
165
|
+
successful_response.stubs(:headers).returns(headers.merge({'Cache-Control' => 'public,no-cache'}))
|
166
|
+
LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(successful_response)
|
167
|
+
get "/api/some_controller/some_action?key=123456"
|
168
|
+
headers.each_pair do |header,value|
|
169
|
+
# just tests that the headers are present; the stubs above ensure the values are what we expect
|
170
|
+
last_response.headers[header].should == value
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
context "hitting API actions with HMAC auth" do
|
177
|
+
before :each do
|
178
|
+
successful_response = mock("MockResponse")
|
179
|
+
successful_response.stubs(:code).returns(200)
|
180
|
+
successful_response.stubs(:headers).returns({'Cache-Control' => 'public, no-cache'})
|
181
|
+
Time.stubs(:now).returns(Time.parse("2010-05-10 16:30:00 EDT"))
|
182
|
+
valid_headers = {'X-Referer-Method' => 'GET', 'X-Referer-Date' => [Time.now.httpdate], 'X-Referer-Authorization' => ['AuthHMAC key-id:uxx+EgyzWBKBgS+Y8MzpcWcfy7k='], 'Referer' => 'http://example.org/api/some_controller/some_action'}
|
183
|
+
LockBox.stubs(:get).with("/authentication/hmac", {:headers => valid_headers}).returns(successful_response)
|
184
|
+
|
185
|
+
bad_response = mock("MockResponse")
|
186
|
+
bad_response.stubs(:code).returns(401)
|
187
|
+
bad_response.stubs(:headers).returns({'Cache-Control' => 'public, no-cache'})
|
188
|
+
invalid_headers = {'X-Referer-Method' => 'GET', 'X-Referer-Date' => [Time.now.httpdate], 'X-Referer-Authorization' => 'foo', 'Referer' => 'http://example.org/api/some_controller/some_action'}
|
189
|
+
LockBox.stubs(:get).with("/authentication/hmac", {:headers => invalid_headers}).returns(bad_response)
|
190
|
+
|
191
|
+
@path = "/api/some_controller/some_action"
|
192
|
+
|
193
|
+
hmac_request = Net::HTTP::Get.new(@path, {'Date' => Time.now.httpdate})
|
194
|
+
store = mock("MockStore")
|
195
|
+
store.stubs(:[]).with('key-id').returns("123456")
|
196
|
+
authhmac = AuthHMAC.new(store)
|
197
|
+
authhmac.sign!(hmac_request, 'key-id')
|
198
|
+
@hmac_headers = hmac_request.to_hash
|
199
|
+
end
|
200
|
+
|
201
|
+
it "should return 200 for an HMAC request with a valid auth header" do
|
202
|
+
@hmac_headers.each_pair do |key,value|
|
203
|
+
header key, value
|
204
|
+
end
|
205
|
+
get @path
|
206
|
+
last_response.status.should == 200
|
207
|
+
end
|
208
|
+
|
209
|
+
it "should return 401 for an HMAC request with an invalid auth header" do
|
210
|
+
@hmac_headers['authorization'] = 'foo'
|
211
|
+
@hmac_headers.each_pair do |key,value|
|
212
|
+
header key, value
|
213
|
+
end
|
214
|
+
get @path
|
215
|
+
last_response.status.should == 401
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
context "hitting actions without API" do
|
220
|
+
|
221
|
+
it "should not try to authenticate a request that doesn't start with /api" do
|
222
|
+
get "/"
|
223
|
+
last_response.status.should == 200
|
224
|
+
last_response.body.should == "successfully hit rails app"
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# This file is copied to ~/spec when you run 'ruby script/generate rspec'
|
2
|
+
# from the project root directory.
|
3
|
+
ENV["RAILS_ENV"] ||= 'test'
|
4
|
+
env_file = File.expand_path(File.join(File.dirname(__FILE__),'..','config','environment'))
|
5
|
+
if File.exists?("#{env_file}.rb")
|
6
|
+
require env_file
|
7
|
+
require 'spec/autorun'
|
8
|
+
require 'spec/rails'
|
9
|
+
require 'authlogic/test_case'
|
10
|
+
else
|
11
|
+
require 'rubygems'
|
12
|
+
require 'mocha' # gem install jferris-mocha, not regular mocha
|
13
|
+
end
|
14
|
+
|
15
|
+
# Requires supporting files with custom matchers and macros, etc,
|
16
|
+
# in ./support/ and its subdirectories.
|
17
|
+
Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f}
|
18
|
+
|
19
|
+
Spec::Runner.configure do |config|
|
20
|
+
if defined?(Rails)
|
21
|
+
config.use_transactional_fixtures = true
|
22
|
+
config.use_instantiated_fixtures = false
|
23
|
+
config.fixture_path = RAILS_ROOT + '/spec/fixtures/'
|
24
|
+
config.include(Authlogic::TestCase)
|
25
|
+
end
|
26
|
+
config.mock_with Mocha::API
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lockbox_middleware
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 2
|
8
|
+
- 0
|
9
|
+
version: 1.2.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Chris Gill
|
13
|
+
- Brian Cardarella
|
14
|
+
- Nathan Woodhull
|
15
|
+
- Wes Morgan
|
16
|
+
autorequire:
|
17
|
+
bindir: bin
|
18
|
+
cert_chain: []
|
19
|
+
|
20
|
+
date: 2010-06-15 00:00:00 -04:00
|
21
|
+
default_executable:
|
22
|
+
dependencies:
|
23
|
+
- !ruby/object:Gem::Dependency
|
24
|
+
name: httparty
|
25
|
+
prerelease: false
|
26
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 5
|
33
|
+
- 2
|
34
|
+
version: 0.5.2
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: auth-hmac
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: rack
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - "="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
segments:
|
57
|
+
- 1
|
58
|
+
- 1
|
59
|
+
- 0
|
60
|
+
version: 1.1.0
|
61
|
+
type: :runtime
|
62
|
+
version_requirements: *id003
|
63
|
+
description: Rack middleware for the Lockbox centralized API authorization service. Brought to you by the DNC Innovation Lab.
|
64
|
+
email: innovationlab@dnc.org
|
65
|
+
executables: []
|
66
|
+
|
67
|
+
extensions: []
|
68
|
+
|
69
|
+
extra_rdoc_files:
|
70
|
+
- LICENSE
|
71
|
+
- README.rdoc
|
72
|
+
files:
|
73
|
+
- lib/lockbox_cache.rb
|
74
|
+
- lib/lockbox_middleware.rb
|
75
|
+
- LICENSE
|
76
|
+
- README.rdoc
|
77
|
+
has_rdoc: true
|
78
|
+
homepage:
|
79
|
+
licenses: []
|
80
|
+
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options:
|
83
|
+
- --charset=UTF-8
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
segments:
|
91
|
+
- 0
|
92
|
+
version: "0"
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
segments:
|
98
|
+
- 0
|
99
|
+
version: "0"
|
100
|
+
requirements: []
|
101
|
+
|
102
|
+
rubyforge_project:
|
103
|
+
rubygems_version: 1.3.6
|
104
|
+
signing_key:
|
105
|
+
specification_version: 3
|
106
|
+
summary: Centralized API authorization
|
107
|
+
test_files:
|
108
|
+
- spec/lib/lockbox_middleware_spec.rb
|
109
|
+
- spec/spec_helper.rb
|
110
|
+
- spec/support/mocha.rb
|