lockbox_middleware 1.6.2 → 1.6.4
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.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.
|