volcano-sdk 0.9.0 → 0.9.1

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
  SHA256:
3
- metadata.gz: bf20d0cdb7959be9f989d8b1f1e5f88b53a065a7b899b42f7c3a09af5d1327c6
4
- data.tar.gz: 3350d3a721b2d25b67aaf0a76a8cab45cb1ca26c69d386c600789bb60589cfca
3
+ metadata.gz: 9c502d5485b551f8308988375a3b89d581b46ef42af03143898afc583395cc0b
4
+ data.tar.gz: 336f60885935dd30f4ff6886505095f90bfb4d9c7c7b1f572e5511b53e824897
5
5
  SHA512:
6
- metadata.gz: 94a0671011cb9256c3bf768fe05fbea0e1d49fb8c3d29adb694cc5a01eb83e80fb0d978b4aa1c83919a3b38ca7cdd6069a7af5b41eaefa743622bdc745f68083
7
- data.tar.gz: 756140f1665c4f360839a37da9ad15de033cf45373bee40a57b7b109d8d0830094af36974923841d538722869bd9f7fe987c7e3f50a55d5b5f9e657d6106d84f
6
+ metadata.gz: 7849ada9eaa8cf569961c5a4b750c33610ff46ff0914321a753e09ff2b3b5f969b9879c11db7014199f2e31ba61052b45fa67ef61603b4fa7e427f9857628f84
7
+ data.tar.gz: 782fbaf15537f564c4349cea3f206c63857b95f8e1b919203784f0e4963a0e31a0ce9739ac10011336847e84e9137b69232379acd96d9ee9e1d8218b14e5c1a1
data/README.md CHANGED
@@ -440,11 +440,14 @@ client.auth.sign_out
440
440
  raise "still signed in" if client.auth.current_session
441
441
  ```
442
442
 
443
- `sign_out` uses the access-token session when available, even if a refresh token was supplied.
443
+ Sign-out uses the refresh token directly when the SDK received both credentials together from
444
+ sign-in or a validated refresh. Supplied credentials use the access-token session; on HTTP 401,
445
+ the SDK can refresh once and revoke that same session without adopting the renewed credentials.
444
446
  It revokes the captured session and clears the captured in-memory
445
447
  session. It succeeds without a request when no session exists. If revocation
446
- fails, the SDK still clears that session and raises the typed error. A concurrent
447
- refresh of the same session is cleared; a separate sign-in or adoption remains current.
448
+ fails, the SDK still clears that session and raises the typed error. Sign-out waits for an already-running refresh and uses its validated credentials.
449
+ Later refresh attempts raise `Volcano::Error::SessionChangedError` without a request.
450
+ Concurrent sign-out calls share one result. A separate sign-in or adoption remains current.
448
451
 
449
452
  ### Invoke a function
450
453
 
data/lib/volcano/auth.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'monitor'
4
-
5
3
  module Volcano
6
4
  # Authenticates users and updates the client session.
7
5
  class Auth
@@ -12,7 +10,6 @@ module Volcano
12
10
  @client = client
13
11
  @transport = transport
14
12
  @api_url = api_url
15
- @refresh_lock = Monitor.new
16
13
  end
17
14
 
18
15
  def current_session
@@ -56,6 +53,15 @@ module Volcano
56
53
  end
57
54
  end
58
55
 
56
+ def refresh_response(refresh_token)
57
+ Transport.invoke do
58
+ @transport.auth_refresh(
59
+ authorization: @client.anon_token,
60
+ refresh_token: refresh_token
61
+ )
62
+ end
63
+ end
64
+
59
65
  def build_session(payload)
60
66
  owned_session(
61
67
  access_token: payload.fetch('access_token'),
@@ -65,6 +71,12 @@ module Volcano
65
71
  )
66
72
  end
67
73
 
74
+ def parse_refresh_session(payload)
75
+ owned_complete_session(build_session(payload))
76
+ rescue KeyError, TypeError, NoMethodError, ArgumentError => e
77
+ raise Error::TransportError, INCOMPLETE_SESSION, cause: e
78
+ end
79
+
68
80
  def complete_session?(session)
69
81
  return false unless session.is_a?(Session)
70
82
 
@@ -52,10 +52,12 @@ module Volcano
52
52
 
53
53
  def refresh_with_notifications(binding)
54
54
  notifications = []
55
- @refresh_lock.synchronize do
56
- active_generation, = owned_session_binding(binding)
57
- perform_refresh(binding, notifications) if active_generation == binding.first
58
- end
55
+ active_generation, = owned_session_binding(binding)
56
+ binding[1].refresh { perform_refresh(binding, notifications) } if active_generation == binding.first
57
+ raise Error::SessionChangedError if binding[1].closing?
58
+ rescue Error::VolcanoError
59
+ validate_read_failure(binding)
60
+ raise
59
61
  ensure
60
62
  # Only the dispatcher owner drains, after refresh coordination is released.
61
63
  notifications.each(&:call)
@@ -74,50 +76,48 @@ module Volcano
74
76
  def rejected_refresh?(binding, active)
75
77
  generation, lineage, = binding
76
78
  @rejected_refresh == [generation, lineage] &&
77
- active == [generation + 1, lineage + 1, nil]
79
+ active.first == generation + 1 && active.last.nil?
78
80
  end
79
81
 
80
82
  def perform_refresh(binding, notifications)
83
+ active = owned_session_binding(binding)
84
+ return active.last if active.first != binding.first
81
85
  raise Error::AuthenticationError, 'No refresh token' unless binding.last.refresh_token
82
86
 
83
- SessionCredentials.validate_refresh_source(binding.last)
84
87
  session = refreshed_session(binding, notifications)
85
- stored = @client.store_session_if_current?(
86
- session, binding.first, event: :token_refreshed, notifications: notifications
87
- )
88
- raise Error::SessionChangedError unless stored
88
+ unless binding[1].closing?
89
+ @client.store_session_if_current?(
90
+ session, binding.first, event: :token_refreshed, notifications: notifications
91
+ )
92
+ end
93
+ session
89
94
  end
90
95
 
91
96
  def refreshed_session(binding, notifications)
92
- parse_refresh_session(refresh_payload(binding, notifications))
97
+ _, owner, current = binding
98
+ verified = owner.verified_pair?(current)
99
+ SessionCredentials.validate_refresh_source(current, verified: verified)
100
+ owner.verify_pair(nil)
101
+ session = parse_refresh_session(refresh_payload(binding, notifications))
102
+ verify_refreshed_session(owner, current, session)
103
+ session
104
+ rescue Error::RateLimitedError
105
+ owner.verify_pair(current) if verified
106
+ raise
93
107
  end
94
108
 
95
- def parse_refresh_session(payload)
96
- owned_complete_session(build_session(payload))
97
- rescue KeyError, TypeError, NoMethodError, ArgumentError => e
98
- raise Error::TransportError, INCOMPLETE_SESSION, cause: e
99
- end
100
-
101
- def refresh_response(refresh_token)
102
- Transport.invoke do
103
- @transport.auth_refresh(
104
- authorization: @client.anon_token,
105
- refresh_token: refresh_token
106
- )
107
- end
109
+ def verify_refreshed_session(owner, current, session)
110
+ SessionCredentials.validate_refresh(current, session)
111
+ owner.verify_pair(session)
108
112
  end
109
113
 
110
114
  def refresh_payload(binding, notifications)
111
115
  Transport.body(refresh_response(binding.last.refresh_token), 200)
112
116
  rescue Error::AuthenticationError
113
- generation, lineage, = binding
114
- cleared = @client.clear_session_if_current?(generation, notifications: notifications)
115
- raise Error::SessionChangedError unless cleared
116
-
117
- @rejected_refresh = [generation, lineage]
118
- raise
119
- rescue Error::VolcanoError
120
- owned_session_binding(binding)
117
+ generation, owner, = binding
118
+ if !owner.closing? && @client.clear_session_if_current?(generation, notifications: notifications)
119
+ @rejected_refresh = [generation, owner]
120
+ end
121
121
  raise
122
122
  end
123
123
  end
@@ -5,29 +5,32 @@ module Volcano
5
5
  class Auth
6
6
  def sign_out
7
7
  binding = @client.capture_session_binding
8
- return unless binding.last
8
+ return binding[1].wait_for_sign_out unless binding.last
9
9
 
10
10
  notifications = []
11
- @refresh_lock.synchronize { sign_out_captured(binding, notifications) }
11
+ binding[1].sign_out do |preceding, pending|
12
+ sign_out_captured(binding, preceding, pending, notifications)
13
+ end
12
14
  ensure
13
15
  notifications&.each(&:call)
14
16
  end
15
17
 
16
18
  private
17
19
 
18
- def sign_out_captured(binding, notifications)
19
- generation, lineage, current = revocation_binding(binding)
20
- error = revocation_error(current)
21
- lineage = nil unless access_token_session_id(current.access_token)
22
- cleared = @client.clear_session_if_current?(generation, lineage: lineage, notifications: notifications)
20
+ def sign_out_captured(binding, preceding, pending, notifications)
21
+ generation, owner, = binding
22
+ current, refresh_error = revocation_credentials(binding, preceding)
23
+ error = revocation_error(current, owner, pending ? refresh_error : nil, joined: pending)
24
+ cleared = @client.clear_session_if_current?(generation, lineage: owner, notifications: notifications)
23
25
  raise Error::SessionChangedError, cause: error unless cleared
24
26
 
25
27
  raise error if error
26
28
  end
27
29
 
28
- def revocation_binding(binding)
29
- active = @client.capture_session_binding
30
- active.last && active[1] == binding[1] ? active : binding
30
+ def revocation_credentials(binding, preceding)
31
+ [binding[1].result(preceding) || binding.last, nil]
32
+ rescue Error::VolcanoError => e
33
+ [binding.last, e]
31
34
  end
32
35
 
33
36
  def logout_response(refresh_token)
@@ -39,9 +42,11 @@ module Volcano
39
42
  end
40
43
  end
41
44
 
42
- def revocation_error(session)
45
+ def revocation_error(session, owner, refresh_error, joined:)
43
46
  session_id = access_token_session_id(session.access_token)
44
- return revoke_access_session(session, session_id) if session_id
47
+ verified = owner.verified_pair?(session)
48
+ return revoke_access_session(session, session_id, refresh_error, joined: joined) if session_id && !verified
49
+ return refresh_error if refresh_error && !verified
45
50
  return unless session.refresh_token
46
51
 
47
52
  Transport.body(logout_response(session.refresh_token), 204)
@@ -50,9 +55,10 @@ module Volcano
50
55
  e
51
56
  end
52
57
 
53
- def revoke_access_session(session, session_id)
58
+ def revoke_access_session(session, session_id, refresh_error, joined:)
54
59
  error = delete_session_error(session.access_token, session_id)
55
60
  return error unless error&.status == 401 && session.refresh_token
61
+ return refresh_error || error if joined
56
62
 
57
63
  refreshed = parse_refresh_session(Transport.body(refresh_response(session.refresh_token), 200))
58
64
  SessionCredentials.validate_refresh(session, refreshed)
@@ -27,7 +27,7 @@ module Volcano
27
27
  def initialize(session: nil)
28
28
  @mutex = Mutex.new
29
29
  @generation = 0
30
- @lineage = 0
30
+ @lineage = SessionOperations.new
31
31
  @session = session
32
32
  @callbacks = {}
33
33
  @next_callback_id = 0
@@ -51,8 +51,9 @@ module Volcano
51
51
  def store_if_current?(session, generation, event: :signed_in, notifications: nil, lineage: nil)
52
52
  dispatch = @mutex.synchronize do
53
53
  return false unless lineage.nil? ? generation == @generation : lineage == @lineage
54
+ return true if session.nil? && @session.nil?
54
55
 
55
- enqueue_notification(replace(session, event), event, session)
56
+ enqueue_notification(replace(session, event, verified_pair: session), event, session)
56
57
  end
57
58
  return true unless dispatch
58
59
 
@@ -80,19 +81,19 @@ module Volcano
80
81
 
81
82
  private
82
83
 
83
- def replace(session, event)
84
+ def replace(session, event, verified_pair: nil)
84
85
  SessionCredentials.validate_refresh(@session, session) if event == :token_refreshed
85
86
  @session = session
86
87
  @generation += 1
87
- @lineage += 1 unless event == :token_refreshed
88
+ @lineage.clear_local_credentials unless session
89
+ @lineage = SessionOperations.new(verified_pair) if session && event != :token_refreshed
88
90
  @callbacks.keys
89
91
  end
90
92
 
91
93
  def register(callback)
92
- callback_id = @next_callback_id
93
94
  @next_callback_id += 1
94
- @callbacks[callback_id] = callback
95
- [callback_id, @session]
95
+ @callbacks[@next_callback_id] = callback
96
+ [@next_callback_id, @session]
96
97
  end
97
98
 
98
99
  def unsubscribe(callback_id)
@@ -103,8 +103,8 @@ module Volcano
103
103
  def postgres_stop?(request) = request.equal?(STOP)
104
104
 
105
105
  def current_postgres_request?(request)
106
- _, lineage, = @realtime.__send__(:capture_session_binding)
107
- @subscribed && request.subscription_epoch == @postgres_epoch &&
106
+ _, lineage, session = @realtime.__send__(:capture_session_binding)
107
+ @subscribed && session && request.subscription_epoch == @postgres_epoch &&
108
108
  request.session_lineage == lineage
109
109
  end
110
110
  end
@@ -28,16 +28,15 @@ module Volcano
28
28
  end
29
29
  private_class_method :credential
30
30
 
31
- def self.validate_refresh_source(current)
32
- return if current.user_id || session_id(current.access_token)
31
+ def self.validate_refresh_source(current, verified: false)
32
+ return if verified || session_id(current.access_token)
33
33
 
34
- raise Error::AuthenticationError, 'Cannot refresh unknown identity without a session identifier'
34
+ raise Error::AuthenticationError, 'Cannot refresh supplied credentials without a session identifier'
35
35
  end
36
36
 
37
37
  def self.validate_refresh(current, refreshed)
38
38
  return unless current
39
39
 
40
- validate_refresh_source(current)
41
40
  expected = session_id(current.access_token)
42
41
  if expected && expected != session_id(refreshed&.access_token)
43
42
  raise Error::AuthenticationError, 'Refreshed credentials belong to a different server session'
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
5
+ module Volcano
6
+ # Keeps refresh and revocation attached to one explicit sign-in/adoption.
7
+ class SessionOperations
8
+ Outcome = Struct.new(:done, :value, :error)
9
+ private_constant :Outcome
10
+
11
+ def initialize(verified = nil)
12
+ @mutex = Mutex.new
13
+ @condition = ConditionVariable.new
14
+ @refresh = nil
15
+ @sign_out = nil
16
+ @locally_cleared = false
17
+ verify_pair(verified)
18
+ end
19
+
20
+ def verify_pair(session)
21
+ @mutex.synchronize do
22
+ @verified_pair = session && [session.access_token, session.refresh_token].freeze unless @locally_cleared
23
+ end
24
+ end
25
+
26
+ def clear_local_credentials
27
+ @mutex.synchronize do
28
+ return if @sign_out
29
+
30
+ @locally_cleared = true
31
+ @verified_pair = nil
32
+ @refresh = nil if @refresh&.done
33
+ end
34
+ end
35
+
36
+ def verified_pair?(session)
37
+ @mutex.synchronize { session.refresh_token && @verified_pair == [session.access_token, session.refresh_token] }
38
+ end
39
+
40
+ def closing? = @mutex.synchronize { !@sign_out.nil? }
41
+
42
+ def refresh(&)
43
+ operation, owner = @mutex.synchronize do
44
+ raise Error::SessionChangedError if @sign_out || @locally_cleared
45
+ next [@refresh, false] if @refresh && !@refresh.done
46
+
47
+ @refresh = Outcome.new(false)
48
+ [@refresh, true]
49
+ end
50
+ execute(operation, owner, &)
51
+ end
52
+
53
+ def sign_out
54
+ operation, owner, preceding, pending = @mutex.synchronize do
55
+ first = @sign_out.nil?
56
+ @sign_out ||= Outcome.new(false)
57
+ [@sign_out, first, @refresh, @refresh && !@refresh.done]
58
+ end
59
+ execute(operation, owner) do
60
+ yield(preceding, pending)
61
+ ensure
62
+ @mutex.synchronize { @verified_pair = @refresh = nil }
63
+ end
64
+ end
65
+
66
+ def wait_for_sign_out
67
+ operation = @mutex.synchronize { @sign_out unless @sign_out&.done }
68
+ result(operation)
69
+ end
70
+
71
+ def result(operation)
72
+ return unless operation
73
+
74
+ @mutex.synchronize { @condition.wait(@mutex) until operation.done }
75
+ raise operation.error if operation.error
76
+
77
+ operation.value
78
+ end
79
+
80
+ private
81
+
82
+ def execute(operation, owner)
83
+ return result(operation) unless owner
84
+
85
+ completed = false
86
+ begin
87
+ value = yield
88
+ completed = true
89
+ ensure
90
+ complete(operation, value, completed ? nil : $ERROR_INFO)
91
+ end
92
+ value
93
+ end
94
+
95
+ def complete(operation, value, error)
96
+ @mutex.synchronize do
97
+ operation.value = value
98
+ operation.error = error
99
+ operation.done = true
100
+ @refresh = nil if @locally_cleared && @refresh == operation
101
+ @condition.broadcast
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Volcano
4
- VERSION = '0.9.0'
4
+ VERSION = '0.9.1'
5
5
  end
data/lib/volcano.rb CHANGED
@@ -7,6 +7,7 @@ require_relative 'volcano/session'
7
7
  require_relative 'volcano/function_response'
8
8
  require_relative 'volcano/log_responses'
9
9
  require_relative 'volcano/storage_models'
10
+ require_relative 'volcano/session_operations'
10
11
  require_relative 'volcano/auth_state'
11
12
  require_relative 'volcano/errors'
12
13
  require_relative 'volcano/redaction'
@@ -53,7 +54,7 @@ require_relative 'volcano/client'
53
54
  # Public namespace for the Volcano Ruby SDK.
54
55
  module Volcano
55
56
  private_constant :GeneratedTransport, :LockAutoRenewal, :LockLeaseClock, :LockRenewer, :LockSession,
56
- :Redaction
57
+ :Redaction, :SessionOperations
57
58
 
58
59
  Realtime.private_constant :Protocol, :ProtocolDispatch, :ProtocolPublicationDispatch, :ProtocolRecovery,
59
60
  :ProtocolRecoveryDispatch, :ProtocolRecoveryPosition
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: volcano-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kong
@@ -576,6 +576,7 @@ files:
576
576
  - lib/volcano/realtime/reconnect.rb
577
577
  - lib/volcano/redaction.rb
578
578
  - lib/volcano/session.rb
579
+ - lib/volcano/session_operations.rb
579
580
  - lib/volcano/sign_up_result.rb
580
581
  - lib/volcano/storage.rb
581
582
  - lib/volcano/storage_models.rb