lockbox_middleware 1.6.2 → 1.6.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +18 -6
- data/lib/lockbox_middleware.rb +61 -9
- data/spec/lib/lockbox_middleware_spec.rb +41 -0
- metadata +18 -4
data/README.rdoc
CHANGED
@@ -38,20 +38,31 @@ to protect with LockBox.
|
|
38
38
|
|
39
39
|
Here's an example lockbox.yml:
|
40
40
|
|
41
|
+
common: &COMMON
|
42
|
+
protected_paths:
|
43
|
+
- ^/api/
|
41
44
|
production:
|
45
|
+
<<: *COMMON
|
42
46
|
base_uri: http://lockbox.foo.org
|
43
47
|
development:
|
48
|
+
<<: *COMMON
|
44
49
|
base_uri: http://localhost:3001
|
45
50
|
cucumber:
|
51
|
+
<<: *COMMON
|
46
52
|
base_uri: http://localhost:3001
|
47
53
|
test:
|
54
|
+
<<: *COMMON
|
48
55
|
base_uri: http://localhost:3001
|
49
|
-
all:
|
50
|
-
protect_paths:
|
51
|
-
- ^/api/
|
52
56
|
|
53
|
-
|
54
|
-
|
57
|
+
== Graphite Integration
|
58
|
+
|
59
|
+
The lockbox-middleware gem supports sending runtime metrics to Graphite (http://graphite.wikidot.com/) via statsd-client.
|
60
|
+
This is off by default, but can be turned on by adding the keys 'statsd_host', 'statsd_port', and 'graphite_prefix' to
|
61
|
+
your lockbox.yml file. For example:
|
62
|
+
|
63
|
+
statsd_host: statsd.example.com
|
64
|
+
statsd_port: 8125
|
65
|
+
graphite_prefix: my.app.lockbox_middleware
|
55
66
|
|
56
67
|
== Server Installation
|
57
68
|
|
@@ -81,6 +92,7 @@ Github: http://github.com/dnclabs/lockbox/tree/master
|
|
81
92
|
- Chris Gill
|
82
93
|
- Brian Cardarella
|
83
94
|
- Wes Morgan
|
95
|
+
- Dave Steinberg
|
84
96
|
|
85
97
|
Copyright 2010 Democratic National Committee,
|
86
|
-
All Rights Reserved.
|
98
|
+
All Rights Reserved.
|
data/lib/lockbox_middleware.rb
CHANGED
@@ -2,6 +2,7 @@ require 'rubygems'
|
|
2
2
|
require 'httpotato'
|
3
3
|
require 'lockbox_cache'
|
4
4
|
require 'hmac_request'
|
5
|
+
require 'statsd'
|
5
6
|
|
6
7
|
class LockBox
|
7
8
|
include HTTPotato
|
@@ -37,10 +38,11 @@ class LockBox
|
|
37
38
|
def initialize(app)
|
38
39
|
@app = app
|
39
40
|
@cache = LockBoxCache::Cache.new
|
41
|
+
@graphite = setup_graphite
|
40
42
|
end
|
41
43
|
|
42
44
|
def call(env)
|
43
|
-
dup.call!(env)
|
45
|
+
time_it("call") { dup.call!(env) }
|
44
46
|
end
|
45
47
|
|
46
48
|
def cache_string_for_key(api_key)
|
@@ -61,20 +63,25 @@ class LockBox
|
|
61
63
|
if protected_path
|
62
64
|
request = HmacRequest.new_from_rack_env(env)
|
63
65
|
if !request['key'].nil?
|
66
|
+
auth_type = 'key'
|
64
67
|
auth = auth_via_key(request['key'], request)
|
65
68
|
else
|
69
|
+
auth_type = 'hmac'
|
66
70
|
auth = auth_via_hmac(request)
|
67
71
|
end
|
68
72
|
|
69
73
|
if auth[:authorized]
|
74
|
+
record_it("#{auth_type}.authorized")
|
70
75
|
app_response = @app.call(env)
|
71
76
|
return [app_response[0], app_response[1].merge(auth[:headers]), app_response[2]]
|
72
77
|
else
|
78
|
+
record_it("#{auth_type}.denied")
|
73
79
|
message = "Access Denied"
|
74
80
|
return [401, {'Content-Type' => 'text/plain', 'Content-Length' => "#{message.length}"}, [message]]
|
75
81
|
end
|
76
82
|
else
|
77
83
|
#pass everything else straight through to app
|
84
|
+
record_it("unprotected")
|
78
85
|
return @app.call(env)
|
79
86
|
end
|
80
87
|
end
|
@@ -83,7 +90,11 @@ class LockBox
|
|
83
90
|
cached_auth = check_key_cache(api_key)
|
84
91
|
# currently we don't cache forward headers
|
85
92
|
return {:authorized => cached_auth, :headers => {}} if cached_auth
|
86
|
-
|
93
|
+
|
94
|
+
auth_response = time_it("key.http_request") {
|
95
|
+
self.class.get("/authentication/#{api_key}", {:headers => request.get_xreferer_auth_headers, :request => {:application_name => LockBox.config['application_name']}})
|
96
|
+
}
|
97
|
+
|
87
98
|
authorized = (auth_response.code == 200)
|
88
99
|
cache_key_response_if_allowed(api_key, auth_response) if authorized
|
89
100
|
{:authorized => authorized, :headers => response_headers(auth_response)}
|
@@ -92,7 +103,11 @@ class LockBox
|
|
92
103
|
def auth_via_hmac(hmac_request)
|
93
104
|
cached_auth = check_hmac_cache(hmac_request)
|
94
105
|
return {:authorized => cached_auth, :headers => {}} if cached_auth
|
95
|
-
|
106
|
+
|
107
|
+
auth_response = time_it("hmac.http_request") {
|
108
|
+
self.class.get("/authentication/hmac", {:headers => hmac_request.get_xreferer_auth_headers, :request => {:application_name => LockBox.config['application_name']}})
|
109
|
+
}
|
110
|
+
|
96
111
|
authorized = (auth_response.code == 200)
|
97
112
|
cache_hmac_response_if_allowed(hmac_request, auth_response) if authorized
|
98
113
|
{:authorized => authorized, :headers => response_headers(auth_response)}
|
@@ -113,7 +128,9 @@ class LockBox
|
|
113
128
|
end
|
114
129
|
caching_allowed = (cache_max_age > 0 && cache_public)
|
115
130
|
expiration = Time.at(Time.now.to_i + cache_max_age)
|
116
|
-
|
131
|
+
if caching_allowed
|
132
|
+
time_it("key.cache_write") { @cache.write(cache_string_for_key(api_key), expiration.to_i) }
|
133
|
+
end
|
117
134
|
end
|
118
135
|
|
119
136
|
def cache_hmac_response_if_allowed(hmac_request, auth_response)
|
@@ -131,7 +148,7 @@ class LockBox
|
|
131
148
|
expiration = Time.at(Time.now.to_i + cache_max_age)
|
132
149
|
if caching_allowed
|
133
150
|
api_key = auth_response.headers['X-LockBox-API-Key']
|
134
|
-
@cache.write(cache_string_for_hmac(hmac_request.hmac_id), [api_key, expiration.to_i])
|
151
|
+
time_it("hmac.cache_write") { @cache.write(cache_string_for_hmac(hmac_request.hmac_id), [api_key, expiration.to_i]) }
|
135
152
|
end
|
136
153
|
end
|
137
154
|
|
@@ -144,13 +161,15 @@ class LockBox
|
|
144
161
|
end
|
145
162
|
|
146
163
|
def check_key_cache(api_key)
|
147
|
-
expiration = @cache.read(cache_string_for_key(api_key))
|
164
|
+
expiration = time_it("key.cache_read") { @cache.read(cache_string_for_key(api_key)) }
|
148
165
|
return nil if expiration.nil?
|
149
166
|
expiration = Time.at(expiration)
|
150
167
|
if expiration <= Time.now
|
168
|
+
record_it("key.cache_expired")
|
151
169
|
@cache.delete(cache_string_for_key(api_key))
|
152
170
|
nil
|
153
171
|
else
|
172
|
+
record_it("key.cache_hit")
|
154
173
|
true
|
155
174
|
end
|
156
175
|
end
|
@@ -158,18 +177,51 @@ class LockBox
|
|
158
177
|
def check_hmac_cache(hmac_request)
|
159
178
|
hmac_id, hmac_hash = hmac_request.hmac_id, hmac_request.hmac_hash
|
160
179
|
return nil if hmac_id.nil? || hmac_hash.nil?
|
161
|
-
cached_val = @cache.read(cache_string_for_hmac(hmac_id))
|
180
|
+
cached_val = time_it("hmac.cache_read") { @cache.read(cache_string_for_hmac(hmac_id)) }
|
162
181
|
return nil if cached_val.nil?
|
163
182
|
key, expiration = cached_val
|
164
183
|
expiration = Time.at(expiration)
|
165
184
|
if expiration <= Time.now
|
185
|
+
record_it("hmac.cache_expired")
|
166
186
|
@cache.delete(cache_string_for_hmac(hmac_id))
|
167
187
|
nil
|
168
188
|
else
|
169
189
|
#as long as the request is signed correctly, no need to contact the lockbox server to verify
|
170
190
|
#just see if the request is signed properly and let it through if it is
|
171
|
-
|
172
|
-
|
191
|
+
if hmac_request.hmac_auth({hmac_id => key}) == key
|
192
|
+
record_it("hmac.cache_hit")
|
193
|
+
return true
|
194
|
+
else
|
195
|
+
return nil
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def graphite_path
|
201
|
+
self.class.config["graphite_path"]
|
202
|
+
end
|
203
|
+
def setup_graphite
|
204
|
+
return nil unless ( self.class.config.has_key?("statsd_host") &&
|
205
|
+
self.class.config.has_key?("statsd_port") &&
|
206
|
+
self.class.config.has_key?("graphite_path") )
|
207
|
+
Statsd.host = self.class.config["statsd_host"]
|
208
|
+
Statsd.port = self.class.config["statsd_port"]
|
209
|
+
Statsd
|
210
|
+
end
|
211
|
+
|
212
|
+
def record_it(data_path)
|
213
|
+
Statsd.increment("#{graphite_path}.#{data_path}") if @graphite
|
214
|
+
end
|
215
|
+
|
216
|
+
def time_it(data_path)
|
217
|
+
start_ts = Time.now
|
218
|
+
rv = yield
|
219
|
+
|
220
|
+
if @graphite
|
221
|
+
#puts "Calling #timing with #{graphite_path}.#{data_path}"
|
222
|
+
Statsd.timing( "#{graphite_path}.#{data_path}", (Time.now - start_ts) * 1000 )
|
173
223
|
end
|
224
|
+
|
225
|
+
rv
|
174
226
|
end
|
175
227
|
end
|
@@ -136,6 +136,7 @@ describe 'LockBox' do
|
|
136
136
|
env = Rack::MockRequest.env_for "/api/some_controller/some_action?key=123456"
|
137
137
|
app.call(env)[1].should include('Content-Type')
|
138
138
|
end
|
139
|
+
|
139
140
|
end
|
140
141
|
|
141
142
|
it "should cache lockbox responses for max-age when Cache-Control allows it" do
|
@@ -375,4 +376,44 @@ describe 'LockBox' do
|
|
375
376
|
end
|
376
377
|
end
|
377
378
|
|
379
|
+
context "logging to statsd / graphite" do
|
380
|
+
before(:each) do
|
381
|
+
@graphite_path = "foo.bar"
|
382
|
+
safely_edit_config_file({:statsd_host => "localhost", :statsd_port => 8125, :graphite_path => @graphite_path})
|
383
|
+
|
384
|
+
@max_age = 3600
|
385
|
+
successful_response = mock("MockResponse")
|
386
|
+
successful_response.stubs(:code).returns(200)
|
387
|
+
successful_response.stubs(:headers).returns({'Cache-Control' => "public,max-age=#{@max_age},must-revalidate"})
|
388
|
+
LockBox.stubs(:get).with("/authentication/123456", any_parameters).returns(successful_response)
|
389
|
+
bad_response = mock("MockResponse")
|
390
|
+
bad_response.stubs(:code).returns(401)
|
391
|
+
bad_response.stubs(:headers).returns({'Cache-Control' => 'public,no-cache'})
|
392
|
+
LockBox.stubs(:get).with("/authentication/blah", any_parameters).returns(bad_response)
|
393
|
+
|
394
|
+
Statsd.stubs(:timing)
|
395
|
+
Statsd.stubs(:increment)
|
396
|
+
end
|
397
|
+
|
398
|
+
after :each do
|
399
|
+
if @tmp_config_file && @config_file
|
400
|
+
FileUtils.mv(@tmp_config_file, @config_file)
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
it "should record timing data for the overall request" do
|
405
|
+
get "/api/some_controller/some_action?key=123456"
|
406
|
+
Statsd.should have_received( :timing ).with("#{@graphite_path}.call", anything)
|
407
|
+
end
|
408
|
+
|
409
|
+
it "should record a successful authorization" do
|
410
|
+
get "/api/some_controller/some_action?key=123456"
|
411
|
+
Statsd.should have_received( :increment ).with("#{@graphite_path}.key.authorized", anything)
|
412
|
+
end
|
413
|
+
|
414
|
+
it "should record denied requests" do
|
415
|
+
get "/api/some_controller/some_action?key=blah"
|
416
|
+
Statsd.should have_received( :increment ).with("#{@graphite_path}.key.denied", anything)
|
417
|
+
end
|
418
|
+
end
|
378
419
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lockbox_middleware
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 7
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
8
|
- 6
|
9
|
-
-
|
10
|
-
version: 1.6.
|
9
|
+
- 4
|
10
|
+
version: 1.6.4
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Chris Gill
|
@@ -64,6 +64,20 @@ dependencies:
|
|
64
64
|
version: "0"
|
65
65
|
type: :runtime
|
66
66
|
version_requirements: *id003
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: statsd-client
|
69
|
+
prerelease: false
|
70
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 3
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
type: :runtime
|
80
|
+
version_requirements: *id004
|
67
81
|
description: Rack middleware for the LockBox centralized API authorization service. Brought to you by the DNC Innovation Lab.
|
68
82
|
email: innovationlab@dnc.org
|
69
83
|
executables: []
|
@@ -115,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
129
|
requirements: []
|
116
130
|
|
117
131
|
rubyforge_project:
|
118
|
-
rubygems_version: 1.6.
|
132
|
+
rubygems_version: 1.6.1
|
119
133
|
signing_key:
|
120
134
|
specification_version: 3
|
121
135
|
summary: Rack middleware for the LockBox centralized API authorization service.
|