workato-connector-sdk 0.1.1 → 0.4.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 +5 -5
- data/README.md +57 -30
- data/exe/workato +12 -1
- data/lib/workato/cli/edit_command.rb +1 -3
- data/lib/workato/cli/exec_command.rb +23 -7
- data/lib/workato/cli/generate_command.rb +1 -2
- data/lib/workato/cli/generators/connector_generator.rb +4 -1
- data/lib/workato/cli/main.rb +50 -3
- data/lib/workato/cli/oauth2_command.rb +180 -0
- data/lib/workato/cli/push_command.rb +36 -24
- data/lib/workato/connector/sdk/action.rb +52 -4
- data/lib/workato/connector/sdk/connection.rb +144 -0
- data/lib/workato/connector/sdk/connector.rb +24 -9
- data/lib/workato/connector/sdk/dsl/aws.rb +225 -0
- data/lib/workato/connector/sdk/dsl/error.rb +1 -1
- data/lib/workato/connector/sdk/dsl/http.rb +19 -0
- data/lib/workato/connector/sdk/dsl/time.rb +8 -1
- data/lib/workato/connector/sdk/dsl/workato_code_lib.rb +21 -4
- data/lib/workato/connector/sdk/dsl.rb +2 -0
- data/lib/workato/connector/sdk/errors.rb +16 -1
- data/lib/workato/connector/sdk/object_definitions.rb +10 -10
- data/lib/workato/connector/sdk/operation.rb +38 -40
- data/lib/workato/connector/sdk/request.rb +11 -7
- data/lib/workato/connector/sdk/schema/field/array.rb +25 -0
- data/lib/workato/connector/sdk/schema/field/convertors.rb +189 -0
- data/lib/workato/connector/sdk/schema/field/date.rb +28 -0
- data/lib/workato/connector/sdk/schema/field/date_time.rb +28 -0
- data/lib/workato/connector/sdk/schema/field/integer.rb +27 -0
- data/lib/workato/connector/sdk/schema/field/number.rb +27 -0
- data/lib/workato/connector/sdk/schema/field/object.rb +25 -0
- data/lib/workato/connector/sdk/schema/field/string.rb +26 -0
- data/lib/workato/connector/sdk/schema/type/time.rb +53 -0
- data/lib/workato/connector/sdk/schema/type/unicode_string.rb +22 -0
- data/lib/workato/connector/sdk/schema.rb +230 -0
- data/lib/workato/connector/sdk/settings.rb +6 -3
- data/lib/workato/connector/sdk/trigger.rb +25 -0
- data/lib/workato/connector/sdk/version.rb +1 -1
- data/lib/workato/connector/sdk.rb +1 -0
- data/lib/workato/extension/string.rb +20 -10
- data/lib/workato/web/app.rb +23 -0
- data/templates/Gemfile.erb +1 -0
- data/templates/spec/action_spec.rb.erb +7 -1
- data/templates/spec/trigger_spec.rb.erb +6 -0
- metadata +102 -4
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'uri'
|
4
4
|
require 'ruby-progressbar'
|
5
5
|
require 'zip'
|
6
|
+
require 'fileutils'
|
6
7
|
|
7
8
|
module Workato
|
8
9
|
module CLI
|
@@ -19,6 +20,7 @@ module Workato
|
|
19
20
|
'live-eu' => 'https://app.eu.workato.com'
|
20
21
|
}.freeze
|
21
22
|
|
23
|
+
API_USER_PATH = '/api/users/me'
|
22
24
|
API_IMPORT_PATH = '/api/packages/import'
|
23
25
|
API_PACKAGE_PATH = '/api/packages'
|
24
26
|
IMPORT_IN_PROGRESS = 'in_progress'
|
@@ -30,38 +32,33 @@ module Workato
|
|
30
32
|
AWAIT_IMPORT_SLEEP_INTERVAL = 15 # seconds
|
31
33
|
AWAIT_IMPORT_TIMEOUT_INTERVAL = 120 # seconds
|
32
34
|
|
33
|
-
def initialize(
|
34
|
-
@folder_id = folder
|
35
|
+
def initialize(options:)
|
35
36
|
@options = options
|
36
37
|
@api_base_url = ENVIRONMENTS.fetch(options[:environment])
|
37
38
|
@api_email = options[:api_email] || ENV[WORKATO_API_EMAIL_ENV]
|
38
39
|
@api_token = options[:api_token] || ENV[WORKATO_API_TOKEN_ENV]
|
40
|
+
@folder_id = options[:folder]
|
39
41
|
end
|
40
42
|
|
41
43
|
def call
|
42
|
-
|
44
|
+
zip_file_path = build_package
|
43
45
|
say_status :success, 'Build package' if verbose?
|
44
46
|
|
45
|
-
import_id = import_package(
|
47
|
+
import_id = import_package(zip_file_path)
|
46
48
|
say_status :success, 'Upload package' if verbose?
|
47
49
|
say_status :waiting, 'Process package' if verbose?
|
48
50
|
|
49
51
|
result = await_import(import_id)
|
50
|
-
if result.fetch('status') == 'failed'
|
51
|
-
|
52
|
-
|
53
|
-
say "Connector was successfully uploaded to #{api_base_url}"
|
54
|
-
end
|
55
|
-
rescue StandardError => e
|
56
|
-
say e.message
|
52
|
+
raise human_friendly_error(result) if result.fetch('status') == 'failed'
|
53
|
+
|
54
|
+
say "Connector was successfully uploaded to #{api_base_url}"
|
57
55
|
ensure
|
58
|
-
|
56
|
+
FileUtils.rm_f(zip_file_path) if zip_file_path
|
59
57
|
end
|
60
58
|
|
61
59
|
private
|
62
60
|
|
63
61
|
attr_reader :options,
|
64
|
-
:folder_id,
|
65
62
|
:api_token,
|
66
63
|
:api_email,
|
67
64
|
:api_base_url
|
@@ -78,16 +75,13 @@ module Workato
|
|
78
75
|
end
|
79
76
|
|
80
77
|
def build_package
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
add_logo(archive)
|
78
|
+
::Dir::Tmpname.create(['connector', '.zip']) do |path|
|
79
|
+
::Zip::File.open(path, ::Zip::File::CREATE) do |archive|
|
80
|
+
add_connector(archive)
|
81
|
+
add_manifest(archive)
|
82
|
+
add_logo(archive)
|
83
|
+
end
|
88
84
|
end
|
89
|
-
|
90
|
-
zip_file
|
91
85
|
end
|
92
86
|
|
93
87
|
def add_connector(archive)
|
@@ -110,11 +104,11 @@ module Workato
|
|
110
104
|
end
|
111
105
|
end
|
112
106
|
|
113
|
-
def import_package(
|
107
|
+
def import_package(zip_file_path)
|
114
108
|
url = "#{api_base_url}#{API_IMPORT_PATH}/#{folder_id}"
|
115
109
|
response = RestClient.post(
|
116
110
|
url,
|
117
|
-
File.open(
|
111
|
+
File.open(zip_file_path),
|
118
112
|
auth_headers.merge(
|
119
113
|
'Content-Type' => 'application/zip'
|
120
114
|
)
|
@@ -197,7 +191,25 @@ module Workato
|
|
197
191
|
}
|
198
192
|
end
|
199
193
|
|
194
|
+
def folder_id
|
195
|
+
@folder_id ||=
|
196
|
+
begin
|
197
|
+
url = "#{api_base_url}#{API_USER_PATH}"
|
198
|
+
response = RestClient.get(url, auth_headers)
|
199
|
+
|
200
|
+
json = JSON.parse(response.body)
|
201
|
+
json.fetch('root_folder_id').tap do |folder_id|
|
202
|
+
say_status :success, "Fetch root folder ID: #{folder_id}" if verbose?
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def human_friendly_error(result)
|
208
|
+
result.fetch('error').gsub("#{PACKAGE_ENTRY_NAME}.json: ", '')
|
209
|
+
end
|
210
|
+
|
200
211
|
private_constant :IMPORT_IN_PROGRESS,
|
212
|
+
:API_USER_PATH,
|
201
213
|
:API_IMPORT_PATH,
|
202
214
|
:API_PACKAGE_PATH,
|
203
215
|
:PACKAGE_ENTRY_NAME,
|
@@ -14,6 +14,8 @@ module Workato
|
|
14
14
|
RETRY_DELAY = 5.seconds
|
15
15
|
MAX_RETRIES = 3
|
16
16
|
|
17
|
+
MAX_REINVOKES = 5
|
18
|
+
|
17
19
|
def initialize(action:, connection: {}, methods: {}, settings: {}, object_definitions: nil)
|
18
20
|
super(
|
19
21
|
operation: action,
|
@@ -26,10 +28,32 @@ module Workato
|
|
26
28
|
initialize_retry
|
27
29
|
end
|
28
30
|
|
29
|
-
def execute(settings = nil, input = {}, extended_input_schema = [], extended_output_schema = [],
|
31
|
+
def execute(settings = nil, input = {}, extended_input_schema = [], extended_output_schema = [], continue = {},
|
32
|
+
&block)
|
30
33
|
raise InvalidDefinitionError, "'execute' block is required for action" unless block || action[:execute]
|
31
34
|
|
32
|
-
|
35
|
+
loop do
|
36
|
+
if @reinvokes_remaining&.zero?
|
37
|
+
raise "Max number of reinvokes on SDK Gem reached. Current limit is #{reinvoke_limit}"
|
38
|
+
end
|
39
|
+
|
40
|
+
reinvoke_sleep if @reinvoke_after
|
41
|
+
|
42
|
+
@reinvoke_after = nil
|
43
|
+
|
44
|
+
result = super(
|
45
|
+
settings,
|
46
|
+
input,
|
47
|
+
extended_input_schema,
|
48
|
+
extended_output_schema,
|
49
|
+
continue,
|
50
|
+
&(block || action[:execute])
|
51
|
+
)
|
52
|
+
|
53
|
+
break result unless @reinvoke_after
|
54
|
+
|
55
|
+
continue = @reinvoke_after[:continue]
|
56
|
+
end
|
33
57
|
rescue RequestError => e
|
34
58
|
raise e unless retry?(e)
|
35
59
|
|
@@ -37,12 +61,28 @@ module Workato
|
|
37
61
|
sleep(RETRY_DELAY) && retry
|
38
62
|
end
|
39
63
|
|
64
|
+
def invoke(input = {})
|
65
|
+
extended_schema = extended_schema(nil, input)
|
66
|
+
config_schema = Schema.new(schema: config_fields_schema)
|
67
|
+
input_schema = Schema.new(schema: extended_schema[:input])
|
68
|
+
output_schema = Schema.new(schema: extended_schema[:output])
|
69
|
+
|
70
|
+
input = apply_input_schema(input, config_schema + input_schema)
|
71
|
+
output = execute(nil, input, input_schema, output_schema)
|
72
|
+
apply_output_schema(output, output_schema)
|
73
|
+
end
|
74
|
+
|
40
75
|
def checkpoint!(continue:, temp_output: nil)
|
41
|
-
|
76
|
+
# no-op
|
42
77
|
end
|
43
78
|
|
44
79
|
def reinvoke_after(seconds:, continue:, temp_output: nil)
|
45
|
-
|
80
|
+
@reinvokes_remaining = (@reinvokes_remaining ? @reinvokes_remaining - 1 : reinvoke_limit)
|
81
|
+
@reinvoke_after = {
|
82
|
+
seconds: seconds,
|
83
|
+
continue: continue,
|
84
|
+
temp_output: temp_output
|
85
|
+
}
|
46
86
|
end
|
47
87
|
|
48
88
|
private
|
@@ -81,6 +121,14 @@ module Workato
|
|
81
121
|
@retry_methods.include?(exception.method.to_s.downcase)
|
82
122
|
end
|
83
123
|
|
124
|
+
def reinvoke_sleep
|
125
|
+
sleep((ENV['WAIT_REINVOKE_AFTER'].presence || @reinvoke_after[:seconds]).to_f)
|
126
|
+
end
|
127
|
+
|
128
|
+
def reinvoke_limit
|
129
|
+
@reinvoke_limit ||= (ENV['MAX_REINVOKES'].presence || MAX_REINVOKES).to_i
|
130
|
+
end
|
131
|
+
|
84
132
|
alias action operation
|
85
133
|
end
|
86
134
|
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './block_invocation_refinements'
|
4
|
+
|
5
|
+
module Workato
|
6
|
+
module Connector
|
7
|
+
module Sdk
|
8
|
+
class Connection
|
9
|
+
using BlockInvocationRefinements
|
10
|
+
|
11
|
+
attr_reader :source
|
12
|
+
|
13
|
+
def initialize(connection: {}, methods: {}, settings: {})
|
14
|
+
@methods_source = methods.with_indifferent_access
|
15
|
+
@source = connection.with_indifferent_access
|
16
|
+
@settings = settings
|
17
|
+
end
|
18
|
+
|
19
|
+
def authorization
|
20
|
+
@authorization ||= Authorization.new(
|
21
|
+
connection: source,
|
22
|
+
methods: methods_source,
|
23
|
+
settings: @settings
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def base_uri(settings = {})
|
28
|
+
source[:base_uri]&.call(settings.with_indifferent_access)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :methods_source
|
34
|
+
|
35
|
+
class Authorization
|
36
|
+
attr_reader :source
|
37
|
+
|
38
|
+
def initialize(connection: {}, methods: {}, settings: {})
|
39
|
+
@connection_source = connection.with_indifferent_access
|
40
|
+
@source = (connection[:authorization] || {}).with_indifferent_access
|
41
|
+
@methods_source = methods.with_indifferent_access
|
42
|
+
@settings = settings
|
43
|
+
end
|
44
|
+
|
45
|
+
def token_url?
|
46
|
+
source[:token_url].present?
|
47
|
+
end
|
48
|
+
|
49
|
+
def acquire?
|
50
|
+
source[:acquire].present?
|
51
|
+
end
|
52
|
+
|
53
|
+
def refresh?
|
54
|
+
source[:refresh].present?
|
55
|
+
end
|
56
|
+
|
57
|
+
def type
|
58
|
+
source[:type]
|
59
|
+
end
|
60
|
+
|
61
|
+
def refresh_on
|
62
|
+
Array.wrap(source[:refresh_on]).compact
|
63
|
+
end
|
64
|
+
|
65
|
+
def detect_on
|
66
|
+
Array.wrap(source[:detect_on]).compact
|
67
|
+
end
|
68
|
+
|
69
|
+
def client_id(settings = {})
|
70
|
+
client_id = source[:client_id]
|
71
|
+
|
72
|
+
if client_id.is_a?(Proc)
|
73
|
+
Dsl::WithDsl.execute(settings.with_indifferent_access, &client_id)
|
74
|
+
else
|
75
|
+
client_id
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def client_secret(settings = {})
|
80
|
+
client_secret_source = source[:client_secret]
|
81
|
+
|
82
|
+
if client_secret_source.is_a?(Proc)
|
83
|
+
Dsl::WithDsl.execute(settings.with_indifferent_access, &client_secret_source)
|
84
|
+
else
|
85
|
+
client_secret_source
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def authorization_url(settings = {})
|
90
|
+
source[:authorization_url]&.call(settings.with_indifferent_access)
|
91
|
+
end
|
92
|
+
|
93
|
+
def token_url(settings = {})
|
94
|
+
source[:token_url]&.call(settings.with_indifferent_access)
|
95
|
+
end
|
96
|
+
|
97
|
+
def acquire(settings = {}, oauth2_code = nil, redirect_url = nil)
|
98
|
+
acquire_proc = source[:acquire]
|
99
|
+
raise InvalidDefinitionError, "Expect 'acquire' block" unless acquire_proc
|
100
|
+
|
101
|
+
Workato::Connector::Sdk::Operation.new(
|
102
|
+
connection: Connection.new(
|
103
|
+
connection: connection_source.merge(
|
104
|
+
authorization: source.merge(
|
105
|
+
apply: nil
|
106
|
+
)
|
107
|
+
),
|
108
|
+
methods: methods_source,
|
109
|
+
settings: @settings
|
110
|
+
),
|
111
|
+
methods: methods_source,
|
112
|
+
settings: @settings
|
113
|
+
).execute(settings, { auth_code: oauth2_code, redirect_url: redirect_url }) do |connection, input|
|
114
|
+
instance_exec(connection, input[:auth_code], input[:redirect_url], &acquire_proc)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def refresh(settings = {}, refresh_token = nil)
|
119
|
+
refresh_proc = source[:refresh]
|
120
|
+
raise InvalidDefinitionError, "Expect 'refresh' block" unless refresh_proc
|
121
|
+
|
122
|
+
Workato::Connector::Sdk::Operation.new(
|
123
|
+
connection: Connection.new(
|
124
|
+
methods: methods_source,
|
125
|
+
settings: @settings
|
126
|
+
),
|
127
|
+
methods: methods_source,
|
128
|
+
settings: @settings
|
129
|
+
).execute(settings, { refresh_token: refresh_token }) do |connection, input|
|
130
|
+
instance_exec(connection, input[:refresh_token], &refresh_proc)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
attr_reader :connection_source,
|
137
|
+
:methods_source
|
138
|
+
end
|
139
|
+
|
140
|
+
private_constant :Authorization
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -24,9 +24,9 @@ module Workato
|
|
24
24
|
def actions
|
25
25
|
@actions ||= ActionsProxy.new(
|
26
26
|
actions: source[:actions].presence || {},
|
27
|
-
object_definitions: object_definitions,
|
28
27
|
methods: methods_source,
|
29
|
-
|
28
|
+
object_definitions: object_definitions,
|
29
|
+
connection: connection,
|
30
30
|
settings: settings
|
31
31
|
)
|
32
32
|
end
|
@@ -34,7 +34,7 @@ module Workato
|
|
34
34
|
def methods
|
35
35
|
@methods ||= MethodsProxy.new(
|
36
36
|
methods: methods_source,
|
37
|
-
connection:
|
37
|
+
connection: connection,
|
38
38
|
settings: settings
|
39
39
|
)
|
40
40
|
end
|
@@ -45,7 +45,7 @@ module Workato
|
|
45
45
|
execute: source[:test]
|
46
46
|
},
|
47
47
|
methods: methods_source,
|
48
|
-
connection:
|
48
|
+
connection: connection,
|
49
49
|
settings: send(:settings)
|
50
50
|
).execute(settings)
|
51
51
|
end
|
@@ -53,9 +53,9 @@ module Workato
|
|
53
53
|
def triggers
|
54
54
|
@triggers ||= TriggersProxy.new(
|
55
55
|
triggers: source[:triggers].presence || {},
|
56
|
-
object_definitions: object_definitions,
|
57
56
|
methods: methods_source,
|
58
|
-
connection:
|
57
|
+
connection: connection,
|
58
|
+
object_definitions: object_definitions,
|
59
59
|
settings: settings
|
60
60
|
)
|
61
61
|
end
|
@@ -64,7 +64,7 @@ module Workato
|
|
64
64
|
@object_definitions ||= ObjectDefinitions.new(
|
65
65
|
object_definitions: source[:object_definitions].presence || {},
|
66
66
|
methods: methods_source,
|
67
|
-
connection:
|
67
|
+
connection: connection,
|
68
68
|
settings: settings
|
69
69
|
)
|
70
70
|
end
|
@@ -72,6 +72,14 @@ module Workato
|
|
72
72
|
def pick_lists
|
73
73
|
@pick_lists ||= PickListsProxy.new(
|
74
74
|
pick_lists: source[:pick_lists].presence || {},
|
75
|
+
methods: methods_source,
|
76
|
+
connection: connection,
|
77
|
+
settings: settings
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def connection
|
82
|
+
@connection ||= Connection.new(
|
75
83
|
methods: methods_source,
|
76
84
|
connection: connection_source,
|
77
85
|
settings: settings
|
@@ -107,7 +115,7 @@ module Workato
|
|
107
115
|
|
108
116
|
def define_action_methods(actions)
|
109
117
|
actions.each do |action, definition|
|
110
|
-
define_singleton_method(action) do
|
118
|
+
define_singleton_method(action) do |input_ = nil|
|
111
119
|
@actions ||= {}
|
112
120
|
@actions[action] ||= Action.new(
|
113
121
|
action: definition,
|
@@ -116,6 +124,9 @@ module Workato
|
|
116
124
|
connection: connection,
|
117
125
|
settings: settings
|
118
126
|
)
|
127
|
+
return @actions[action] if input_.nil?
|
128
|
+
|
129
|
+
@actions[action].invoke(input_)
|
119
130
|
end
|
120
131
|
end
|
121
132
|
end
|
@@ -213,7 +224,7 @@ module Workato
|
|
213
224
|
|
214
225
|
def define_trigger_methods(triggers)
|
215
226
|
triggers.each do |trigger, definition|
|
216
|
-
define_singleton_method(trigger) do
|
227
|
+
define_singleton_method(trigger) do |input_ = nil, payload = {}, headers = {}, params = {}|
|
217
228
|
@triggers[trigger] ||= Trigger.new(
|
218
229
|
trigger: definition,
|
219
230
|
object_definitions: object_definitions,
|
@@ -221,6 +232,10 @@ module Workato
|
|
221
232
|
connection: connection,
|
222
233
|
settings: settings
|
223
234
|
)
|
235
|
+
|
236
|
+
return @triggers[trigger] if input_.nil?
|
237
|
+
|
238
|
+
@triggers[trigger].invoke(input_, payload, headers, params)
|
224
239
|
end
|
225
240
|
end
|
226
241
|
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Workato
|
4
|
+
module Connector
|
5
|
+
module Sdk
|
6
|
+
module Dsl
|
7
|
+
module AWS
|
8
|
+
TEMP_CREDENTIALS_REFRESH_TIMEOUT = 60 # seconds
|
9
|
+
|
10
|
+
DUMMY_AWS_IAM_EXTERNAL_ID = 'dummy-aws-iam-external-id'
|
11
|
+
DUMMY_AWS_WORKATO_ACCOUNT_ID = 'dummy-aws-workato-account-id'
|
12
|
+
|
13
|
+
AMAZON_ROLE_CLIENT_ID = ENV['AMAZON_ROLE_CLIENT_ID']
|
14
|
+
AMAZON_ROLE_CLIENT_KEY = ENV['AMAZON_ROLE_CLIENT_KEY']
|
15
|
+
AMAZON_ROLE_CLIENT_SECRET = ENV['AMAZON_ROLE_CLIENT_SECRET']
|
16
|
+
|
17
|
+
WWW_FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8'
|
18
|
+
|
19
|
+
def aws
|
20
|
+
AWS
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def generate_signature(connection:,
|
25
|
+
service:,
|
26
|
+
region:,
|
27
|
+
host: "#{service}.#{region}.amazonaws.com",
|
28
|
+
path: '/',
|
29
|
+
method: 'GET',
|
30
|
+
params: {},
|
31
|
+
headers: {},
|
32
|
+
payload: '')
|
33
|
+
|
34
|
+
credentials = if connection[:aws_assume_role].present?
|
35
|
+
role_based_auth(settings: connection)
|
36
|
+
else
|
37
|
+
{
|
38
|
+
access_key_id: connection[:aws_api_key],
|
39
|
+
secret_access_key: connection[:aws_secret_key]
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
url, headers = create_signature(
|
44
|
+
credentials: credentials,
|
45
|
+
service: service,
|
46
|
+
host: host,
|
47
|
+
region: region,
|
48
|
+
method: method,
|
49
|
+
path: path,
|
50
|
+
params: params,
|
51
|
+
headers: (headers || {}).with_indifferent_access,
|
52
|
+
payload: payload
|
53
|
+
)
|
54
|
+
|
55
|
+
{
|
56
|
+
url: url,
|
57
|
+
headers: headers
|
58
|
+
}.with_indifferent_access
|
59
|
+
end
|
60
|
+
|
61
|
+
def iam_external_id
|
62
|
+
settings[:aws_external_id] || DUMMY_AWS_IAM_EXTERNAL_ID
|
63
|
+
end
|
64
|
+
|
65
|
+
def workato_account_id
|
66
|
+
settings[:aws_workato_account_id] || AMAZON_ROLE_CLIENT_ID || DUMMY_AWS_WORKATO_ACCOUNT_ID
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def on_settings_updated
|
72
|
+
Workato::Connector::Sdk::Operation.on_settings_updated
|
73
|
+
end
|
74
|
+
|
75
|
+
def role_based_auth(settings:)
|
76
|
+
settings[:aws_external_id] ||= iam_external_id
|
77
|
+
temp_credentials = settings[:temp_credentials] || {}
|
78
|
+
|
79
|
+
# Refresh temp token that will expire within 60 seconds.
|
80
|
+
expiration = temp_credentials[:expiration]&.to_time(:utc)
|
81
|
+
if !expiration || expiration <= TEMP_CREDENTIALS_REFRESH_TIMEOUT.seconds.from_now
|
82
|
+
temp_credentials = refresh_temp_credentials(settings)
|
83
|
+
end
|
84
|
+
{
|
85
|
+
access_key_id: temp_credentials[:api_key],
|
86
|
+
secret_access_key: temp_credentials[:secret_key],
|
87
|
+
session_token: temp_credentials[:session_token]
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def refresh_temp_credentials(settings)
|
92
|
+
sts_credentials = {
|
93
|
+
access_key_id: amazon_role_client_key(settings),
|
94
|
+
secret_access_key: amazon_role_client_secret(settings)
|
95
|
+
}
|
96
|
+
|
97
|
+
sts_params = {
|
98
|
+
'Version' => '2011-06-15',
|
99
|
+
'Action' => 'AssumeRole',
|
100
|
+
'RoleSessionName' => 'workato',
|
101
|
+
'RoleArn' => settings[:aws_assume_role],
|
102
|
+
'ExternalId' => settings[:aws_external_id].presence
|
103
|
+
}.compact
|
104
|
+
|
105
|
+
sts_auth_url, sts_auth_headers = create_signature(
|
106
|
+
credentials: sts_credentials,
|
107
|
+
params: sts_params,
|
108
|
+
service: 'sts',
|
109
|
+
host: 'sts.amazonaws.com',
|
110
|
+
region: 'us-east-1',
|
111
|
+
headers: {
|
112
|
+
'Accept' => 'application/xml',
|
113
|
+
'Content-Type' => WWW_FORM_CONTENT_TYPE
|
114
|
+
}
|
115
|
+
)
|
116
|
+
|
117
|
+
request_temp_credentials(url: sts_auth_url, headers: sts_auth_headers).tap do |temp_credentials|
|
118
|
+
update_settings(settings, temp_credentials)
|
119
|
+
end
|
120
|
+
rescue StandardError => e
|
121
|
+
raise e if settings[:aws_external_id].blank?
|
122
|
+
|
123
|
+
settings[:aws_external_id] = nil
|
124
|
+
retry
|
125
|
+
end
|
126
|
+
|
127
|
+
def update_settings(settings, temp_credentials)
|
128
|
+
settings.merge!(temp_credentials: temp_credentials)
|
129
|
+
Workato::Connector::Sdk::Operation.on_settings_updated&.call(
|
130
|
+
'Refresh AWS temporary credentials',
|
131
|
+
settings
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
def request_temp_credentials(url:, headers:)
|
136
|
+
response = RestClient::Request.execute(
|
137
|
+
url: url,
|
138
|
+
headers: headers,
|
139
|
+
method: :get
|
140
|
+
)
|
141
|
+
response = Workato::Connector::Sdk::Xml.parse_xml_to_hash(response.body)
|
142
|
+
|
143
|
+
temp_credentials = response.dig('AssumeRoleResponse', 0, 'AssumeRoleResult', 0, 'Credentials', 0)
|
144
|
+
{
|
145
|
+
session_token: temp_credentials.dig('SessionToken', 0, 'content!'),
|
146
|
+
api_key: temp_credentials.dig('AccessKeyId', 0, 'content!'),
|
147
|
+
secret_key: temp_credentials.dig('SecretAccessKey', 0, 'content!'),
|
148
|
+
expiration: temp_credentials.dig('Expiration', 0, 'content!')
|
149
|
+
}
|
150
|
+
end
|
151
|
+
|
152
|
+
def create_signature(credentials:,
|
153
|
+
service:,
|
154
|
+
host:,
|
155
|
+
region:,
|
156
|
+
path: '/',
|
157
|
+
method: 'GET',
|
158
|
+
params: {},
|
159
|
+
headers: {},
|
160
|
+
payload: '')
|
161
|
+
url = URI::HTTPS.build(host: host, path: path, query: params.presence.to_param&.gsub('+', '%20')).to_s
|
162
|
+
signer_options = {
|
163
|
+
service: service,
|
164
|
+
region: region,
|
165
|
+
access_key_id: amazon_role_client_key(credentials),
|
166
|
+
secret_access_key: amazon_role_client_secret(credentials),
|
167
|
+
session_token: credentials[:session_token]
|
168
|
+
}
|
169
|
+
|
170
|
+
apply_service_specific_options(service, headers, signer_options, payload)
|
171
|
+
|
172
|
+
signer = Aws::Sigv4::Signer.new(signer_options)
|
173
|
+
signature = signer.sign_request(http_method: method, url: url, headers: headers, body: payload)
|
174
|
+
|
175
|
+
headers_with_sig = merge_headers_with_sig_headers(headers, signature.headers)
|
176
|
+
headers_with_sig = headers_with_sig.transform_keys { |key| key.gsub(/\b[a-z]/, &:upcase) }
|
177
|
+
[url, headers_with_sig]
|
178
|
+
end
|
179
|
+
|
180
|
+
def apply_service_specific_options(service, headers, signer_options, payload)
|
181
|
+
accept_headers = headers.key?('Accept') || headers.key?('accept')
|
182
|
+
content_type = headers.key?('content-type') || headers.key?('Content-Type')
|
183
|
+
|
184
|
+
case service
|
185
|
+
when 'ec2'
|
186
|
+
signer_options[:apply_checksum_header] = false
|
187
|
+
|
188
|
+
headers.except!('Accept', 'Content-Type')
|
189
|
+
when 's3'
|
190
|
+
signer_options[:uri_escape_path] = false
|
191
|
+
|
192
|
+
headers['Accept'] = 'application/xml' unless accept_headers
|
193
|
+
headers['Content-Type'] = WWW_FORM_CONTENT_TYPE unless content_type
|
194
|
+
headers['X-Amz-Content-SHA256'] = 'UNSIGNED-PAYLOAD' if payload.blank?
|
195
|
+
when 'monitoring'
|
196
|
+
signer_options[:apply_checksum_header] = false
|
197
|
+
|
198
|
+
headers['Accept'] = 'application/json' unless accept_headers
|
199
|
+
headers['Content-Type'] = WWW_FORM_CONTENT_TYPE unless content_type
|
200
|
+
when 'lambda'
|
201
|
+
signer_options[:apply_checksum_header] = false
|
202
|
+
|
203
|
+
headers['Content-Type'] = WWW_FORM_CONTENT_TYPE unless content_type
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def merge_headers_with_sig_headers(headers, sig_headers)
|
208
|
+
headers_keys = headers.transform_keys { |key| key.to_s.downcase }
|
209
|
+
sig_headers_to_merge = sig_headers.reject { |key| headers_keys.include?(key.downcase) }
|
210
|
+
headers.merge(sig_headers_to_merge)
|
211
|
+
end
|
212
|
+
|
213
|
+
def amazon_role_client_key(settings)
|
214
|
+
settings[:access_key_id] || AMAZON_ROLE_CLIENT_KEY
|
215
|
+
end
|
216
|
+
|
217
|
+
def amazon_role_client_secret(settings)
|
218
|
+
settings[:secret_access_key] || AMAZON_ROLE_CLIENT_SECRET
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|