sso 0.1.2 → 0.1.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5e1f2feee69f64105e38a748befeebe0de6d33a5
4
- data.tar.gz: c0637201a0d104c075dc7c049eef0d9f381e0596
3
+ metadata.gz: b9660c9f78d14f395529c39a41a4355c7b0141cd
4
+ data.tar.gz: 56552af9e5cc37cae7ef084c68e80a0ac0a4d4c0
5
5
  SHA512:
6
- metadata.gz: 3fbaa085b9054c762040a7a91f19999ce4a7745b388c431ff754f7b7c23ffc6ac21e15f8bc3cfeb995dd8803682cd363117f13577ccd4c7ddb24647f96a179db
7
- data.tar.gz: 336dfa7ce8ee44a663fe2a8678c67966f021f4a2927fded0d239333877e9043e9fead2c503f1296d2df54b225e913cfbfed669d43ad612e3a9921679a710c346
6
+ metadata.gz: b86244b2218c89bdac9433bf12c69ee0119e834cc1ec2c9d24a260e5121c05810a28cfbdb70cb012032383e59c1969e8322e5be21a7ef67b10973de0af66c8a5
7
+ data.tar.gz: 2823fc5f108f37bda20eaf72c75abaa70db5a270cb5522fa7d2dee388e57d26990d8a2010d2d283946bf3cef28d36a62be6254eea09b7f0fe3af9319185f30f4
data/lib/sso.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  require 'operation'
2
2
 
3
3
  require 'sso/logging'
4
+ require 'sso/meter'
4
5
  require 'sso/benchmarking'
5
-
6
- require 'sso/server/configuration'
7
- require 'sso/server/configure'
6
+ require 'sso/configuration'
7
+ require 'sso/configure'
8
8
 
9
9
  require 'sso/client/omniauth/strategies/sso'
10
10
 
@@ -2,13 +2,17 @@ module SSO
2
2
  # Helper to log results of benchmarks.
3
3
  module Benchmarking
4
4
  include ::SSO::Logging
5
+ include ::SSO::Meter
5
6
 
6
- def benchmark(name, &block)
7
+ def benchmark(name: nil, metric: nil, &block)
8
+ return unless block_given?
7
9
  result = nil
8
10
  seconds = Benchmark.realtime do
9
11
  result = block.call
10
12
  end
11
- info { "#{name} took #{(seconds * 1000).round}ms" }
13
+ milliseconds = (seconds * 1000).round
14
+ debug { "#{name || metric || 'Benchmark'} took #{milliseconds}ms" }
15
+ histogram key: metric, value: milliseconds if metric
12
16
  result
13
17
  end
14
18
  end
@@ -85,7 +85,7 @@ module SSO
85
85
  end
86
86
 
87
87
  def failure_rack_array
88
- payload = { success: true, code: :passport_verification_failed }
88
+ payload = { success: false, code: :passport_verification_failed }
89
89
  [200, { 'Content-Type' => 'application/json' }, [payload.to_json]]
90
90
  end
91
91
 
@@ -121,7 +121,7 @@ module SSO
121
121
  end
122
122
 
123
123
  def decrypt_chip!
124
- benchmark 'Passport chip decryption' do
124
+ benchmark(name: 'Passport chip decryption') do
125
125
  decipher = chip_digest
126
126
  decipher.decrypt
127
127
  decipher.key = chip_key
@@ -18,18 +18,22 @@ module SSO
18
18
  fetch_response { |failure| return failure }
19
19
  interpret_response
20
20
 
21
- rescue JSON::ParserError
21
+ rescue ::JSON::ParserError
22
22
  error { 'SSO Server response is not valid JSON.' }
23
23
  error { response.inspect }
24
+ Operations.failure :server_response_not_parseable, object: response
25
+ end
26
+
27
+ def human_readable_timeout_in_ms
28
+ "#{timeout_in_milliseconds}ms"
24
29
  end
25
30
 
26
31
  private
27
32
 
28
33
  def fetch_response
29
- yield Operations.failure(:server_unreachable, object: response) unless response.code == 200
34
+ yield Operations.failure(:server_unreachable, object: response) unless response.code.to_s == '200'
30
35
  yield Operations.failure(:server_response_not_parseable, object: response) unless parsed_response
31
36
  yield Operations.failure(:server_response_missing_success_flag, object: response) unless response_has_success_flag?
32
- yield Operations.failure(:server_response_unsuccessful, object: response) unless parsed_response['success'].to_s == 'true'
33
37
  Operations.success :server_response_looks_legit
34
38
  end
35
39
 
@@ -130,7 +134,7 @@ module SSO
130
134
 
131
135
  def response!
132
136
  debug { "Fetching Passport from #{endpoint.inspect}" }
133
- benchmark 'Passport authorization request' do
137
+ benchmark(name: 'Passport verification request', metric: 'client.passport.verification.duration') do
134
138
  ::HTTParty.get endpoint, timeout: timeout_in_seconds, query: query_params, headers: { 'Accept' => 'application/json' }
135
139
  end
136
140
  end
@@ -140,7 +144,7 @@ module SSO
140
144
  end
141
145
 
142
146
  def response_has_success_flag?
143
- parsed_response && parsed_response.respond_to?(:key?) && parsed_response.key?('success')
147
+ parsed_response && parsed_response.respond_to?(:key?) && (parsed_response.key?('success') || parsed_response.key?(:success))
144
148
  end
145
149
 
146
150
  end
@@ -12,6 +12,7 @@ module SSO
12
12
  class AfterFetch
13
13
  include ::SSO::Logging
14
14
  include ::SSO::Benchmarking
15
+ include ::SSO::Meter
15
16
 
16
17
  attr_reader :passport, :warden, :options
17
18
  delegate :request, to: :warden
@@ -31,12 +32,14 @@ module SSO
31
32
  return unless passport.is_a?(::SSO::Client::Passport)
32
33
  verify
33
34
 
34
- rescue Timeout::Error
35
+ rescue ::Timeout::Error
35
36
  error { 'SSO Server timed out. Continuing with last known authentication/authorization...' }
36
- # meter status: :timeout, scope: scope, passport_id: user.passport_id, timeout_ms: human_readable_timeout_in_ms
37
+ meter :timeout, timeout_ms: verifier.human_readable_timeout_in_ms
38
+ Operations.failure :server_request_timed_out
37
39
 
38
40
  rescue => exception
39
41
  ::SSO.config.exception_handler.call exception
42
+ Operations.failure :client_exception_caught
40
43
  end
41
44
 
42
45
  private
@@ -53,14 +56,17 @@ module SSO
53
56
  verification.code
54
57
  end
55
58
 
59
+ def verification_object
60
+ verification.object
61
+ end
62
+
56
63
  def verify
57
64
  debug { "Validating Passport #{passport.id.inspect} of logged in #{passport.user.class} in scope #{warden_scope.inspect}" }
58
65
 
59
66
  case verification_code
60
67
  when :server_unreachable then server_unreachable!
61
68
  when :server_response_not_parseable then server_response_not_parseable!
62
- when :server_response_missing_success_flag! then server_response_missing_success_flag!
63
- when :server_response_unsuccessful! then server_response_unsuccessful!
69
+ when :server_response_missing_success_flag then server_response_missing_success_flag!
64
70
  when :passport_valid then passport_valid!
65
71
  when :passport_valid_and_modified then passport_valid_and_modified!(verification.object)
66
72
  when :passport_invalid then passport_invalid!
@@ -74,39 +80,54 @@ module SSO
74
80
  passport.modified!
75
81
  passport.user = modified_passport.user
76
82
  passport.state = modified_passport.state
77
- # meter status: :valid, passport_id: user.passport_id
83
+ meter :valid_and_modified
84
+ Operations.success :valid_and_modified
78
85
  end
79
86
 
80
87
  def passport_valid!
81
88
  debug { 'Valid passport, no changes' }
82
89
  passport.verified!
83
- # meter status: :valid, passport_id: user.passport_id
90
+ meter :valid
91
+ Operations.success :valid
84
92
  end
85
93
 
86
94
  def passport_invalid!
87
95
  info { 'Your Passport is not valid any more.' }
88
96
  warden.logout warden_scope
89
- # meter status: :invalid, passport_id: user.passport_id
97
+ meter :invalid
98
+ Operations.failure :invalid
90
99
  end
91
100
 
92
101
  def server_unreachable!
93
- error { "SSO Server responded with an unexpected HTTP status code (#{response.code.inspect} instead of 200)." }
102
+ error { "SSO Server responded with an unexpected HTTP status code (#{verification_code.inspect} instead of 200). #{verification_object.inspect}" }
103
+ meter :server_unreachable
104
+ Operations.failure :server_unreachable
94
105
  end
95
106
 
96
107
  def server_response_missing_success_flag!
97
108
  error { 'SSO Server response did not include the expected success flag.' }
109
+ meter :server_response_missing_success_flag
110
+ Operations.failure :server_response_missing_success_flag
98
111
  end
99
112
 
100
113
  def unexpected_server_response_status!
101
114
  error { "SSO Server response did not include a known passport status code. #{verification_code.inspect}" }
115
+ meter :unexpected_server_response_status
116
+ Operations.failure :unexpected_server_response_status
102
117
  end
103
118
 
104
119
  def server_response_not_parseable!
105
120
  error { 'SSO Server response could not be parsed at all.' }
121
+ meter :server_response_not_parseable
122
+ Operations.failure :server_response_not_parseable
106
123
  end
107
124
 
108
- def meter(*_)
109
- # This will be a hook for e.g. statistics, benchmarking, etc, measure everything
125
+ def meter(key, data = {})
126
+ options[:key] = "client.warden.hooks.after_fetch.#{key}"
127
+ options[:tags] = { scope: warden_scope }
128
+ data[:passport_id] = passport.id
129
+ options[:data] = data
130
+ track options
110
131
  end
111
132
 
112
133
  # TODO: Use ActionDispatch remote IP or you might get the Load Balancer's IP instead :(
@@ -21,7 +21,7 @@ module SSO
21
21
  debug { "Persisting trusted Passport #{authentication.object.inspect}" }
22
22
  success! authentication.object
23
23
  else
24
- debug { 'Authentication from Passport failed.' }
24
+ debug { 'Authentication from Passport on Client failed.' }
25
25
  debug { "Responding with #{authentication.object.inspect}" }
26
26
  custom! authentication.object
27
27
  end
@@ -31,7 +31,7 @@ module SSO
31
31
  end
32
32
 
33
33
  def passport_authentication
34
- benchmark 'Passport proxy verification' do
34
+ benchmark(name: 'Passport proxy verification request', metric: 'client.passport.verification') do
35
35
  ::SSO::Client::Authentications::Passport.new(request).authenticate
36
36
  end
37
37
  end
@@ -2,6 +2,7 @@ require 'logger'
2
2
 
3
3
  module SSO
4
4
  class Configuration
5
+ include ::SSO::Logging
5
6
 
6
7
  # Server
7
8
 
@@ -54,6 +55,11 @@ module SSO
54
55
  end
55
56
  attr_writer :exception_handler
56
57
 
58
+ def metric
59
+ @metric || default_metric
60
+ end
61
+ attr_writer :metric
62
+
57
63
  def passport_chip_key
58
64
  @passport_chip_key || fail('You need to configure a secret passport_chip_key, see SSO::Configuration for more info.')
59
65
  end
@@ -110,8 +116,14 @@ module SSO
110
116
  end
111
117
  end
112
118
 
119
+ def default_metric
120
+ proc do |type:, key:, value:, tags:, data:|
121
+ debug { "Measuring #{type.inspect} with key #{key.inspect} and value #{value.inspect} and tags #{tags} and data #{data}" }
122
+ end
123
+ end
124
+
113
125
  def default_session_backend
114
- fail('You need to configure session_backend, see SSO::Configuration for more info.') unless %w(developmen test).include?(environment)
126
+ fail('You need to configure session_backend, see SSO::Configuration for more info.') unless %w(development test).include?(environment)
115
127
  end
116
128
 
117
129
  def default_passport_verification_timeout_ms
File without changes
data/lib/sso/meter.rb ADDED
@@ -0,0 +1,32 @@
1
+ module SSO
2
+ module Meter
3
+ include ::SSO::Logging
4
+
5
+ def track(key:, value: 1, tags: nil, data: {})
6
+ data[:caller] = caller_name
7
+ # info { "Measuring increment #{key.inspect} with value #{value.inspect} and tags #{tags} and data #{data}" }
8
+ metric.call type: :increment, key: "sso.#{key}", value: value, tags: tags, data: data
9
+
10
+ rescue => exception
11
+ ::SSO.config.exception_handler.call exception
12
+ end
13
+
14
+ def histogram(key:, value:, tags: nil, data: {})
15
+ data[:caller] = caller_name
16
+ # info { "Measuring histogram #{key.inspect} with value #{value.inspect} and tags #{tags} and data #{data}" }
17
+ metric.call type: :histogram, key: "sso.#{key}", value: value, tags: tags, data: data
18
+
19
+ rescue => exception
20
+ ::SSO.config.exception_handler.call exception
21
+ end
22
+
23
+ def caller_name
24
+ self.class.name
25
+ end
26
+
27
+ def metric
28
+ ::SSO.config.metric
29
+ end
30
+
31
+ end
32
+ end
@@ -15,7 +15,7 @@ module SSO
15
15
  result
16
16
  else
17
17
  # TODO: Prevent Flooding here.
18
- debug { "The Passport authentication failed: #{result.code}" }
18
+ debug { "The Server Passport authentication failed: #{result.code}" }
19
19
  Operations.failure :passport_authentication_failed, object: failure_rack_array
20
20
  end
21
21
  end
@@ -73,7 +73,7 @@ module SSO
73
73
  # all passports simply because a load balancer is pointing to the wrong Rails application or something.
74
74
  #
75
75
  def failure_rack_array
76
- payload = { success: true, code: :passport_invalid }
76
+ payload = { success: false, code: :passport_invalid }
77
77
  [200, { 'Content-Type' => 'application/json' }, [payload.to_json]]
78
78
  end
79
79
 
@@ -27,7 +27,7 @@ module SSO
27
27
  end
28
28
 
29
29
  def json_code(code)
30
- [200, { 'Content-Type' => 'application/json' }, [{ success: true, code: code }.to_json]]
30
+ [200, { 'Content-Type' => 'application/json' }, [{ success: false, code: code }.to_json]]
31
31
  end
32
32
 
33
33
  def passports_path
@@ -19,16 +19,16 @@ module SSO
19
19
  end
20
20
 
21
21
  token = request.params['access_token']
22
- debug { "Detected incoming Passport creation request for access token #{token.inspect}" }
22
+ debug { "Detected incoming Passport exchange request for access token #{token.inspect}" }
23
23
  access_token = ::Doorkeeper::AccessToken.find_by_token token
24
24
 
25
- return json_code :access_token_not_found unless access_token
26
- return json_code :access_token_invalid unless access_token.valid?
25
+ return json_error :access_token_not_found unless access_token
26
+ return json_error :access_token_invalid unless access_token.valid?
27
27
 
28
28
  finding = ::SSO::Server::Passports.find_by_access_token_id(access_token.id)
29
29
  if finding.failure?
30
30
  # This should never happen. Every Access Token should be connected to a Passport.
31
- return json_code :passport_not_found
31
+ return json_error :passport_not_found
32
32
  end
33
33
  passport = finding.object
34
34
 
@@ -44,8 +44,8 @@ module SSO
44
44
  [200, { 'Content-Type' => 'application/json' }, [payload.to_json]]
45
45
  end
46
46
 
47
- def json_code(code)
48
- [200, { 'Content-Type' => 'application/json' }, [{ success: true, code: code }.to_json]]
47
+ def json_error(code)
48
+ [200, { 'Content-Type' => 'application/json' }, [{ success: false, code: code }.to_json]]
49
49
  end
50
50
 
51
51
  def passports_path
@@ -46,7 +46,7 @@ module SSO
46
46
  end
47
47
 
48
48
  def state!
49
- result = benchmark 'Passport user state calculation' do
49
+ result = benchmark(name: 'Passport user state calculation') do
50
50
  OpenSSL::HMAC.hexdigest user_state_digest, user_state_key, user_state_base
51
51
  end
52
52
  debug { "The user state is #{result.inspect}" }
@@ -62,7 +62,7 @@ module SSO
62
62
  end
63
63
 
64
64
  def chip!
65
- benchmark 'Passport chip encryption' do
65
+ benchmark(name: 'Passport chip encryption') do
66
66
  ensure_secret
67
67
  cipher = chip_digest
68
68
  cipher.encrypt
@@ -20,8 +20,8 @@ module SSO
20
20
  debug { "Responding with #{authentication.object}" }
21
21
  custom! authentication.object
22
22
  else
23
- debug { 'Authentication from Passport failed.' }
24
- fail authentication.code
23
+ debug { 'Authentication from Passport on Server failed.' }
24
+ custom! authentication.object
25
25
  end
26
26
 
27
27
  rescue => exception
@@ -29,7 +29,7 @@ module SSO
29
29
  end
30
30
 
31
31
  def passport_authentication
32
- benchmark 'Passport verification' do
32
+ benchmark(name: 'Passport verification') do
33
33
  ::SSO::Server::Authentications::Passport.new(request).authenticate
34
34
  end
35
35
  end
@@ -2,7 +2,7 @@ class SessionsController < ApplicationController
2
2
  include ::SSO::Logging
3
3
  delegate :logout, to: :warden
4
4
 
5
- before_action :not_json, only: [:new]
5
+ before_action :prevent_json, only: [:new]
6
6
 
7
7
  # POI
8
8
  def new
@@ -27,8 +27,9 @@ class SessionsController < ApplicationController
27
27
 
28
28
  private
29
29
 
30
- def not_json
31
- return unless request.format == :json
30
+ def prevent_json
31
+ return unless request.format.to_sym == :json
32
+ warn { "This request is asking for JSON where it shouldn't" }
32
33
  render status: :unauthorized, json: { status: :error, code: :authentication_failed }
33
34
  end
34
35
 
@@ -0,0 +1,83 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe ::SSO::Benchmarking do
4
+
5
+ let(:instance) { MyTestNamespace::MyClass.new }
6
+
7
+ before do
8
+ stub_const 'MyTestNamespace', Module.new
9
+ stub_const 'MyTestNamespace::MyClass', Class.new { include SSO::Benchmarking }
10
+ end
11
+
12
+ describe '#benchmark' do
13
+ context 'without block' do
14
+ it 'is nil' do
15
+ expect(instance.benchmark).to be_nil
16
+ end
17
+ end
18
+
19
+ context 'block given' do
20
+ context 'without arguments' do
21
+ it 'returns what was passed in' do
22
+ duration = instance.benchmark { :something }
23
+ expect(duration).to eq :something
24
+ end
25
+
26
+ it 'logs' do
27
+ expect(instance).to receive(:debug) do |_, &block|
28
+ expect(block.call).to eq 'Benchmark took 0ms'
29
+ end
30
+ instance.benchmark {}
31
+ end
32
+
33
+ it 'does not meter' do
34
+ expect(::SSO.config).to_not receive(:metric)
35
+ instance.benchmark {}
36
+ end
37
+ end
38
+
39
+ context 'only with name' do
40
+ it 'logs with the name' do
41
+ expect(instance).to receive(:debug) do |_, &block|
42
+ expect(block.call).to eq 'Long calculation took 0ms'
43
+ end
44
+ instance.benchmark(name: 'Long calculation') {}
45
+ end
46
+
47
+ it 'does not meter' do
48
+ expect(instance).to_not receive(:histogram)
49
+ instance.benchmark(name: 'Long calculation') {}
50
+ end
51
+ end
52
+
53
+ context 'only with metric' do
54
+ it 'logs with the metric' do
55
+ expect(instance).to receive(:debug) do |_, &block|
56
+ expect(block.call).to eq 'blob.serialization took 0ms'
57
+ end
58
+ instance.benchmark(metric: 'blob.serialization') {}
59
+ end
60
+
61
+ it 'meters as histogram with the metric as name' do
62
+ expect(instance).to receive(:histogram).with key: 'blob.serialization', value: 0
63
+ instance.benchmark(metric: 'blob.serialization') {}
64
+ end
65
+ end
66
+
67
+ context 'with name and metric' do
68
+ it 'logs with the name' do
69
+ expect(instance).to receive(:debug) do |_, &block|
70
+ expect(block.call).to eq 'Synchronous encryption took 0ms'
71
+ end
72
+ instance.benchmark(name: 'Synchronous encryption', metric: 'encryption.aes') {}
73
+ end
74
+
75
+ it 'meters as histogram with the metric as name' do
76
+ expect(instance).to receive(:histogram).with key: 'encryption.aes', value: 0
77
+ instance.benchmark(name: 'Synchronous encryption', metric: 'encryption.aes') {}
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- RSpec.describe SSO::Client::Warden::Hooks::AfterFetch, type: :request, db: true do
3
+ RSpec.describe SSO::Client::Warden::Hooks::AfterFetch, type: :request, db: true, stub_benchmarks: true do
4
4
 
5
5
  # Client side
6
6
  let(:warden_env) { {} }
@@ -10,6 +10,7 @@ RSpec.describe SSO::Client::Warden::Hooks::AfterFetch, type: :request, db: true
10
10
  let(:hook) { described_class.new passport: client_passport, warden: warden, options: {} }
11
11
  let(:client_user) { double :client_user, name: 'Good old client user' }
12
12
  let(:client_passport) { ::SSO::Client::Passport.new id: passport_id, secret: passport_secret, state: passport_state, user: client_user }
13
+ let(:operation) { hook.call }
13
14
 
14
15
  # Shared
15
16
  let!(:oauth_app) { create :outsider_doorkeeper_application }
@@ -34,6 +35,10 @@ RSpec.describe SSO::Client::Warden::Hooks::AfterFetch, type: :request, db: true
34
35
  context 'invalid passport' do
35
36
  let(:passport_secret) { SecureRandom.uuid }
36
37
 
38
+ before do
39
+ expect(warden).to receive :logout
40
+ end
41
+
37
42
  it 'does not verify the passport' do
38
43
  expect(client_passport).to_not be_verified
39
44
  hook.call
@@ -45,6 +50,20 @@ RSpec.describe SSO::Client::Warden::Hooks::AfterFetch, type: :request, db: true
45
50
  hook.call
46
51
  expect(client_passport).to_not be_modified
47
52
  end
53
+
54
+ it 'fails' do
55
+ expect(operation).to be_failure
56
+ end
57
+
58
+ it 'has a useful error code' do
59
+ expect(operation.code).to eq :invalid
60
+ end
61
+
62
+ it 'meters the invalid passport' do
63
+ expect(::SSO.config.metric).to receive(:call).with type: :histogram, key: 'sso.client.passport.verification.duration', value: 42_000, tags: nil, data: { caller: 'SSO::Client::PassportVerifier' }
64
+ expect(::SSO.config.metric).to receive(:call).with type: :increment, key: 'sso.client.warden.hooks.after_fetch.invalid', value: 1, tags: { scope: nil }, data: { passport_id: client_passport.id, caller: 'SSO::Client::Warden::Hooks::AfterFetch' }
65
+ hook.call
66
+ end
48
67
  end
49
68
 
50
69
  context 'user does not change' do
@@ -64,6 +83,20 @@ RSpec.describe SSO::Client::Warden::Hooks::AfterFetch, type: :request, db: true
64
83
  hook.call
65
84
  expect(client_passport.user.name).to eq 'Good old client user'
66
85
  end
86
+
87
+ it 'succeeds' do
88
+ expect(operation).to be_success
89
+ end
90
+
91
+ it 'has a useful error code' do
92
+ expect(operation.code).to eq :valid
93
+ end
94
+
95
+ it 'meters the invalid passport' do
96
+ expect(::SSO.config.metric).to receive(:call).with type: :histogram, key: 'sso.client.passport.verification.duration', value: 42_000, tags: nil, data: { caller: 'SSO::Client::PassportVerifier' }
97
+ expect(::SSO.config.metric).to receive(:call).with type: :increment, key: 'sso.client.warden.hooks.after_fetch.valid', value: 1, tags: { scope: nil }, data: { passport_id: client_passport.id, caller: 'SSO::Client::Warden::Hooks::AfterFetch' }
98
+ hook.call
99
+ end
67
100
  end
68
101
 
69
102
  context 'user attribute changed which is not included in the state digest' do
@@ -88,6 +121,20 @@ RSpec.describe SSO::Client::Warden::Hooks::AfterFetch, type: :request, db: true
88
121
  hook.call
89
122
  expect(client_passport.user.name).to eq 'Good old client user'
90
123
  end
124
+
125
+ it 'succeeds' do
126
+ expect(operation).to be_success
127
+ end
128
+
129
+ it 'has a useful error code' do
130
+ expect(operation.code).to eq :valid
131
+ end
132
+
133
+ it 'meters the invalid passport' do
134
+ expect(::SSO.config.metric).to receive(:call).with type: :histogram, key: 'sso.client.passport.verification.duration', value: 42_000, tags: nil, data: { caller: 'SSO::Client::PassportVerifier' }
135
+ expect(::SSO.config.metric).to receive(:call).with type: :increment, key: 'sso.client.warden.hooks.after_fetch.valid', value: 1, tags: { scope: nil }, data: { passport_id: client_passport.id, caller: 'SSO::Client::Warden::Hooks::AfterFetch' }
136
+ hook.call
137
+ end
91
138
  end
92
139
 
93
140
  context 'user attribute changed which results in a new state digest' do
@@ -112,6 +159,148 @@ RSpec.describe SSO::Client::Warden::Hooks::AfterFetch, type: :request, db: true
112
159
  hook.call
113
160
  expect(client_passport.user['name']).to eq server_user.name
114
161
  end
162
+
163
+ it 'succeeds' do
164
+ expect(operation).to be_success
165
+ end
166
+
167
+ it 'has a useful error code' do
168
+ expect(operation.code).to eq :valid_and_modified
169
+ end
170
+
171
+ it 'meters the invalid passport' do
172
+ expect(::SSO.config.metric).to receive(:call).with type: :histogram, key: 'sso.client.passport.verification.duration', value: 42_000, tags: nil, data: { caller: 'SSO::Client::PassportVerifier' }
173
+ expect(::SSO.config.metric).to receive(:call).with type: :increment, key: 'sso.client.warden.hooks.after_fetch.valid_and_modified', value: 1, tags: { scope: nil }, data: { passport_id: client_passport.id, caller: 'SSO::Client::Warden::Hooks::AfterFetch' }
174
+ hook.call
175
+ end
176
+ end
177
+
178
+ context 'server request times out' do
179
+ before do
180
+ expect(::HTTParty).to receive(:get).and_raise ::Net::ReadTimeout
181
+ end
182
+
183
+ it 'fails' do
184
+ expect(operation).to be_failure
185
+ end
186
+
187
+ it 'has a useful error code' do
188
+ expect(operation.code).to eq :server_request_timed_out
189
+ end
190
+
191
+ it 'meters the timeout' do
192
+ expect(::SSO.config.metric).to receive(:call).with type: :increment, key: 'sso.client.warden.hooks.after_fetch.timeout', value: 1, tags: { scope: nil }, data: { timeout_ms: '100ms', passport_id: client_passport.id, caller: 'SSO::Client::Warden::Hooks::AfterFetch' }
193
+ hook.call
194
+ end
195
+ end
196
+
197
+ context 'server unreachable' do
198
+ before do
199
+ expect(::HTTParty).to receive(:get).and_return double(:response, code: 302)
200
+ end
201
+
202
+ it 'fails' do
203
+ expect(operation).to be_failure
204
+ end
205
+
206
+ it 'has a useful error code' do
207
+ expect(operation.code).to eq :server_unreachable
208
+ end
209
+
210
+ it 'meters the timeout' do
211
+ expect(::SSO.config.metric).to receive(:call).with type: :histogram, key: 'sso.client.passport.verification.duration', value: 42_000, tags: nil, data: { caller: 'SSO::Client::PassportVerifier' }
212
+ expect(::SSO.config.metric).to receive(:call).with type: :increment, key: 'sso.client.warden.hooks.after_fetch.server_unreachable', value: 1, tags: { scope: nil }, data: { passport_id: client_passport.id, caller: 'SSO::Client::Warden::Hooks::AfterFetch' }
213
+ hook.call
214
+ end
215
+ end
216
+
217
+ context 'server response not parseable' do
218
+ let(:response) { double :response, code: 200 }
219
+
220
+ before do
221
+ expect(::HTTParty).to receive(:get).and_return response
222
+ allow(response).to receive(:parsed_response).and_raise ::JSON::ParserError
223
+ end
224
+
225
+ it 'fails' do
226
+ expect(operation).to be_failure
227
+ end
228
+
229
+ it 'has a useful error code' do
230
+ expect(operation.code).to eq :server_response_not_parseable
231
+ end
232
+
233
+ it 'meters the timeout' do
234
+ expect(::SSO.config.metric).to receive(:call).with type: :histogram, key: 'sso.client.passport.verification.duration', value: 42_000, tags: nil, data: { caller: 'SSO::Client::PassportVerifier' }
235
+ expect(::SSO.config.metric).to receive(:call).with type: :increment, key: 'sso.client.warden.hooks.after_fetch.server_response_not_parseable', value: 1, tags: { scope: nil }, data: { passport_id: client_passport.id, caller: 'SSO::Client::Warden::Hooks::AfterFetch' }
236
+ hook.call
237
+ end
238
+ end
239
+
240
+ context 'server response has no success flag at all' do
241
+ let(:response) { double :response, code: 200, parsed_response: { some: :thing } }
242
+
243
+ before do
244
+ expect(::HTTParty).to receive(:get).and_return response
245
+ end
246
+
247
+ it 'fails' do
248
+ expect(operation).to be_failure
249
+ end
250
+
251
+ it 'has a useful error code' do
252
+ expect(operation.code).to eq :server_response_missing_success_flag
253
+ end
254
+
255
+ it 'meters the timeout' do
256
+ expect(::SSO.config.metric).to receive(:call).with type: :histogram, key: 'sso.client.passport.verification.duration', value: 42_000, tags: nil, data: { caller: 'SSO::Client::PassportVerifier' }
257
+ expect(::SSO.config.metric).to receive(:call).with type: :increment, key: 'sso.client.warden.hooks.after_fetch.server_response_missing_success_flag', value: 1, tags: { scope: nil }, data: { passport_id: client_passport.id, caller: 'SSO::Client::Warden::Hooks::AfterFetch' }
258
+ hook.call
259
+ end
260
+ end
261
+
262
+ context 'server behaves weirdly' do
263
+ let(:response) { double :response, code: 200, parsed_response: { success: true } }
264
+
265
+ before do
266
+ expect(::HTTParty).to receive(:get).and_return response
267
+ end
268
+
269
+ it 'fails' do
270
+ expect(operation).to be_failure
271
+ end
272
+
273
+ it 'has a useful error code' do
274
+ expect(operation.code).to eq :unexpected_server_response_status
275
+ end
276
+
277
+ it 'meters the timeout' do
278
+ expect(::SSO.config.metric).to receive(:call).with type: :histogram, key: 'sso.client.passport.verification.duration', value: 42_000, tags: nil, data: { caller: 'SSO::Client::PassportVerifier' }
279
+ expect(::SSO.config.metric).to receive(:call).with type: :increment, key: 'sso.client.warden.hooks.after_fetch.unexpected_server_response_status', value: 1, tags: { scope: nil }, data: { passport_id: client_passport.id, caller: 'SSO::Client::Warden::Hooks::AfterFetch' }
280
+ hook.call
281
+ end
282
+ end
283
+
284
+ context 'client-side exception' do
285
+ before do
286
+ expect(::HTTParty).to receive(:get).and_raise ArgumentError
287
+ end
288
+
289
+ it 'fails' do
290
+ expect(operation).to be_failure
291
+ end
292
+
293
+ it 'has a useful error code' do
294
+ expect(operation.code).to eq :client_exception_caught
295
+ end
296
+ end
297
+
298
+ describe '.activate' do
299
+
300
+ it 'proxies the options to warden' do
301
+ expect(::Warden::Manager).to receive(:after_fetch).with(scope: :insider).and_yield :passport, :warden, :options
302
+ described_class.activate scope: :insider
303
+ end
115
304
  end
116
305
 
117
306
  end
@@ -52,7 +52,7 @@ RSpec.describe SSO::Client::Warden::Strategies::Passport do
52
52
  expect(rack_array.size).to eq 3
53
53
  expect(rack_array[0]).to eq 200
54
54
  expect(rack_array[1]).to eq 'Content-Type' => 'application/json'
55
- expect(rack_array[2]).to eq ['{"success":true,"code":"passport_verification_failed"}']
55
+ expect(rack_array[2]).to eq ['{"success":false,"code":"passport_verification_failed"}']
56
56
  end
57
57
  strategy.authenticate!
58
58
  end
data/spec/spec_helper.rb CHANGED
@@ -34,6 +34,11 @@ RSpec.configure do |config|
34
34
  config.before :suite do
35
35
  DatabaseCleaner.strategy = :transaction
36
36
  DatabaseCleaner.clean_with :truncation
37
+ SSO.config.exception_handler = nil
38
+ SSO.config.passport_chip_key = nil
39
+ SSO.config.oauth_client_id = nil
40
+ SSO.config.oauth_client_secret = nil
41
+ SSO.config.metric = ::SSO::Test::Helpers.meter
37
42
  end
38
43
 
39
44
  config.before :each do
@@ -48,6 +53,10 @@ RSpec.configure do |config|
48
53
  SSO.config.exception_handler = proc { |exception| fail exception }
49
54
  end
50
55
 
56
+ config.before :each, stub_benchmarks: true do
57
+ stub_benchmarks
58
+ end
59
+
51
60
  config.after :each do
52
61
  Timecop.return
53
62
  SSO.config.exception_handler = nil
@@ -4,6 +4,17 @@ module SSO
4
4
  module Test
5
5
  module Helpers
6
6
 
7
+ def self.meter
8
+ proc {}
9
+ end
10
+
11
+ def stub_benchmarks
12
+ allow(Benchmark).to receive(:realtime) do |&block|
13
+ block.call
14
+ 42
15
+ end
16
+ end
17
+
7
18
  # Inspired by Warden::Spec::Helpers
8
19
  def env_with_params(path = '/', params = {}, env = {})
9
20
  method = params.delete(:method) || 'GET'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sso
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - halo
@@ -265,12 +265,13 @@ files:
265
265
  - lib/sso/client/passport_verifier.rb
266
266
  - lib/sso/client/warden/hooks/after_fetch.rb
267
267
  - lib/sso/client/warden/strategies/passport.rb
268
+ - lib/sso/configuration.rb
269
+ - lib/sso/configure.rb
268
270
  - lib/sso/logging.rb
271
+ - lib/sso/meter.rb
269
272
  - lib/sso/server.rb
270
273
  - lib/sso/server/README.md
271
274
  - lib/sso/server/authentications/passport.rb
272
- - lib/sso/server/configuration.rb
273
- - lib/sso/server/configure.rb
274
275
  - lib/sso/server/doorkeeper/access_token_marker.rb
275
276
  - lib/sso/server/doorkeeper/grant_marker.rb
276
277
  - lib/sso/server/doorkeeper/resource_owner_authenticator.rb
@@ -324,6 +325,7 @@ files:
324
325
  - spec/dummy/db/schema.rb
325
326
  - spec/integration/oauth/authorization_code_spec.rb
326
327
  - spec/integration/oauth/password_spec.rb
328
+ - spec/lib/sso/benchmarking_spec.rb
327
329
  - spec/lib/sso/client/authentications/passport_spec.rb
328
330
  - spec/lib/sso/client/warden/hooks/after_fetch_spec.rb
329
331
  - spec/lib/sso/client/warden/strategies/passport_spec.rb
@@ -403,6 +405,7 @@ test_files:
403
405
  - spec/dummy/Rakefile
404
406
  - spec/integration/oauth/authorization_code_spec.rb
405
407
  - spec/integration/oauth/password_spec.rb
408
+ - spec/lib/sso/benchmarking_spec.rb
406
409
  - spec/lib/sso/client/authentications/passport_spec.rb
407
410
  - spec/lib/sso/client/warden/hooks/after_fetch_spec.rb
408
411
  - spec/lib/sso/client/warden/strategies/passport_spec.rb