volcano-sdk 0.29.0 → 0.31.0
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 +4 -4
- data/lib/volcano/auth.rb +10 -47
- data/lib/volcano/auth_email_change.rb +1 -1
- data/lib/volcano/auth_get_user.rb +36 -77
- data/lib/volcano/auth_notification_dispatch.rb +16 -0
- data/lib/volcano/auth_oauth.rb +4 -0
- data/lib/volcano/auth_oauth_api.rb +8 -3
- data/lib/volcano/auth_oauth_sign_in.rb +1 -1
- data/lib/volcano/auth_oauth_token.rb +2 -0
- data/lib/volcano/auth_profile_fields.rb +98 -0
- data/lib/volcano/auth_refresh.rb +29 -32
- data/lib/volcano/auth_session_binding.rb +25 -0
- data/lib/volcano/auth_session_construction.rb +67 -0
- data/lib/volcano/auth_sessions.rb +9 -45
- data/lib/volcano/auth_sign_out.rb +22 -14
- data/lib/volcano/auth_sign_up.rb +2 -0
- data/lib/volcano/auth_state.rb +3 -2
- data/lib/volcano/auth_state_notifications.rb +7 -4
- data/lib/volcano/auth_update_user.rb +1 -1
- data/lib/volcano/session.rb +0 -73
- data/lib/volcano/session_credentials.rb +82 -0
- data/lib/volcano/session_operations.rb +13 -6
- data/lib/volcano/session_page_mapping.rb +89 -0
- data/lib/volcano/storage.rb +18 -34
- data/lib/volcano/storage_models.rb +18 -5
- data/lib/volcano/storage_mutations.rb +8 -3
- data/lib/volcano/storage_object_response.rb +88 -0
- data/lib/volcano/storage_response.rb +92 -0
- data/lib/volcano/storage_resumable_upload.rb +13 -2
- data/lib/volcano/storage_session_adapter.rb +61 -0
- data/lib/volcano/storage_transport_adapter.rb +73 -0
- data/lib/volcano/storage_upload_models.rb +51 -27
- data/lib/volcano/storage_upload_sessions.rb +1 -35
- data/lib/volcano/version.rb +1 -1
- data/lib/volcano.rb +18 -7
- data/sig/auth_credentials.rbs +15 -0
- data/sig/auth_oauth.rbs +16 -0
- data/sig/auth_profile.rbs +10 -0
- data/sig/auth_sessions.rbs +7 -0
- data/sig/auth_subscription.rbs +6 -0
- data/sig/session.rbs +1 -1
- data/sig/storage.rbs +29 -0
- data/sig/storage_records.rbs +3 -1
- data/sig/upload_session_status.rbs +3 -1
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36cf6c142c98f421f086d919b4d5e83034e95831d2b4246a892be57ebfaea9d5
|
|
4
|
+
data.tar.gz: 877909af108367683c4cbb434235baa27669af9d303dd0b52307919446f8c348
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 832893a1b1ba6caa88f144b81d8b8e155d2041be82fccffe4328eeb1f59f4fc8e3ce84adbb476f3b1a6b26ed719da7293f9dfb6a563270cdcb5222125661f378
|
|
7
|
+
data.tar.gz: acfb489b98c3e8dcca7918ee1136d4e04f62fec2a7287fcc16fcd952edd3bfae2d4973b6307ef53d577a08cb4104511666917e4435c3d7722bb3cde544d7ad3d
|
data/lib/volcano/auth.rb
CHANGED
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
module Volcano
|
|
4
4
|
# Authenticates users and updates the client session.
|
|
5
5
|
class Auth
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
include AuthRefresh
|
|
7
|
+
include AuthSignOut
|
|
8
|
+
include AuthProfile
|
|
9
|
+
include AuthUpdateUser
|
|
10
|
+
include AuthEmailChange
|
|
11
|
+
include AuthSessions
|
|
12
|
+
include AuthSessionConstruction
|
|
8
13
|
|
|
9
14
|
def initialize(client, transport, api_url:)
|
|
10
15
|
@client = client
|
|
@@ -20,10 +25,10 @@ module Volcano
|
|
|
20
25
|
@client.store_session(owned_complete_session(session), event: nil)
|
|
21
26
|
end
|
|
22
27
|
|
|
23
|
-
def on_auth_state_change(&
|
|
24
|
-
raise ArgumentError, 'callback block required' unless
|
|
28
|
+
def on_auth_state_change(&)
|
|
29
|
+
raise ArgumentError, 'callback block required' unless block_given?
|
|
25
30
|
|
|
26
|
-
@client.subscribe_auth_state_change(&
|
|
31
|
+
@client.subscribe_auth_state_change(&)
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
def sign_in(email:, password:)
|
|
@@ -70,47 +75,5 @@ module Volcano
|
|
|
70
75
|
)
|
|
71
76
|
end
|
|
72
77
|
end
|
|
73
|
-
|
|
74
|
-
def build_session(payload)
|
|
75
|
-
owned_session(
|
|
76
|
-
access_token: payload.fetch('access_token'),
|
|
77
|
-
refresh_token: payload.fetch('refresh_token'),
|
|
78
|
-
user_id: payload.fetch('user').fetch('id'),
|
|
79
|
-
user: payload.fetch('user')
|
|
80
|
-
)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def parse_refresh_session(payload)
|
|
84
|
-
owned_complete_session(build_session(payload))
|
|
85
|
-
rescue KeyError, TypeError, NoMethodError, ArgumentError => e
|
|
86
|
-
raise Error::TransportError, INCOMPLETE_SESSION, cause: e
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def complete_session?(session)
|
|
90
|
-
return false unless session.is_a?(Session)
|
|
91
|
-
|
|
92
|
-
values = [session.access_token, session.refresh_token, session.user_id]
|
|
93
|
-
values.all? { |value| value.is_a?(String) && !value.strip.empty? } &&
|
|
94
|
-
matching_session_user?(session)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def matching_session_user?(session)
|
|
98
|
-
session.user.nil? || (session.user.is_a?(Hash) && session.user['id'] == session.user_id)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def owned_complete_session(session)
|
|
102
|
-
raise ArgumentError, INCOMPLETE_SESSION unless complete_session?(session)
|
|
103
|
-
|
|
104
|
-
owned_session(**session.to_h)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def owned_session(access_token:, refresh_token:, user_id:, user: nil)
|
|
108
|
-
Session.new(
|
|
109
|
-
access_token: access_token.dup.freeze,
|
|
110
|
-
refresh_token: refresh_token.dup.freeze,
|
|
111
|
-
user_id: user_id.dup.freeze,
|
|
112
|
-
user: user
|
|
113
|
-
)
|
|
114
|
-
end
|
|
115
78
|
end
|
|
116
79
|
end
|
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'time'
|
|
4
|
-
|
|
5
3
|
module Volcano
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
RFC3339_OFFSET = /(?:[Zz]|[+-]\d{2}:\d{2})\z/
|
|
10
|
-
USER_OPTIONAL_VALUES = %w[project_id email_confirmed user_metadata app_metadata avatar_url].freeze
|
|
11
|
-
USER_OPTIONAL_STRINGS = %w[project_id avatar_url].freeze
|
|
12
|
-
USER_STATUSES = %w[active banned deleted].freeze
|
|
13
|
-
USER_TIMESTAMPS = %w[banned_until last_sign_in_at created_at updated_at].freeze
|
|
14
|
-
private_constant :INVALID_USER, :RFC3339_OFFSET, :USER_OPTIONAL_STRINGS,
|
|
15
|
-
:USER_OPTIONAL_VALUES, :USER_STATUSES, :USER_TIMESTAMPS
|
|
4
|
+
# Current-user requests and session cache updates.
|
|
5
|
+
module AuthProfile
|
|
6
|
+
include AuthProfileFields
|
|
16
7
|
|
|
17
8
|
def user
|
|
18
|
-
profile_request {
|
|
9
|
+
profile_request { ->(token) { get_user_response(token) } }
|
|
19
10
|
end
|
|
20
11
|
|
|
21
12
|
alias get_user user
|
|
@@ -32,7 +23,7 @@ module Volcano
|
|
|
32
23
|
end
|
|
33
24
|
|
|
34
25
|
def cache_current_user(payload, generation)
|
|
35
|
-
profile =
|
|
26
|
+
profile = profile_user_payload(payload)
|
|
36
27
|
user = build_user(profile)
|
|
37
28
|
raise Error::SessionChangedError unless @client.update_session_user_if_current?(profile, generation)
|
|
38
29
|
|
|
@@ -46,79 +37,47 @@ module Volcano
|
|
|
46
37
|
end
|
|
47
38
|
|
|
48
39
|
def build_user(payload)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
User.new(**user_attributes(payload))
|
|
40
|
+
new_profile_user(user_identity(payload), user_optional_attributes(payload), user_timestamps(payload))
|
|
52
41
|
rescue ArgumentError => e
|
|
53
42
|
raise Error::AuthenticationError, INVALID_USER, cause: e
|
|
54
43
|
end
|
|
55
44
|
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
id:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def user_payload(payload)
|
|
67
|
-
raise Error::AuthenticationError, INVALID_USER unless payload.is_a?(Hash)
|
|
68
|
-
|
|
69
|
-
payload['user']
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def valid_user?(payload)
|
|
73
|
-
return false unless payload.is_a?(Hash)
|
|
74
|
-
|
|
75
|
-
required_user_fields?(payload) && valid_optional_user_fields?(payload)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def valid_optional_user_fields?(payload)
|
|
79
|
-
valid_email_confirmation?(payload) && valid_metadata?(payload) &&
|
|
80
|
-
valid_optional_strings?(payload) && valid_timestamps?(payload)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def required_user_fields?(payload)
|
|
84
|
-
complete_string?(payload['id']) && payload['email'].is_a?(String) &&
|
|
85
|
-
USER_STATUSES.include?(payload['status'])
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def valid_email_confirmation?(payload)
|
|
89
|
-
[true, false, nil].include?(payload['email_confirmed'])
|
|
45
|
+
def new_profile_user(identity, optional, timestamps)
|
|
46
|
+
User.new(
|
|
47
|
+
id: identity[:id], email: identity[:email], status: identity[:status],
|
|
48
|
+
project_id: optional[:project_id], email_confirmed: optional[:email_confirmed],
|
|
49
|
+
user_metadata: optional[:user_metadata], app_metadata: optional[:app_metadata],
|
|
50
|
+
avatar_url: optional[:avatar_url], banned_until: timestamps[:banned_until],
|
|
51
|
+
last_sign_in_at: timestamps[:last_sign_in_at], created_at: timestamps[:created_at],
|
|
52
|
+
updated_at: timestamps[:updated_at]
|
|
53
|
+
)
|
|
90
54
|
end
|
|
91
55
|
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def valid_optional_strings?(payload)
|
|
99
|
-
USER_OPTIONAL_STRINGS.all? do |name|
|
|
100
|
-
payload[name].nil? || payload[name].is_a?(String)
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def valid_timestamps?(payload)
|
|
105
|
-
USER_TIMESTAMPS.all? { |name| valid_timestamp?(payload[name]) }
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def valid_timestamp?(value)
|
|
109
|
-
return true if value.nil?
|
|
110
|
-
|
|
111
|
-
value.is_a?(String) && RFC3339_OFFSET.match?(value) && Time.iso8601(value)
|
|
112
|
-
rescue ArgumentError
|
|
113
|
-
false
|
|
56
|
+
def user_identity(payload)
|
|
57
|
+
{
|
|
58
|
+
id: profile_required_id(payload['id']),
|
|
59
|
+
email: profile_required_string(payload['email']),
|
|
60
|
+
status: profile_status(payload['status'])
|
|
61
|
+
}
|
|
114
62
|
end
|
|
115
63
|
|
|
116
|
-
def
|
|
117
|
-
|
|
64
|
+
def user_optional_attributes(payload)
|
|
65
|
+
{
|
|
66
|
+
project_id: profile_optional_string(payload['project_id']),
|
|
67
|
+
email_confirmed: profile_optional_boolean(payload['email_confirmed']),
|
|
68
|
+
user_metadata: profile_optional_object(payload['user_metadata']),
|
|
69
|
+
app_metadata: profile_optional_object(payload['app_metadata']),
|
|
70
|
+
avatar_url: profile_optional_string(payload['avatar_url'])
|
|
71
|
+
}
|
|
118
72
|
end
|
|
119
73
|
|
|
120
|
-
def
|
|
121
|
-
|
|
74
|
+
def user_timestamps(payload)
|
|
75
|
+
{
|
|
76
|
+
banned_until: profile_parse_time(payload['banned_until']),
|
|
77
|
+
last_sign_in_at: profile_parse_time(payload['last_sign_in_at']),
|
|
78
|
+
created_at: profile_parse_time(payload['created_at']),
|
|
79
|
+
updated_at: profile_parse_time(payload['updated_at'])
|
|
80
|
+
}
|
|
122
81
|
end
|
|
123
82
|
end
|
|
124
83
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
# Runs queued auth-state callbacks outside session coordination locks.
|
|
5
|
+
module AuthNotificationDispatch
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def run_auth_notifications(notifications)
|
|
9
|
+
notifications.each do |notification|
|
|
10
|
+
notification.call
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
private_constant :AuthNotificationDispatch
|
|
16
|
+
end
|
data/lib/volcano/auth_oauth.rb
CHANGED
|
@@ -88,5 +88,9 @@ module Volcano
|
|
|
88
88
|
def valid_linked_oauth_provider?(provider, linked_at, updated_at)
|
|
89
89
|
complete_string?(provider) && linked_at.is_a?(Time) && updated_at.is_a?(Time)
|
|
90
90
|
end
|
|
91
|
+
|
|
92
|
+
def complete_string?(value)
|
|
93
|
+
value.is_a?(String) && !value.strip.empty?
|
|
94
|
+
end
|
|
91
95
|
end
|
|
92
96
|
end
|
|
@@ -6,9 +6,12 @@ module Volcano
|
|
|
6
6
|
def call_oauth_api(provider, endpoint:, method: 'GET', body: nil)
|
|
7
7
|
session_payload(200, decode: :oauth_api_data) do
|
|
8
8
|
provider_name = oauth_provider_name(provider)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
owned_endpoint = endpoint.dup.freeze
|
|
10
|
+
owned_method = method.dup.freeze
|
|
11
|
+
owned_body = body.nil? ? nil : JSON.parse(JSON.generate(body), freeze: true)
|
|
12
|
+
lambda do |token|
|
|
13
|
+
oauth_api_response(token, provider_name, endpoint: owned_endpoint, method: owned_method, body: owned_body)
|
|
14
|
+
end
|
|
12
15
|
end
|
|
13
16
|
end
|
|
14
17
|
|
|
@@ -24,6 +27,8 @@ module Volcano
|
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
def oauth_api_data(response_body)
|
|
30
|
+
raise TypeError, 'Expected OAuth provider API response data' unless response_body.is_a?(Hash)
|
|
31
|
+
|
|
27
32
|
data = response_body.fetch('data')
|
|
28
33
|
freeze_oauth_api_data(data)
|
|
29
34
|
rescue KeyError, NoMethodError
|
|
@@ -20,7 +20,7 @@ module Volcano
|
|
|
20
20
|
def exchange_oauth_code(code:, redirect_to:, state:, expected_state:)
|
|
21
21
|
validate_oauth_callback_state(state, expected_state)
|
|
22
22
|
generation, = @client.capture_session
|
|
23
|
-
session = build_session(Transport.body(oauth_exchange_response(code, redirect_to), 200))
|
|
23
|
+
session = owned_complete_session(build_session(Transport.body(oauth_exchange_response(code, redirect_to), 200)))
|
|
24
24
|
raise Error::SessionChangedError unless @client.store_session_if_current?(session, generation)
|
|
25
25
|
|
|
26
26
|
session
|
|
@@ -32,6 +32,8 @@ module Volcano
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def oauth_provider_token_status(body)
|
|
35
|
+
raise TypeError, 'Expected complete OAuth provider token status' unless body.is_a?(Hash)
|
|
36
|
+
|
|
35
37
|
message, provider, expires_in = body.fetch_values('message', 'provider', 'expires_in')
|
|
36
38
|
OAuthProviderTokenStatus.new(message:, provider:, expires_in:)
|
|
37
39
|
rescue KeyError, NoMethodError
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module Volcano
|
|
6
|
+
# Validates and maps the current-user JSON response.
|
|
7
|
+
module AuthProfileFields
|
|
8
|
+
INVALID_USER = 'Expected a complete user profile'
|
|
9
|
+
RFC3339_OFFSET = /(?:[Zz]|[+-]\d{2}:\d{2})\z/
|
|
10
|
+
USER_STATUSES = %w[active banned deleted].freeze
|
|
11
|
+
private_constant :INVALID_USER, :RFC3339_OFFSET, :USER_STATUSES
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def profile_user_payload(payload)
|
|
16
|
+
raise Error::AuthenticationError, INVALID_USER unless payload.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
profile_json_object(payload['user'])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def profile_json_object(payload)
|
|
22
|
+
raise Error::AuthenticationError, INVALID_USER unless payload.is_a?(Hash)
|
|
23
|
+
|
|
24
|
+
payload.to_h do |key, value|
|
|
25
|
+
raise Error::AuthenticationError, INVALID_USER unless key.is_a?(String)
|
|
26
|
+
|
|
27
|
+
[key, profile_json_value(value)]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def profile_json_value(value)
|
|
32
|
+
case value
|
|
33
|
+
when Array then value.map { |item| profile_json_value(item) }
|
|
34
|
+
when Hash then profile_json_object(value)
|
|
35
|
+
else profile_json_scalar(value)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def profile_json_scalar(value)
|
|
40
|
+
case value
|
|
41
|
+
when nil, true, false, Integer, String then value
|
|
42
|
+
when Float then profile_finite_float(value)
|
|
43
|
+
else raise Error::AuthenticationError, INVALID_USER
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def profile_finite_float(value)
|
|
48
|
+
raise Error::AuthenticationError, INVALID_USER unless value.finite?
|
|
49
|
+
|
|
50
|
+
value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def profile_required_string(value)
|
|
54
|
+
raise Error::AuthenticationError, INVALID_USER unless value.is_a?(String)
|
|
55
|
+
|
|
56
|
+
value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def profile_required_id(value)
|
|
60
|
+
raise Error::AuthenticationError, INVALID_USER unless value.is_a?(String) && !value.strip.empty?
|
|
61
|
+
|
|
62
|
+
value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def profile_status(value)
|
|
66
|
+
raise Error::AuthenticationError, INVALID_USER unless value.is_a?(String) && USER_STATUSES.include?(value)
|
|
67
|
+
|
|
68
|
+
value
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def profile_optional_string(value)
|
|
72
|
+
return if value.nil?
|
|
73
|
+
|
|
74
|
+
profile_required_string(value)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def profile_optional_object(value)
|
|
78
|
+
profile_json_object(value) unless value.nil?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def profile_optional_boolean(value)
|
|
82
|
+
return if value.nil?
|
|
83
|
+
return true if value == true
|
|
84
|
+
return false if value == false
|
|
85
|
+
|
|
86
|
+
raise Error::AuthenticationError, INVALID_USER
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def profile_parse_time(value)
|
|
90
|
+
return if value.nil?
|
|
91
|
+
raise Error::AuthenticationError, INVALID_USER unless value.is_a?(String) && RFC3339_OFFSET.match?(value)
|
|
92
|
+
|
|
93
|
+
Time.iso8601(value).freeze
|
|
94
|
+
rescue ArgumentError
|
|
95
|
+
raise Error::AuthenticationError, INVALID_USER
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/volcano/auth_refresh.rb
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Volcano
|
|
4
4
|
# Refreshes only the session captured by the initiating operation.
|
|
5
|
-
|
|
5
|
+
module AuthRefresh
|
|
6
|
+
include AuthNotificationDispatch
|
|
7
|
+
include AuthSessionBinding
|
|
8
|
+
|
|
6
9
|
def refresh_session
|
|
7
10
|
binding = @client.capture_session_binding
|
|
8
|
-
|
|
11
|
+
session = binding.last
|
|
12
|
+
raise Error::AuthenticationError, 'No active session' unless session
|
|
9
13
|
|
|
10
|
-
refresh_captured_session(binding)
|
|
14
|
+
refresh_captured_session([binding.first, binding[1], session])
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
def session_request(binding: @client.capture_session_binding)
|
|
@@ -26,7 +30,7 @@ module Volcano
|
|
|
26
30
|
response
|
|
27
31
|
end
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
def session_read(...) = session_request(...)
|
|
30
34
|
|
|
31
35
|
private
|
|
32
36
|
|
|
@@ -51,6 +55,7 @@ module Volcano
|
|
|
51
55
|
end
|
|
52
56
|
|
|
53
57
|
def refresh_with_notifications(binding)
|
|
58
|
+
# @type var notifications: Array[Method]
|
|
54
59
|
notifications = []
|
|
55
60
|
active_generation, = owned_session_binding(binding)
|
|
56
61
|
binding[1].refresh { perform_refresh(binding, notifications) } if active_generation == binding.first
|
|
@@ -60,31 +65,17 @@ module Volcano
|
|
|
60
65
|
raise
|
|
61
66
|
ensure
|
|
62
67
|
# Only the dispatcher owner drains, after refresh coordination is released.
|
|
63
|
-
notifications
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def owned_session_binding(binding)
|
|
67
|
-
active = @client.capture_session_binding
|
|
68
|
-
raise Error::AuthenticationError, 'No active session' if rejected_refresh?(binding, active)
|
|
69
|
-
|
|
70
|
-
_, active_lineage, current = active
|
|
71
|
-
raise Error::SessionChangedError unless current && active_lineage == binding[1]
|
|
72
|
-
|
|
73
|
-
active
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def rejected_refresh?(binding, active)
|
|
77
|
-
generation, lineage, = binding
|
|
78
|
-
@client.refresh_rejected?(generation, lineage) &&
|
|
79
|
-
active.first == generation + 1 && active.last.nil?
|
|
68
|
+
run_auth_notifications(notifications)
|
|
80
69
|
end
|
|
81
70
|
|
|
82
71
|
def perform_refresh(binding, notifications)
|
|
83
72
|
active = owned_session_binding(binding)
|
|
84
73
|
return active.last if active.first != binding.first
|
|
85
|
-
raise Error::AuthenticationError, 'No refresh token' unless binding.last.refresh_token
|
|
86
74
|
|
|
87
|
-
|
|
75
|
+
refresh_token = binding.last.refresh_token
|
|
76
|
+
raise Error::AuthenticationError, 'No refresh token' unless refresh_token
|
|
77
|
+
|
|
78
|
+
session = refreshed_session(binding, refresh_token, notifications)
|
|
88
79
|
unless binding[1].closing?
|
|
89
80
|
@client.store_session_if_current?(
|
|
90
81
|
session, binding.first, event: :token_refreshed, notifications: notifications
|
|
@@ -93,17 +84,23 @@ module Volcano
|
|
|
93
84
|
session
|
|
94
85
|
end
|
|
95
86
|
|
|
96
|
-
def refreshed_session(binding, notifications)
|
|
87
|
+
def refreshed_session(binding, refresh_token, notifications)
|
|
97
88
|
_, owner, current = binding
|
|
98
89
|
verified = owner.verified_pair?(current)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
begin
|
|
91
|
+
SessionCredentials.validate_refresh_source(current, verified: verified)
|
|
92
|
+
owner.verify_pair(nil)
|
|
93
|
+
parse_and_verify_refresh(owner, current, binding, refresh_token, notifications)
|
|
94
|
+
rescue Error::RateLimitedError
|
|
95
|
+
owner.verify_pair(current) if verified
|
|
96
|
+
raise
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_and_verify_refresh(owner, current, binding, refresh_token, notifications)
|
|
101
|
+
session = parse_refresh_session(refresh_payload(binding, refresh_token, notifications))
|
|
102
102
|
verify_refreshed_session(owner, current, session)
|
|
103
103
|
session
|
|
104
|
-
rescue Error::RateLimitedError
|
|
105
|
-
owner.verify_pair(current) if verified
|
|
106
|
-
raise
|
|
107
104
|
end
|
|
108
105
|
|
|
109
106
|
def verify_refreshed_session(owner, current, session)
|
|
@@ -111,8 +108,8 @@ module Volcano
|
|
|
111
108
|
owner.verify_pair(session)
|
|
112
109
|
end
|
|
113
110
|
|
|
114
|
-
def refresh_payload(binding, notifications)
|
|
115
|
-
Transport.body(refresh_response(
|
|
111
|
+
def refresh_payload(binding, refresh_token, notifications)
|
|
112
|
+
Transport.body(refresh_response(refresh_token), 200)
|
|
116
113
|
rescue Error::AuthenticationError
|
|
117
114
|
generation, owner, = binding
|
|
118
115
|
@client.reject_refresh_if_current?(generation, notifications: notifications) unless owner.closing?
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
# Rejects stale or replaced auth-session lineages.
|
|
5
|
+
module AuthSessionBinding
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def owned_session_binding(binding)
|
|
9
|
+
active = @client.capture_session_binding
|
|
10
|
+
raise Error::AuthenticationError, 'No active session' if rejected_refresh?(binding, active)
|
|
11
|
+
|
|
12
|
+
_, active_lineage, current = active
|
|
13
|
+
raise Error::SessionChangedError unless current && active_lineage == binding[1]
|
|
14
|
+
|
|
15
|
+
[active.first, active_lineage, current]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def rejected_refresh?(binding, active)
|
|
19
|
+
generation, lineage, = binding
|
|
20
|
+
@client.refresh_rejected?(generation, lineage) &&
|
|
21
|
+
active.first == generation + 1 && active.last.nil?
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
private_constant :AuthSessionBinding
|
|
25
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
# Builds and validates owned credential snapshots for the auth facade.
|
|
5
|
+
module AuthSessionConstruction
|
|
6
|
+
INCOMPLETE_SESSION = 'Expected a complete Volcano::Session'
|
|
7
|
+
private_constant :INCOMPLETE_SESSION
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def build_session(payload)
|
|
12
|
+
raise TypeError, INCOMPLETE_SESSION unless payload.is_a?(Hash)
|
|
13
|
+
|
|
14
|
+
user = payload.fetch('user')
|
|
15
|
+
raise TypeError, INCOMPLETE_SESSION unless user.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
owned_session(
|
|
18
|
+
access_token: required_session_string(payload.fetch('access_token')),
|
|
19
|
+
refresh_token: required_session_string(payload.fetch('refresh_token')),
|
|
20
|
+
user_id: required_session_string(user.fetch('id')),
|
|
21
|
+
user: user
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def parse_refresh_session(payload)
|
|
26
|
+
owned_complete_session(build_session(payload))
|
|
27
|
+
rescue KeyError, TypeError, NoMethodError, ArgumentError => e
|
|
28
|
+
raise Error::TransportError, INCOMPLETE_SESSION, cause: e
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def complete_session?(session)
|
|
32
|
+
values = [session.access_token, session.refresh_token, session.user_id]
|
|
33
|
+
values.all? { |value| value.is_a?(String) && !value.strip.empty? } &&
|
|
34
|
+
matching_session_user?(session)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def matching_session_user?(session)
|
|
38
|
+
session.user.nil? || (session.user.is_a?(Hash) && session.user['id'] == session.user_id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def owned_complete_session(session)
|
|
42
|
+
raise ArgumentError, INCOMPLETE_SESSION unless session.is_a?(Session) && complete_session?(session)
|
|
43
|
+
|
|
44
|
+
owned_session(
|
|
45
|
+
access_token: required_session_string(session.access_token),
|
|
46
|
+
refresh_token: required_session_string(session.refresh_token),
|
|
47
|
+
user_id: required_session_string(session.user_id),
|
|
48
|
+
user: session.user
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def required_session_string(value)
|
|
53
|
+
return value if value.is_a?(String) && !value.strip.empty?
|
|
54
|
+
|
|
55
|
+
raise ArgumentError, INCOMPLETE_SESSION
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def owned_session(access_token:, refresh_token:, user_id:, user: nil)
|
|
59
|
+
Session.new(
|
|
60
|
+
access_token: access_token.dup.freeze,
|
|
61
|
+
refresh_token: refresh_token.dup.freeze,
|
|
62
|
+
user_id: user_id.dup.freeze,
|
|
63
|
+
user: user
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|