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.
@@ -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
- The 'all' section of the yaml file is a LockBox-specific shortcut so you can DRY up your protected path definitions.
54
- Settings in more specific environments will override those in the 'all' section.
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.
@@ -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
- auth_response = self.class.get("/authentication/#{api_key}", {:headers => request.get_xreferer_auth_headers, :request => {:application_name => LockBox.config['application_name']}})
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
- auth_response = self.class.get("/authentication/hmac", {:headers => hmac_request.get_xreferer_auth_headers, :request => {:application_name => LockBox.config['application_name']}})
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
- @cache.write(cache_string_for_key(api_key), expiration.to_i) if caching_allowed
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
- return true if hmac_request.hmac_auth({hmac_id => key}) == key
172
- return nil
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: 11
4
+ hash: 7
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
8
  - 6
9
- - 2
10
- version: 1.6.2
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.0
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.