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
|
@@ -1,49 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative 'session_page_mapping'
|
|
4
4
|
|
|
5
5
|
module Volcano
|
|
6
|
-
# Maps the internal wire response to immutable public session values.
|
|
7
|
-
module SessionPageMapping
|
|
8
|
-
SESSION_FIELDS = %i[
|
|
9
|
-
id user_id provider expires_at is_active is_current user_agent ip_address last_ip_address
|
|
10
|
-
last_activity_at session_started_at created_at updated_at
|
|
11
|
-
].freeze
|
|
12
|
-
REQUIRED_SESSION_FIELDS = SESSION_FIELDS.first(6).freeze
|
|
13
|
-
|
|
14
|
-
private
|
|
15
|
-
|
|
16
|
-
def session_page(body)
|
|
17
|
-
raise TypeError, 'Expected a complete session page' unless body.is_a?(Hash)
|
|
18
|
-
|
|
19
|
-
sessions = body.fetch('sessions')
|
|
20
|
-
raise TypeError, 'Expected a complete session page' unless sessions.is_a?(Array)
|
|
21
|
-
|
|
22
|
-
SessionPage.new(
|
|
23
|
-
sessions: sessions.map { |attributes| auth_session(attributes) },
|
|
24
|
-
total: body.fetch('total'), page: body.fetch('page'), limit: body.fetch('limit'),
|
|
25
|
-
total_pages: body.fetch('total_pages')
|
|
26
|
-
)
|
|
27
|
-
rescue KeyError
|
|
28
|
-
raise TypeError, 'Expected a complete session page'
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def auth_session(attributes)
|
|
32
|
-
raise TypeError, 'Expected a complete authentication session' unless attributes.is_a?(Hash)
|
|
33
|
-
|
|
34
|
-
required = REQUIRED_SESSION_FIELDS.to_h { |name| [name, attributes.fetch(name.to_s)] }
|
|
35
|
-
optional = (SESSION_FIELDS - REQUIRED_SESSION_FIELDS).to_h do |name|
|
|
36
|
-
[name, attributes[name.to_s]]
|
|
37
|
-
end
|
|
38
|
-
AuthSession.new(**required, **optional)
|
|
39
|
-
rescue KeyError
|
|
40
|
-
raise TypeError, 'Expected a complete authentication session'
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
private_constant :SessionPageMapping
|
|
44
|
-
|
|
45
6
|
# Multi-device session behavior for the authentication facade.
|
|
46
|
-
|
|
7
|
+
module AuthSessions
|
|
47
8
|
include SessionPageMapping
|
|
48
9
|
|
|
49
10
|
def list_sessions(page: 1, limit: 20)
|
|
@@ -57,10 +18,11 @@ module Volcano
|
|
|
57
18
|
|
|
58
19
|
def delete_session(session_id)
|
|
59
20
|
binding = @client.capture_session_binding
|
|
60
|
-
|
|
21
|
+
session = binding.last
|
|
22
|
+
raise Error::AuthenticationError, 'No active session' unless session
|
|
61
23
|
|
|
62
24
|
request_id = session_id.dup.freeze
|
|
63
|
-
deletes_current = same_session_id?(
|
|
25
|
+
deletes_current = same_session_id?(session.access_token, request_id)
|
|
64
26
|
error = delete_bound_session_error(binding, request_id)
|
|
65
27
|
current_unchanged = session_unchanged_after_deletion?(deletes_current, error, binding)
|
|
66
28
|
raise Error::SessionChangedError, cause: error unless current_unchanged
|
|
@@ -119,12 +81,14 @@ module Volcano
|
|
|
119
81
|
|
|
120
82
|
def deletion_binding_current?(binding, error)
|
|
121
83
|
active = @client.capture_session_binding
|
|
122
|
-
|
|
84
|
+
return true if active.last && active[1] == binding[1]
|
|
85
|
+
|
|
86
|
+
!error.nil? && rejected_refresh?(binding, active)
|
|
123
87
|
end
|
|
124
88
|
|
|
125
89
|
def same_session_id?(access_token, session_id)
|
|
126
90
|
current_session_id = access_token_session_id(access_token)
|
|
127
|
-
session_id.is_a?(String) && current_session_id&.casecmp?(session_id)
|
|
91
|
+
session_id.is_a?(String) && current_session_id&.casecmp?(session_id) == true
|
|
128
92
|
end
|
|
129
93
|
|
|
130
94
|
def access_token_session_id(access_token)
|
|
@@ -2,24 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module Volcano
|
|
4
4
|
# Revokes the captured server session and clears its local lineage.
|
|
5
|
-
|
|
5
|
+
module AuthSignOut
|
|
6
|
+
include AuthNotificationDispatch
|
|
7
|
+
|
|
6
8
|
def sign_out
|
|
9
|
+
# @type var notifications: Array[Method]
|
|
10
|
+
notifications = []
|
|
7
11
|
binding = @client.capture_session_binding
|
|
8
|
-
|
|
12
|
+
session = binding.last
|
|
13
|
+
return binding[1].wait_for_sign_out unless session
|
|
9
14
|
|
|
10
|
-
notifications = []
|
|
11
15
|
binding[1].sign_out do |preceding, pending|
|
|
12
|
-
sign_out_captured(binding, preceding, pending, notifications)
|
|
16
|
+
sign_out_captured(binding, session, preceding, pending, notifications)
|
|
13
17
|
end
|
|
14
18
|
ensure
|
|
15
|
-
notifications
|
|
19
|
+
run_auth_notifications(notifications)
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
private
|
|
19
23
|
|
|
20
|
-
def sign_out_captured(binding, preceding, pending, notifications)
|
|
24
|
+
def sign_out_captured(binding, session, preceding, pending, notifications)
|
|
21
25
|
generation, owner, = binding
|
|
22
|
-
current, refresh_error = revocation_credentials(
|
|
26
|
+
current, refresh_error = revocation_credentials(owner, session, preceding)
|
|
23
27
|
error = revocation_error(current, owner, pending ? refresh_error : nil, joined: pending)
|
|
24
28
|
cleared = @client.clear_session_if_current?(generation, lineage: owner, notifications: notifications)
|
|
25
29
|
raise Error::SessionChangedError, cause: error unless cleared
|
|
@@ -27,10 +31,13 @@ module Volcano
|
|
|
27
31
|
raise error if error
|
|
28
32
|
end
|
|
29
33
|
|
|
30
|
-
def revocation_credentials(
|
|
31
|
-
|
|
34
|
+
def revocation_credentials(owner, session, preceding)
|
|
35
|
+
resolved = owner.result(preceding) || session
|
|
36
|
+
raise TypeError, 'Expected a session from refresh' unless resolved.is_a?(Session)
|
|
37
|
+
|
|
38
|
+
[resolved, nil]
|
|
32
39
|
rescue Error::VolcanoError => e
|
|
33
|
-
[
|
|
40
|
+
[session, e]
|
|
34
41
|
end
|
|
35
42
|
|
|
36
43
|
def logout_response(refresh_token)
|
|
@@ -67,16 +74,17 @@ module Volcano
|
|
|
67
74
|
|
|
68
75
|
def revoke_access_session(session, session_id, refresh_error, joined:)
|
|
69
76
|
error = delete_session_error(session.access_token, session_id)
|
|
70
|
-
|
|
77
|
+
refresh_token = refreshable_revocation_token(error, session)
|
|
78
|
+
return error unless refresh_token
|
|
71
79
|
return refresh_error || error if joined
|
|
72
80
|
|
|
73
|
-
refreshed = parse_refresh_session(Transport.body(refresh_response(
|
|
81
|
+
refreshed = parse_refresh_session(Transport.body(refresh_response(refresh_token), 200))
|
|
74
82
|
SessionCredentials.validate_refresh(session, refreshed)
|
|
75
83
|
delete_session_error(refreshed.access_token, session_id)
|
|
76
84
|
end
|
|
77
85
|
|
|
78
|
-
def
|
|
79
|
-
error&.status == 401
|
|
86
|
+
def refreshable_revocation_token(error, session)
|
|
87
|
+
session.refresh_token if error&.status == 401
|
|
80
88
|
end
|
|
81
89
|
end
|
|
82
90
|
end
|
data/lib/volcano/auth_sign_up.rb
CHANGED
|
@@ -29,6 +29,8 @@ module Volcano
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def build_sign_up_result(payload)
|
|
32
|
+
raise TypeError, INVALID_SIGN_UP_RESULT unless payload.is_a?(Hash)
|
|
33
|
+
|
|
32
34
|
confirmation_required = payload.fetch('confirmation_required')
|
|
33
35
|
message = payload.fetch('message')
|
|
34
36
|
valid = [true, false].include?(confirmation_required) && message.is_a?(String)
|
data/lib/volcano/auth_state.rb
CHANGED
|
@@ -81,9 +81,10 @@ module Volcano
|
|
|
81
81
|
|
|
82
82
|
def update_user_if_current?(user, generation)
|
|
83
83
|
@mutex.synchronize do
|
|
84
|
-
|
|
84
|
+
current = @session
|
|
85
|
+
return false unless generation == @generation && current
|
|
85
86
|
|
|
86
|
-
@session = SessionCredentials.with_user(
|
|
87
|
+
@session = SessionCredentials.with_user(current, user)
|
|
87
88
|
true
|
|
88
89
|
end
|
|
89
90
|
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Volcano
|
|
4
4
|
# Queues auth events under the state mutex and invokes subscribers outside it.
|
|
5
5
|
module AuthStateNotifications
|
|
6
|
-
|
|
7
|
-
private_constant :
|
|
6
|
+
CALLBACK_FAILURE = Exception
|
|
7
|
+
private_constant :CALLBACK_FAILURE
|
|
8
8
|
|
|
9
9
|
private
|
|
10
10
|
|
|
@@ -26,12 +26,14 @@ module Volcano
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def drain_notifications
|
|
29
|
+
# @type var failure: Exception?
|
|
29
30
|
failure = nil
|
|
30
31
|
loop do
|
|
31
32
|
notification = next_notification
|
|
32
33
|
break unless notification
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
callback_ids, event, session = notification
|
|
36
|
+
current_failure = notify(callback_ids, event, session)
|
|
35
37
|
failure ||= current_failure
|
|
36
38
|
end
|
|
37
39
|
raise failure if failure
|
|
@@ -48,12 +50,13 @@ module Volcano
|
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
def notify(callback_ids, event, session)
|
|
53
|
+
# @type var failure: Exception?
|
|
51
54
|
failure = nil
|
|
52
55
|
callback_ids.each do |callback_id|
|
|
53
56
|
@mutex.synchronize { @callbacks[callback_id] }&.call(event, session)
|
|
54
57
|
rescue StandardError => e
|
|
55
58
|
Warning.warn("Volcano auth-state callback failed (#{e.class})\n")
|
|
56
|
-
rescue
|
|
59
|
+
rescue CALLBACK_FAILURE => e
|
|
57
60
|
unsubscribe(callback_id)
|
|
58
61
|
failure ||= e
|
|
59
62
|
end
|
data/lib/volcano/session.rb
CHANGED
|
@@ -9,79 +9,6 @@ module Volcano
|
|
|
9
9
|
end
|
|
10
10
|
private_constant :SESSION_USER_CODER
|
|
11
11
|
|
|
12
|
-
# Owns credential snapshots and preserves validated identity during refresh.
|
|
13
|
-
module SessionCredentials
|
|
14
|
-
def self.build(access_token, refresh_token)
|
|
15
|
-
raise ArgumentError, 'refresh_token requires access_token' if access_token.nil? && !refresh_token.nil?
|
|
16
|
-
return if access_token.nil?
|
|
17
|
-
|
|
18
|
-
Session.new(access_token: credential(access_token, :access_token),
|
|
19
|
-
refresh_token: credential(refresh_token, :refresh_token))
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def self.credential(value, name)
|
|
23
|
-
return if value.nil?
|
|
24
|
-
|
|
25
|
-
raise ArgumentError, "#{name} must be a non-empty string" unless value.is_a?(String) && !value.strip.empty?
|
|
26
|
-
|
|
27
|
-
value.dup.freeze
|
|
28
|
-
end
|
|
29
|
-
private_class_method :credential
|
|
30
|
-
|
|
31
|
-
def self.validate_refresh_source(current, verified: false)
|
|
32
|
-
return if verified || session_id(current.access_token)
|
|
33
|
-
|
|
34
|
-
raise Error::AuthenticationError, 'Cannot refresh supplied credentials without a session identifier'
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def self.validate_refresh(current, refreshed)
|
|
38
|
-
return unless current
|
|
39
|
-
|
|
40
|
-
validate_refresh_session_id(current, refreshed)
|
|
41
|
-
return unless current.user_id && current.user_id != refreshed&.user_id
|
|
42
|
-
|
|
43
|
-
raise Error::AuthenticationError, 'Refreshed session belongs to a different user'
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def self.validate_refresh_session_id(current, refreshed)
|
|
47
|
-
expected = session_id(current.access_token)
|
|
48
|
-
return unless expected && expected != session_id(refreshed&.access_token)
|
|
49
|
-
|
|
50
|
-
raise Error::AuthenticationError, 'Refreshed credentials belong to a different server session'
|
|
51
|
-
end
|
|
52
|
-
private_class_method :validate_refresh_session_id
|
|
53
|
-
|
|
54
|
-
# Untrusted continuity constraint, never authenticated user identity.
|
|
55
|
-
def self.session_id(access_token)
|
|
56
|
-
payload = token_payload(access_token)
|
|
57
|
-
return unless payload.is_a?(Hash)
|
|
58
|
-
|
|
59
|
-
value = payload['session_id']
|
|
60
|
-
value.downcase if value.is_a?(String) && value.match?(/\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/i)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def self.token_payload(access_token)
|
|
64
|
-
return unless access_token.is_a?(String)
|
|
65
|
-
|
|
66
|
-
parts = access_token.split('.')
|
|
67
|
-
return unless parts.length == 3
|
|
68
|
-
|
|
69
|
-
encoded = parts.fetch(1).tr('-_', '+/')
|
|
70
|
-
JSON.parse((encoded + ('=' * (-encoded.length % 4))).unpack1('m0'))
|
|
71
|
-
rescue ArgumentError, JSON::ParserError
|
|
72
|
-
nil
|
|
73
|
-
end
|
|
74
|
-
private_class_method :token_payload
|
|
75
|
-
|
|
76
|
-
def self.with_user(session, user)
|
|
77
|
-
user_id = (session.user_id || user.fetch('id')).dup.freeze
|
|
78
|
-
raise Error::AuthenticationError, 'Profile belongs to a different user' unless user['id'] == user_id
|
|
79
|
-
|
|
80
|
-
Session.new(**session.to_h, user_id: user_id, user: user)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
private_constant :SessionCredentials
|
|
84
|
-
|
|
85
12
|
# Local credentials; refresh credentials and user identity may be unknown.
|
|
86
13
|
Session = Data.define(:access_token, :refresh_token, :user_id, :user) do
|
|
87
14
|
def initialize(access_token:, refresh_token: nil, user_id: nil, user: nil)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'session'
|
|
6
|
+
|
|
7
|
+
module Volcano
|
|
8
|
+
# Owns credential snapshots and preserves validated identity during refresh.
|
|
9
|
+
module SessionCredentials
|
|
10
|
+
def self.build(access_token, refresh_token)
|
|
11
|
+
raise ArgumentError, 'refresh_token requires access_token' if access_token.nil? && !refresh_token.nil?
|
|
12
|
+
return if access_token.nil?
|
|
13
|
+
|
|
14
|
+
Session.new(access_token: credential(access_token, :access_token),
|
|
15
|
+
refresh_token: refresh_token.nil? ? nil : credential(refresh_token, :refresh_token))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.credential(value, name)
|
|
19
|
+
raise ArgumentError, "#{name} must be a non-empty string" unless value.is_a?(String) && !value.strip.empty?
|
|
20
|
+
|
|
21
|
+
value.dup.freeze
|
|
22
|
+
end
|
|
23
|
+
private_class_method :credential
|
|
24
|
+
|
|
25
|
+
def self.validate_refresh_source(current, verified: false)
|
|
26
|
+
return if verified || session_id(current.access_token)
|
|
27
|
+
|
|
28
|
+
raise Error::AuthenticationError, 'Cannot refresh supplied credentials without a session identifier'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.validate_refresh(current, refreshed)
|
|
32
|
+
return unless current
|
|
33
|
+
|
|
34
|
+
validate_refresh_session_id(current, refreshed)
|
|
35
|
+
return unless current.user_id && current.user_id != refreshed&.user_id
|
|
36
|
+
|
|
37
|
+
raise Error::AuthenticationError, 'Refreshed session belongs to a different user'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.validate_refresh_session_id(current, refreshed)
|
|
41
|
+
expected = session_id(current.access_token)
|
|
42
|
+
return unless expected && expected != session_id(refreshed&.access_token)
|
|
43
|
+
|
|
44
|
+
raise Error::AuthenticationError, 'Refreshed credentials belong to a different server session'
|
|
45
|
+
end
|
|
46
|
+
private_class_method :validate_refresh_session_id
|
|
47
|
+
|
|
48
|
+
# Untrusted continuity constraint, never authenticated user identity.
|
|
49
|
+
def self.session_id(access_token)
|
|
50
|
+
payload = token_payload(access_token)
|
|
51
|
+
return unless payload.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
value = payload['session_id']
|
|
54
|
+
value.downcase if value.is_a?(String) && value.match?(/\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/i)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.token_payload(access_token)
|
|
58
|
+
return unless access_token.is_a?(String)
|
|
59
|
+
|
|
60
|
+
parts = access_token.split('.')
|
|
61
|
+
return unless parts.length == 3
|
|
62
|
+
|
|
63
|
+
encoded = parts.fetch(1).tr('-_', '+/')
|
|
64
|
+
JSON.parse(Base64.strict_decode64(encoded + ('=' * (-encoded.length % 4))))
|
|
65
|
+
rescue ArgumentError, JSON::ParserError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
private_class_method :token_payload
|
|
69
|
+
|
|
70
|
+
def self.with_user(session, user)
|
|
71
|
+
user_id = session.user_id || user.fetch('id')
|
|
72
|
+
raise Error::AuthenticationError, 'Profile has no valid user identifier' unless user_id.is_a?(String)
|
|
73
|
+
|
|
74
|
+
user_id = user_id.dup.freeze
|
|
75
|
+
raise Error::AuthenticationError, 'Profile belongs to a different user' unless user['id'] == user_id
|
|
76
|
+
|
|
77
|
+
Session.new(access_token: session.access_token, refresh_token: session.refresh_token,
|
|
78
|
+
user_id: user_id, user: user)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
private_constant :SessionCredentials
|
|
82
|
+
end
|
|
@@ -42,7 +42,9 @@ module Volcano
|
|
|
42
42
|
def refresh(&)
|
|
43
43
|
operation, owner = @mutex.synchronize do
|
|
44
44
|
raise Error::SessionChangedError if @sign_out || @locally_cleared
|
|
45
|
-
|
|
45
|
+
|
|
46
|
+
current = @refresh
|
|
47
|
+
next [current, false] if current && !current.done
|
|
46
48
|
|
|
47
49
|
@refresh = Outcome.new(false)
|
|
48
50
|
[@refresh, true]
|
|
@@ -51,11 +53,7 @@ module Volcano
|
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
def sign_out
|
|
54
|
-
operation, owner, preceding, pending = @mutex.synchronize
|
|
55
|
-
first = @sign_out.nil?
|
|
56
|
-
@sign_out ||= Outcome.new(false)
|
|
57
|
-
[@sign_out, first, @refresh, @refresh && !@refresh.done]
|
|
58
|
-
end
|
|
56
|
+
operation, owner, preceding, pending = @mutex.synchronize { claim_sign_out }
|
|
59
57
|
execute(operation, owner) do
|
|
60
58
|
yield(preceding, pending)
|
|
61
59
|
ensure
|
|
@@ -79,10 +77,19 @@ module Volcano
|
|
|
79
77
|
|
|
80
78
|
private
|
|
81
79
|
|
|
80
|
+
def claim_sign_out
|
|
81
|
+
first = @sign_out.nil? == true
|
|
82
|
+
operation = @sign_out ||= Outcome.new(false)
|
|
83
|
+
current = @refresh
|
|
84
|
+
[operation, first, current, current && !current.done]
|
|
85
|
+
end
|
|
86
|
+
|
|
82
87
|
def execute(operation, owner)
|
|
83
88
|
return result(operation) unless owner
|
|
84
89
|
|
|
85
90
|
completed = false
|
|
91
|
+
# @type var value: Object?
|
|
92
|
+
value = nil
|
|
86
93
|
begin
|
|
87
94
|
value = yield
|
|
88
95
|
completed = true
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Volcano
|
|
4
|
+
# Maps the internal wire response to immutable public session values.
|
|
5
|
+
module SessionPageMapping
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def session_page(body)
|
|
9
|
+
raise TypeError, 'Expected a complete session page' unless body.is_a?(Hash)
|
|
10
|
+
|
|
11
|
+
sessions = body.fetch('sessions')
|
|
12
|
+
raise TypeError, 'Expected a complete session page' unless sessions.is_a?(Array)
|
|
13
|
+
|
|
14
|
+
SessionPage.new(
|
|
15
|
+
sessions: sessions.map { |attributes| auth_session(attributes) },
|
|
16
|
+
total: required_integer(body, 'total'), page: required_integer(body, 'page'),
|
|
17
|
+
limit: required_integer(body, 'limit'), total_pages: required_integer(body, 'total_pages')
|
|
18
|
+
)
|
|
19
|
+
rescue KeyError
|
|
20
|
+
raise TypeError, 'Expected a complete session page'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def auth_session(attributes)
|
|
24
|
+
raise TypeError, 'Expected a complete authentication session' unless attributes.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
build_auth_session(attributes)
|
|
27
|
+
rescue KeyError
|
|
28
|
+
raise TypeError, 'Expected a complete authentication session'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_auth_session(attributes)
|
|
32
|
+
AuthSession.new(
|
|
33
|
+
id: required_string(attributes, 'id'), user_id: required_string(attributes, 'user_id'),
|
|
34
|
+
provider: required_string(attributes, 'provider'), expires_at: required_time(attributes, 'expires_at'),
|
|
35
|
+
is_active: required_boolean(attributes, 'is_active'), is_current: required_boolean(attributes, 'is_current'),
|
|
36
|
+
user_agent: optional_string(attributes, 'user_agent'), ip_address: optional_string(attributes, 'ip_address'),
|
|
37
|
+
last_ip_address: optional_string(attributes, 'last_ip_address'),
|
|
38
|
+
last_activity_at: optional_time(attributes, 'last_activity_at'),
|
|
39
|
+
session_started_at: optional_time(attributes, 'session_started_at'),
|
|
40
|
+
created_at: optional_time(attributes, 'created_at'), updated_at: optional_time(attributes, 'updated_at')
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def required_integer(body, name)
|
|
45
|
+
value = body.fetch(name)
|
|
46
|
+
raise TypeError, 'Expected a complete session page' unless value.is_a?(Integer)
|
|
47
|
+
|
|
48
|
+
value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def required_string(body, name)
|
|
52
|
+
value = body.fetch(name)
|
|
53
|
+
raise TypeError, 'Expected a complete authentication session' unless value.is_a?(String)
|
|
54
|
+
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def required_time(body, name)
|
|
59
|
+
value = body.fetch(name)
|
|
60
|
+
raise TypeError, 'Expected a complete authentication session' unless value.is_a?(Time)
|
|
61
|
+
|
|
62
|
+
value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def required_boolean(body, name)
|
|
66
|
+
value = body.fetch(name)
|
|
67
|
+
unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
68
|
+
raise TypeError, 'Expected a complete authentication session'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
value
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def optional_string(body, name)
|
|
75
|
+
value = body[name]
|
|
76
|
+
raise TypeError, 'Expected a complete authentication session' unless value.nil? || value.is_a?(String)
|
|
77
|
+
|
|
78
|
+
value
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def optional_time(body, name)
|
|
82
|
+
value = body[name]
|
|
83
|
+
raise TypeError, 'Expected a complete authentication session' unless value.nil? || value.is_a?(Time)
|
|
84
|
+
|
|
85
|
+
value
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
private_constant :SessionPageMapping
|
|
89
|
+
end
|
data/lib/volcano/storage.rb
CHANGED
|
@@ -6,8 +6,8 @@ module Volcano
|
|
|
6
6
|
# Entry point for project object storage.
|
|
7
7
|
class Storage
|
|
8
8
|
def initialize(client, transport, api_url:, anon_key:)
|
|
9
|
-
@client = client
|
|
10
|
-
@transport = transport
|
|
9
|
+
@client = client.is_a?(StorageSessionAdapter) ? client : StorageSessionAdapter.new(client)
|
|
10
|
+
@transport = transport.is_a?(StorageTransportAdapter) ? transport : StorageTransportAdapter.new(transport)
|
|
11
11
|
@api_url = api_url
|
|
12
12
|
@anon_key = anon_key
|
|
13
13
|
end
|
|
@@ -25,9 +25,11 @@ module Volcano
|
|
|
25
25
|
|
|
26
26
|
# Operates on objects in one storage bucket.
|
|
27
27
|
class StorageBucket
|
|
28
|
+
include StorageObjectResponse
|
|
29
|
+
|
|
28
30
|
def initialize(client, transport, name, api_url:, anon_key:)
|
|
29
|
-
@client = client
|
|
30
|
-
@transport = transport
|
|
31
|
+
@client = client.is_a?(StorageSessionAdapter) ? client : StorageSessionAdapter.new(client)
|
|
32
|
+
@transport = transport.is_a?(StorageTransportAdapter) ? transport : StorageTransportAdapter.new(transport)
|
|
31
33
|
@name = name.dup.freeze
|
|
32
34
|
@api_url = api_url.dup.freeze
|
|
33
35
|
@anon_key = anon_key.dup.freeze
|
|
@@ -36,7 +38,7 @@ module Volcano
|
|
|
36
38
|
|
|
37
39
|
def upload(path, value, content_type: nil)
|
|
38
40
|
mime_type = upload_content_type(content_type)
|
|
39
|
-
path = storage_paths(path).
|
|
41
|
+
path = storage_paths(path).fetch(0)
|
|
40
42
|
binding = @client.capture_session_binding
|
|
41
43
|
@client.session_token
|
|
42
44
|
content = upload_bytes(value).freeze
|
|
@@ -49,11 +51,11 @@ module Volcano
|
|
|
49
51
|
content_type: mime_type
|
|
50
52
|
)
|
|
51
53
|
end
|
|
52
|
-
Transport.body(response, 201)
|
|
54
|
+
storage_payload(Transport.body(response, 201))
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
def download(path, range: nil)
|
|
56
|
-
path = storage_paths(path).
|
|
58
|
+
path = storage_paths(path).fetch(0)
|
|
57
59
|
range = range&.dup&.freeze
|
|
58
60
|
response = storage_request do |token|
|
|
59
61
|
@transport.download_storage_object(
|
|
@@ -65,7 +67,7 @@ module Volcano
|
|
|
65
67
|
end
|
|
66
68
|
expected_status = range && response.status == 206 ? 206 : 200
|
|
67
69
|
Transport.body(response, expected_status)
|
|
68
|
-
response.data.b
|
|
70
|
+
storage_string(response.data).b
|
|
69
71
|
end
|
|
70
72
|
|
|
71
73
|
def list(prefix = '', limit: nil, cursor: nil)
|
|
@@ -80,11 +82,10 @@ module Volcano
|
|
|
80
82
|
cursor: cursor
|
|
81
83
|
)
|
|
82
84
|
end
|
|
83
|
-
payload = Transport.body(response, 200)
|
|
84
|
-
next_cursor = payload['next_cursor']
|
|
85
|
+
payload = storage_payload(Transport.body(response, 200))
|
|
85
86
|
StoragePage.new(
|
|
86
|
-
objects: payload.fetch('objects', [])
|
|
87
|
-
next_cursor: next_cursor
|
|
87
|
+
objects: storage_object_list(payload.fetch('objects', [])),
|
|
88
|
+
next_cursor: storage_cursor(payload['next_cursor'])
|
|
88
89
|
)
|
|
89
90
|
end
|
|
90
91
|
|
|
@@ -102,31 +103,14 @@ module Volcano
|
|
|
102
103
|
end
|
|
103
104
|
|
|
104
105
|
def upload_bytes(value)
|
|
105
|
-
bytes = value.
|
|
106
|
+
bytes = if value.is_a?(String)
|
|
107
|
+
value
|
|
108
|
+
elsif value.respond_to?(:read)
|
|
109
|
+
value.read
|
|
110
|
+
end
|
|
106
111
|
raise ArgumentError, 'upload data must be a String or IO' unless bytes.is_a?(String)
|
|
107
112
|
|
|
108
113
|
bytes.b
|
|
109
114
|
end
|
|
110
|
-
|
|
111
|
-
def storage_object(payload)
|
|
112
|
-
StorageObject.new(
|
|
113
|
-
id: payload.fetch('id'),
|
|
114
|
-
bucket_id: payload.fetch('bucket_id'),
|
|
115
|
-
name: payload.fetch('name'),
|
|
116
|
-
size: payload.fetch('size'),
|
|
117
|
-
mime_type: payload.fetch('mime_type'),
|
|
118
|
-
is_public: payload.fetch('is_public'),
|
|
119
|
-
owner_id: payload['owner_id'],
|
|
120
|
-
etag: payload['etag'],
|
|
121
|
-
metadata: payload['metadata'],
|
|
122
|
-
created_at: parse_time(payload['created_at']),
|
|
123
|
-
updated_at: parse_time(payload['updated_at']),
|
|
124
|
-
public_url: payload['public_url']
|
|
125
|
-
)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def parse_time(value)
|
|
129
|
-
value.is_a?(String) ? Time.iso8601(value) : value
|
|
130
|
-
end
|
|
131
115
|
end
|
|
132
116
|
end
|
|
@@ -6,7 +6,13 @@ module Volcano
|
|
|
6
6
|
].freeze
|
|
7
7
|
private_constant :STORAGE_OBJECT_ATTRIBUTES
|
|
8
8
|
|
|
9
|
-
StorageObject = Data.define(*STORAGE_OBJECT_ATTRIBUTES)
|
|
9
|
+
StorageObject = Data.define(*STORAGE_OBJECT_ATTRIBUTES)
|
|
10
|
+
|
|
11
|
+
# Validates and freezes object metadata returned by storage.
|
|
12
|
+
class StorageObject
|
|
13
|
+
# @dynamic id, bucket_id, name, size, mime_type, is_public, owner_id, etag
|
|
14
|
+
# @dynamic metadata, created_at, updated_at, public_url
|
|
15
|
+
# @dynamic members, with, to_h, deconstruct, deconstruct_keys, self.[], self.members
|
|
10
16
|
def initialize(**attributes)
|
|
11
17
|
unknown = attributes.keys - STORAGE_OBJECT_ATTRIBUTES
|
|
12
18
|
raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
|
|
@@ -17,7 +23,8 @@ module Volcano
|
|
|
17
23
|
values = STORAGE_OBJECT_ATTRIBUTES.to_h do |name|
|
|
18
24
|
[name, immutable_value(attributes[name])]
|
|
19
25
|
end
|
|
20
|
-
|
|
26
|
+
attributes = values
|
|
27
|
+
super
|
|
21
28
|
end
|
|
22
29
|
|
|
23
30
|
private
|
|
@@ -40,10 +47,16 @@ module Volcano
|
|
|
40
47
|
end
|
|
41
48
|
end
|
|
42
49
|
|
|
43
|
-
StoragePage = Data.define(:objects, :next_cursor)
|
|
50
|
+
StoragePage = Data.define(:objects, :next_cursor)
|
|
51
|
+
|
|
52
|
+
# Freezes a page of storage objects and its cursor.
|
|
53
|
+
class StoragePage
|
|
54
|
+
# @dynamic objects, next_cursor, members, with, to_h, deconstruct, deconstruct_keys
|
|
55
|
+
# @dynamic self.[], self.members
|
|
44
56
|
def initialize(objects:, next_cursor: nil)
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
objects = objects.to_a.dup.freeze
|
|
58
|
+
next_cursor = next_cursor.nil? ? nil : next_cursor.dup.freeze
|
|
59
|
+
super
|
|
47
60
|
end
|
|
48
61
|
end
|
|
49
62
|
end
|