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