workato-connector-sdk 0.3.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b958407bc907daf631e2bd91ecb5c002f911906e3bf8a88c00aedd612fe80bbe
4
- data.tar.gz: 1445969d82d648667e1de62809722625d06e40a30bb4bec565f56f1b7ce113da
3
+ metadata.gz: 0d849d4b677b749ece945ac694dc692e30ec1dee7d78c70e601a5e3b54d8c9ab
4
+ data.tar.gz: 17b87e09bee6d75ca84e8a8332effacb20350056df7995ccbce4f71f41679cc3
5
5
  SHA512:
6
- metadata.gz: 4ad8b4ef4ed273ad65573acb5a4e45dd1e0772b24a4de82cc4c40a3fe0a926c8f93f067eab0df2a6f23b0f94317f2d4bcaad588d203f9323417d7a973084a1fe
7
- data.tar.gz: 879463eea7f3e0b9ef01ce1a5019eafbc8af18610306541ff97f6b62520869e0db8d1de1c0a61b9bb3c233c0932dc780e1ec04fd61704350ece80e9fbf9c764d
6
+ metadata.gz: 0c89fc0971c2a247151bfdb08554e7479b1bb0a40e49d5da92c131ac698cdd2a6d0b7dded9d762d2fe853596e6a02a1986bcbe5127a72381d462fb9cc224caac
7
+ data.tar.gz: 53a8a4acbe3b23a46676327c87868682faf952a5d49ed2adb46f4b9193f7b55d5c45b1bde5d9a443ff043aa7402b5a5b4a981486e5b998d1b446b864bbc70eec
@@ -85,10 +85,10 @@ module Workato
85
85
  )
86
86
  @settings = settings_store.read
87
87
 
88
- Workato::Connector::Sdk::Operation.on_settings_updated = lambda do |_, _, exception, new_settings|
88
+ Workato::Connector::Sdk::Operation.on_settings_updated = lambda do |message, new_settings|
89
89
  $stdout.pause if verbose?
90
90
  say('')
91
- say("Refresh token triggered on response \"#{exception}\"")
91
+ say(message)
92
92
  loop do
93
93
  answer = ask('Updated settings file with new connection attributes? (Yes or No)').to_s.downcase
94
94
  break if %w[n no].include?(answer)
@@ -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
@@ -6,7 +6,7 @@ module Workato
6
6
  module Dsl
7
7
  module Error
8
8
  def error(message)
9
- raise message
9
+ raise Sdk::RuntimeError, message
10
10
  end
11
11
  end
12
12
  end
@@ -6,6 +6,10 @@ module Workato
6
6
  module Dsl
7
7
  # https://docs.workato.com/developing-connectors/sdk/sdk-reference/http.html#http-methods
8
8
  module HTTP
9
+ PARALLEL_SUCCESS_INDEX = 0
10
+ PARALLEL_RESULTS_INDEX = 1
11
+ PARALLEL_ERRORS_INDEX = 2
12
+
9
13
  def get(url, params = {})
10
14
  http_request(url, method: 'GET').params(params).response_format_json
11
15
  end
@@ -42,6 +46,21 @@ module Workato
42
46
  http_request(url, method: 'MOVE').payload(payload).format_json
43
47
  end
44
48
 
49
+ def parallel(requests = [], threads: 1, rpm: nil, requests_per_period: nil, period: 1.minute.to_i) # rubocop:disable Lint/UnusedMethodArgument
50
+ requests.each.with_object([true, [], []]) do |request, result|
51
+ response = nil
52
+ exception = nil
53
+ begin
54
+ response = request.execute!
55
+ rescue RequestError, RuntimeError => e
56
+ exception = e.to_s
57
+ end
58
+ result[PARALLEL_SUCCESS_INDEX] &&= exception.nil?
59
+ result[PARALLEL_RESULTS_INDEX] << response
60
+ result[PARALLEL_ERRORS_INDEX] << exception
61
+ end
62
+ end
63
+
45
64
  private
46
65
 
47
66
  def http_request(url, method:)
@@ -10,6 +10,8 @@ module Workato
10
10
  JWT_ALGORITHMS = %w[RS256 RS384 RS512].freeze
11
11
  JWT_RSA_KEY_MIN_LENGTH = 2048
12
12
 
13
+ VERIFY_RCA_ALGORITHMS = %w[SHA SHA1 SHA224 SHA256 SHA384 SHA512].freeze
14
+
13
15
  def workato
14
16
  WorkatoCodeLib
15
17
  end
@@ -38,6 +40,21 @@ module Workato
38
40
  ::JWT.encode(payload, rsa_private, algorithm, header_fields)
39
41
  end
40
42
 
43
+ def verify_rsa(payload, certificate, signature, algorithm = 'SHA256')
44
+ algorithm = algorithm.to_s.upcase
45
+ unless VERIFY_RCA_ALGORITHMS.include?(algorithm)
46
+ raise "Unsupported signing method. Supports only #{VERIFY_RCA_ALGORITHMS.join(', ')}. Got: '#{algorithm}'" # rubocop:disable Layout/LineLength
47
+ end
48
+
49
+ cert = OpenSSL::X509::Certificate.new(certificate)
50
+ digest = OpenSSL::Digest.new(algorithm)
51
+ cert.public_key.verify(digest, signature, payload)
52
+ rescue OpenSSL::PKey::PKeyError
53
+ raise 'An error occurred during signature verification. Check arguments'
54
+ rescue OpenSSL::X509::CertificateError
55
+ raise 'Invalid certificate format'
56
+ end
57
+
41
58
  def parse_yaml(yaml)
42
59
  ::Psych.safe_load(yaml)
43
60
  rescue ::Psych::DisallowedClass => e
@@ -10,6 +10,7 @@ require_relative './dsl/lookup_table'
10
10
  require_relative './dsl/workato_code_lib'
11
11
  require_relative './dsl/workato_schema'
12
12
  require_relative './dsl/time'
13
+ require_relative './dsl/aws'
13
14
 
14
15
  module Workato
15
16
  module Connector
@@ -21,6 +22,7 @@ module Workato
21
22
  include LookupTable
22
23
  include WorkatoCodeLib
23
24
  include WorkatoSchema
25
+ include AWS
24
26
 
25
27
  def sleep(seconds)
26
28
  ::Kernel.sleep(seconds.presence || 0)
@@ -9,6 +9,8 @@ module Workato
9
9
 
10
10
  CustomRequestError = Class.new(StandardError)
11
11
 
12
+ RuntimeError = Class.new(StandardError)
13
+
12
14
  class RequestError < StandardError
13
15
  attr_reader :method,
14
16
  :code,
@@ -22,7 +24,7 @@ module Workato
22
24
  end
23
25
  end
24
26
 
25
- class NotImplementedError < RuntimeError
27
+ class NotImplementedError < StandardError
26
28
  def initialize(msg = 'This part of Connector SDK is not implemented in workato-connector-sdk yet')
27
29
  super
28
30
  end
@@ -81,7 +81,7 @@ module Workato
81
81
 
82
82
  settings.merge!(new_settings)
83
83
 
84
- on_settings_updated&.call(http_body, http_code, exception, settings)
84
+ on_settings_updated&.call("Refresh token triggered on response \"#{exception}\"", settings)
85
85
 
86
86
  settings
87
87
  end
@@ -3,7 +3,7 @@
3
3
  module Workato
4
4
  module Connector
5
5
  module Sdk
6
- VERSION = '0.3.0'
6
+ VERSION = '0.4.0'
7
7
  end
8
8
  end
9
9
  end
@@ -117,6 +117,10 @@ module Workato
117
117
  ::ERB::Util.url_encode(self)
118
118
  end
119
119
 
120
+ def decode_url
121
+ CGI.unescape(self)
122
+ end
123
+
120
124
  def decode_urlsafe_base64
121
125
  Extension::Binary.new(Base64.urlsafe_decode64(self))
122
126
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: workato-connector-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pavel Abolmasov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-14 00:00:00.000000000 Z
11
+ date: 2022-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '5.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sigv4
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.2.4
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 1.2.4
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: countries
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -374,6 +388,7 @@ files:
374
388
  - lib/workato/connector/sdk/connector.rb
375
389
  - lib/workato/connector/sdk/dsl.rb
376
390
  - lib/workato/connector/sdk/dsl/account_property.rb
391
+ - lib/workato/connector/sdk/dsl/aws.rb
377
392
  - lib/workato/connector/sdk/dsl/call.rb
378
393
  - lib/workato/connector/sdk/dsl/error.rb
379
394
  - lib/workato/connector/sdk/dsl/http.rb