workato-connector-sdk 0.1.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|