workato-connector-sdk 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: b81c1b9e954ad72d48fdf3a32fa33cb3de02a3115d00e14868226234c98f32ee
4
- data.tar.gz: 22b5572360e2dfd03938acc98857fdd9c8e0bb9d097af83aaf871404bb3312ff
2
+ SHA1:
3
+ metadata.gz: 1b501416ca8344b57fb05ebc9b48b3f1b19a4f4e
4
+ data.tar.gz: 47666c7e297ead213ca1228c38d3ed15dfbd699d
5
5
  SHA512:
6
- metadata.gz: d9d2044bf8f7959198c2a5ee791661dd4be30a5474b74dc6c40ea8b359b99e8d1acfd965b44727c27194e5a8efab8f3d50f6703370ef0a4a00868034d1ca80d9
7
- data.tar.gz: a40cbb8272f22247b29be7b15be2708f1339329e771fd2a994ac9e174ddb67c2bba0e0a0474fe5a68fcd457220d884b0e56f50321b9611f436136202881d17f7
6
+ metadata.gz: 266afec14bd2e2b5dcb4104df330d9cb8f96bac11a1d8d6e9199ca3bbd0576c35ff35475ef7b661610cdeef9292d36c4091cd744cac095a8d067a42dc2ccd7da
7
+ data.tar.gz: c1c1437158f46a43ab50102ce55d2846f76310d417fc130ca8d6aae1c0e34b7d962f26687929466cb94eb1859e5ab82d596c5eab736dd08e87ff0ae2dfdeeef7
data/README.md CHANGED
@@ -39,6 +39,7 @@ Commands:
39
39
  workato generate <SUBCOMMAND> # Generates code from template
40
40
  workato help [COMMAND] # Describe available commands or one specific command
41
41
  workato new <CONNECTOR_PATH> # Inits new connector folder
42
+ workato oauth2 # Implements OAuth Authorization Code flow
42
43
  workato push # Upload and release connector's code
43
44
 
44
45
  Options:
@@ -224,6 +225,7 @@ Commands:
224
225
  workato generate <SUBCOMMAND> # Generates code from template
225
226
  workato help [COMMAND] # Describe available commands or one specific command
226
227
  workato new <CONNECTOR_PATH> # Inits new connector folder
228
+ workato oauth2 # Implements OAuth Authorization Code flow
227
229
  workato push # Upload and release connector's code
228
230
 
229
231
  Options:
@@ -259,7 +261,6 @@ Edit encrypted file, e.g. settings.yaml.enc
259
261
  ### 3.3 workato exec
260
262
  ```
261
263
  workato help exec
262
-
263
264
  Usage:
264
265
  workato exec <PATH>
265
266
 
@@ -270,6 +271,7 @@ Options:
270
271
  -k, [--key=KEY] # Path to file with encrypt/decrypt key. NOTE: key from WORKATO_CONNECTOR_MASTER_KEY has higher priority
271
272
  -i, [--input=INPUT] # Path to file with input JSON
272
273
  [--closure=CLOSURE] # Path to file with next poll closure JSON
274
+ [--continue=CONTINUE] # Path to file with next multistep action continue closure JSON
273
275
  -a, [--args=ARGS] # Path to file with method arguments JSON
274
276
  [--extended-input-schema=EXTENDED_INPUT_SCHEMA] # Path to file with extended input schema definition JSON
275
277
  [--extended-output-schema=EXTENDED_OUTPUT_SCHEMA] # Path to file with extended output schema definition JSON
@@ -279,8 +281,11 @@ Options:
279
281
  [--webhook-headers=WEBHOOK_HEADERS] # Path to file with webhook headers JSON
280
282
  [--webhook-url=WEBHOOK_URL] # Webhook URL for automatic webhook subscription
281
283
  -o, [--output=OUTPUT] # Write output to JSON file
282
- [--debug], [--no-debug]
283
- [--verbose], [--no-verbose]
284
+ [--oauth2-code=OAUTH2_CODE] # OAuth2 code exchange to tokens pair
285
+ [--redirect-url=REDIRECT_URL] # OAuth2 callback url
286
+ [--refresh-token=REFRESH_TOKEN] # OAuth2 refresh token
287
+ [--debug], [--no-debug]
288
+ [--verbose], [--no-verbose]
284
289
 
285
290
  Description:
286
291
  The 'workato exec' executes connector's lambda block at <PATH>. Lambda's parameters can be provided if needed, see options part.
@@ -358,7 +363,31 @@ Please select default HTTP mocking behavior suitable for your project?
358
363
 
359
364
  - `simple` means your HTTP requests will be stored in plain text.
360
365
 
361
- ### 3.6 workato push
366
+ ### 3.6 workato oauth2
367
+ ```
368
+ workato help oauth2
369
+
370
+ Usage:
371
+ workato oauth2
372
+
373
+ Options:
374
+ -c, [--connector=CONNECTOR] # Path to connector source code
375
+ -s, [--settings=SETTINGS] # Path to plain or encrypted file with connection configs, passwords, tokens, secrets etc
376
+ -n, [--connection=CONNECTION] # Connection name if settings file contains multiple settings
377
+ -k, [--key=KEY] # Path to file with encrypt/decrypt key. NOTE: key from WORKATO_CONNECTOR_MASTER_KEY has higher priority
378
+ [--port=PORT] # Listen requests on specific port
379
+ # Default: 45555
380
+ [--ip=IP] # Listen requests on specific interface
381
+ # Default: 127.0.0.1
382
+ [--https], [--no-https] # Start HTTPS server using self-signed certificate
383
+ [--verbose], [--no-verbose]
384
+
385
+ Implements OAuth Authorization Code flow
386
+ ```
387
+
388
+ Use this to implement the OAuth2 Authorization code grant flow for applicable connectors. Applicable connectors are ones where the connection hash has `type: 'oauth2`. For more information, check out our guide on our [main docs site](https://docs.workato.com/developing-connectors/sdk/guides/authentication/oauth/auth-code.html#how-to-guide-oauth-2-0-authorization-code-variant).
389
+
390
+ ### 3.7 workato push
362
391
  ```
363
392
  workato help push
364
393
 
@@ -490,7 +519,7 @@ workato exec test #Output of the test: lambda function should be shown
490
519
  > *Note*: You may also see a intermediary command from the Gem asking if you'd like to refresh your access tokens. This is done when HTTP requests are made which have a response that triggers the `refresh_on` block. Selecting yes would cause the Gem to update your settings file with the latest auth credentials.
491
520
 
492
521
  ### 4.3 Example: Testing your connection on CLI - OAuth 2 - auth code grant flows
493
- For auth code grant flows, the Workato Gem doesn't simulate the browser popup so you'll need to supply the access token and refresh tokens directly!
522
+ For auth code grant flows, the Workato Gem allows you to simulate the OAuth2 flow using the `workato oauth2` command.
494
523
 
495
524
  ```ruby
496
525
  {
@@ -574,27 +603,24 @@ For auth code grant flows, the Workato Gem doesn't simulate the browser popup so
574
603
  and a `settings.yaml.enc` or `settings.yaml` file with the following details
575
604
 
576
605
  ```yaml
577
- My Valid Connection:
578
- access_token: "valid_access_token"
579
- refresh_token: "valid_refresh_token"
580
- user_key: "valid_user_key"
581
- client_id: "valid_client_id"
582
- client_secret: "valid_client_secret"
583
- Invalid Access Token:
584
- access_token: "invalid_access_token"
585
- refresh_token: "valid_refresh_token"
586
- user_key: "valid_user_key"
587
- client_id: "valid_client_id"
588
- client_secret: "valid_client_secret"
606
+ client_id: valid_client_id
607
+ client_secret: valid_client_secret
589
608
  ```
590
609
 
591
- You can now run the following commands to verify that the `test:` lambda function you have defined is working:
610
+ You can now run the following commands to go through the OAuth2 Authorization code flow which includes a browser popup.
592
611
  ```bash
593
- workato exec test --connection='My Valid Connection' #Output of the test: lambda function should be shown
594
- workato exec test --connection='Invalid Access Token' --verbose #You should see a see a set of request where the test is tried, access token receives 401 and the sequence to refresh the gem using the refresh key is used! This allows you to test how your connector refreshes a connection.
612
+ workato oauth2
595
613
  ```
596
614
 
597
- > *Note*: `--verbose` can be used to details everything, including the HTTP requests. You may also see a intermediary command from the Gem asking if you'd like to refresh your access tokens. This is done when HTTP requests are made which have a response that triggers the `refresh_on` block. Selecting yes would cause the Gem to update your settings file with the latest auth credentials.
615
+ https://user-images.githubusercontent.com/25265275/137942408-812fa6ad-353f-4ea2-bf37-f804e2ff7b04.mov
616
+
617
+
618
+ > *Note*: `--verbose` can be used to detail everything, including the HTTP requests.
619
+
620
+
621
+ Now after you've successfully gone through the flow, you may be use the same `workato exec test` command to verify you're applying your token properly in your requests! Depending on when you received your token, you may also see a intermediary command from the Gem asking if you'd like to refresh your access tokens (if it has expired). This is done when HTTP requests are made which have a response that triggers the `refresh_on` block. Selecting "Yes" would cause the Gem to update your settings file with the latest auth credentials.
622
+
623
+ Take note, you may also use `workato exec` to execute lambdas in your `authorization` hash like `acquire` and `refresh`. **That said, we highlight recommend you use `workato exec test` and `workato oauth2` which handle the updating of your `settings.yaml` file automatically.**
598
624
 
599
625
  ### Example: Testing a sample action on CLI
600
626
  Continuing from the previous example, let's take a look at a simple action and invoke the individual lambda functions.
@@ -633,7 +659,7 @@ Continuing from the previous example, let's take a look at a simple action and i
633
659
  end,
634
660
 
635
661
 
636
- execute: lambda do |connection, input, input_schema, output_schema|
662
+ execute: lambda do |connection, input, input_schema, output_schema, closure|
637
663
  get("/api/v2/customers",input)
638
664
  end,
639
665
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
- require 'ruby-progressbar'
5
4
 
6
5
  module Workato
7
6
  module CLI
@@ -55,10 +54,14 @@ module Workato
55
54
  extended_output_schema: from_json(options[:extended_output_schema]).presence || [],
56
55
  config_fields: from_json(options[:config_fields]),
57
56
  closure: from_json(options[:closure], parse_json_times: true).presence,
57
+ continue: from_json(options[:continue], parse_json_times: true),
58
58
  payload: from_json(options[:webhook_payload]),
59
59
  params: from_json(options[:webhook_params]),
60
60
  headers: from_json(options[:webhook_headers]),
61
61
  webhook_url: options[:webhook_url],
62
+ oauth2_code: options[:oauth2_code],
63
+ redirect_url: options[:redirect_url],
64
+ refresh_token: options[:refresh_token],
62
65
  recipe_id: SecureRandom.uuid
63
66
  }
64
67
  end
@@ -149,6 +152,8 @@ module Workato
149
152
  def with_progress
150
153
  return yield unless verbose?
151
154
 
155
+ require 'ruby-progressbar'
156
+
152
157
  say('')
153
158
 
154
159
  old_stdout = $stdout
@@ -6,6 +6,7 @@ require_relative './exec_command'
6
6
  require_relative './edit_command'
7
7
  require_relative './generate_command'
8
8
  require_relative './push_command'
9
+ require_relative './oauth2_command'
9
10
  require_relative './generators/connector_generator'
10
11
  require_relative './generators/master_key_generator'
11
12
 
@@ -42,6 +43,7 @@ module Workato
42
43
  "NOTE: key from #{Workato::Connector::Sdk::DEFAULT_MASTER_KEY_ENV} has higher priority"
43
44
  method_option :input, type: :string, aliases: '-i', desc: 'Path to file with input JSON'
44
45
  method_option :closure, type: :string, desc: 'Path to file with next poll closure JSON'
46
+ method_option :continue, type: :string, desc: 'Path to file with next multistep action continue closure JSON'
45
47
  method_option :args, type: :string, aliases: '-a', desc: 'Path to file with method arguments JSON'
46
48
  method_option :extended_input_schema, type: :string,
47
49
  desc: 'Path to file with extended input schema definition JSON'
@@ -53,6 +55,9 @@ module Workato
53
55
  method_option :webhook_headers, type: :string, desc: 'Path to file with webhook headers JSON'
54
56
  method_option :webhook_url, type: :string, desc: 'Webhook URL for automatic webhook subscription'
55
57
  method_option :output, type: :string, aliases: '-o', desc: 'Write output to JSON file'
58
+ method_option :oauth2_code, type: :string, desc: 'OAuth2 code exchange to tokens pair'
59
+ method_option :redirect_url, type: :string, desc: 'OAuth2 callback url'
60
+ method_option :refresh_token, type: :string, desc: 'OAuth2 refresh token'
56
61
 
57
62
  method_option :debug, type: :boolean
58
63
 
@@ -139,6 +144,46 @@ module Workato
139
144
  options: options
140
145
  ).call
141
146
  end
147
+
148
+ desc 'oauth2', 'Implements OAuth Authorization Code flow'
149
+
150
+ method_option :connector,
151
+ type: :string,
152
+ aliases: '-c',
153
+ desc: 'Path to connector source code',
154
+ lazy_default: Workato::Connector::Sdk::DEFAULT_CONNECTOR_PATH
155
+ method_option :settings,
156
+ type: :string,
157
+ aliases: '-s',
158
+ desc: 'Path to plain or encrypted file with connection configs, passwords, tokens, secrets etc',
159
+ lazy_default: Workato::Connector::Sdk::DEFAULT_ENCRYPTED_SETTINGS_PATH
160
+ method_option :connection,
161
+ type: :string,
162
+ aliases: '-n',
163
+ desc: 'Connection name if settings file contains multiple settings'
164
+ method_option :key,
165
+ type: :string,
166
+ aliases: '-k',
167
+ lazy_default: Workato::Connector::Sdk::DEFAULT_MASTER_KEY_PATH,
168
+ desc: 'Path to file with encrypt/decrypt key. '\
169
+ "NOTE: key from #{Workato::Connector::Sdk::DEFAULT_MASTER_KEY_ENV} has higher priority"
170
+ method_option :port,
171
+ type: :string,
172
+ desc: 'Listen requests on specific port',
173
+ default: Workato::CLI::OAuth2Command::DEFAULT_PORT
174
+ method_option :ip,
175
+ type: :string,
176
+ desc: 'Listen requests on specific interface',
177
+ default: Workato::CLI::OAuth2Command::DEFAULT_ADDRESS
178
+ method_option :https,
179
+ type: :boolean,
180
+ desc: 'Start HTTPS server using self-signed certificate'
181
+
182
+ def oauth2
183
+ OAuth2Command.new(
184
+ options: options
185
+ ).call
186
+ end
142
187
  end
143
188
  end
144
189
  end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'workato/web/app'
4
+
5
+ module Workato
6
+ module CLI
7
+ class OAuth2Command
8
+ include Thor::Shell
9
+
10
+ AWAIT_CODE_TIMEOUT_INTERVAL = 180 # seconds
11
+ AWAIT_CODE_SLEEP_INTERVAL = 5 # seconds
12
+
13
+ DEFAULT_ADDRESS = '127.0.0.1'
14
+ DEFAULT_PORT = '45555'
15
+
16
+ def initialize(options: {})
17
+ @options = options
18
+ @port = options[:port] || DEFAULT_PORT
19
+ @https = options[:https].is_true?
20
+ @base_url = "#{https ? 'https' : 'http'}://localhost:#{port}"
21
+ @redirect_url = "#{base_url}#{Workato::Web::App::CALLBACK_PATH}"
22
+ end
23
+
24
+ def call
25
+ require_gems
26
+ start_webrick
27
+
28
+ say 'Local server is running. Allow following redirect_url in your OAuth2 provider:'
29
+ say "\n"
30
+ say redirect_url
31
+ say ''
32
+
33
+ say_status :success, "Open #{authorize_url} in browser"
34
+ Launchy.open(authorize_url) do |exception|
35
+ raise(Error, "Attempted to open #{authorize_url} and failed because #{exception}")
36
+ end
37
+
38
+ code = await_code
39
+ say_status :success, "Receive OAuth2 code=#{code}"
40
+
41
+ tokens = acquire_token(code)
42
+ say_status :success, 'Receive OAuth2 tokens'
43
+ jj tokens if verbose?
44
+
45
+ settings_store.update(tokens)
46
+ say_status :success, 'Update settings file'
47
+ rescue Timeout::Error
48
+ say "Have not received callback from OAuth2 provider in #{AWAIT_CODE_TIMEOUT_INTERVAL} seconds. Aborting!"
49
+ rescue Errno::EADDRINUSE
50
+ say "Port #{port} already in use. Try to use different port with --port=#{rand(10_000..65_664)}"
51
+ rescue StandardError => e
52
+ say e.message
53
+ ensure
54
+ stop_webrick
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :https,
60
+ :base_url,
61
+ :redirect_url,
62
+ :port,
63
+ :options
64
+
65
+ def verbose?
66
+ !!options[:verbose]
67
+ end
68
+
69
+ def require_gems
70
+ require 'oauth2'
71
+ require 'launchy'
72
+ require 'rack'
73
+ end
74
+
75
+ def start_webrick
76
+ @thread = Thread.start do
77
+ Rack::Handler::WEBrick.run(
78
+ Workato::Web::App.new,
79
+ {
80
+ Port: port,
81
+ BindAddress: options[:ip] || DEFAULT_ADDRESS,
82
+ SSLEnable: https,
83
+ SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE,
84
+ SSLCertName: [%w[CN localhost]]
85
+ }.tap do |server_options|
86
+ unless verbose?
87
+ server_options[:AccessLog] = []
88
+ server_options[:Logger] = WEBrick::Log.new($stderr, 0)
89
+ end
90
+ end
91
+ )
92
+ end
93
+ @thread.abort_on_exception = true
94
+ end
95
+
96
+ def stop_webrick
97
+ Rack::Handler::WEBrick.shutdown
98
+ @thread.exit
99
+ end
100
+
101
+ def client
102
+ @client ||= OAuth2::Client.new(
103
+ connector.connection.authorization.client_id(settings),
104
+ connector.connection.authorization.client_secret(settings),
105
+ site: connector.connection.base_uri(settings),
106
+ token_url: connector.connection.authorization.token_url(settings),
107
+ redirect_uri: redirect_url
108
+ )
109
+ end
110
+
111
+ def authorize_url
112
+ return @authorize_url if defined?(@authorize_url)
113
+
114
+ @authorize_url =
115
+ if (authorization_url = connector.connection.authorization.authorization_url(settings))
116
+ params = {
117
+ client_id: connector.connection.authorization.client_id(settings),
118
+ redirect_uri: redirect_url
119
+ }
120
+ uri = URI(authorization_url)
121
+ uri.query = params.with_indifferent_access.merge(Rack::Utils.parse_nested_query(uri.query || '')).to_param
122
+ uri.to_s
123
+ end
124
+ end
125
+
126
+ def settings_store
127
+ @settings_store ||= Workato::Connector::Sdk::Settings.new(
128
+ path: options[:settings],
129
+ name: options[:connection],
130
+ key_path: options[:key]
131
+ )
132
+ end
133
+
134
+ def settings
135
+ @settings ||= settings_store.read
136
+ end
137
+
138
+ def connector
139
+ @connector ||= Workato::Connector::Sdk::Connector.from_file(
140
+ options[:connector] || Workato::Connector::Sdk::DEFAULT_CONNECTOR_PATH
141
+ )
142
+ end
143
+
144
+ def await_code
145
+ code_uri = URI("#{base_url}#{Workato::Web::App::CODE_PATH}")
146
+
147
+ Timeout.timeout(AWAIT_CODE_TIMEOUT_INTERVAL) do
148
+ loop do
149
+ response = get(code_uri) rescue nil
150
+ break response if response.present?
151
+
152
+ sleep(AWAIT_CODE_SLEEP_INTERVAL)
153
+ end
154
+ end
155
+ end
156
+
157
+ def acquire_token(code)
158
+ if connector.source.dig(:connection, :authorization, :acquire)
159
+ tokens, _, extra_settings = connector.connection.authorization.acquire(settings, await_code, redirect_url)
160
+ tokens ||= {}
161
+ extra_settings ||= {}
162
+ extra_settings.merge(tokens)
163
+ else
164
+ client.auth_code.get_token(code).to_hash
165
+ end
166
+ end
167
+
168
+ def get(uri)
169
+ http = Net::HTTP.new(uri.host, uri.port)
170
+ if https
171
+ http.use_ssl = true
172
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
173
+ end
174
+
175
+ request = Net::HTTP::Get.new(uri.request_uri)
176
+
177
+ response = http.request(request)
178
+ response.body
179
+ end
180
+ end
181
+ end
182
+ end
@@ -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 = [], &block)
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
- super(settings, input, extended_input_schema, extended_output_schema, &(block || action[:execute]))
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
 
@@ -38,11 +62,16 @@ module Workato
38
62
  end
39
63
 
40
64
  def checkpoint!(continue:, temp_output: nil)
41
- raise NotImplementedError
65
+ # no-op
42
66
  end
43
67
 
44
68
  def reinvoke_after(seconds:, continue:, temp_output: nil)
45
- raise NotImplementedError
69
+ @reinvokes_remaining = (@reinvokes_remaining ? @reinvokes_remaining - 1 : reinvoke_limit)
70
+ @reinvoke_after = {
71
+ seconds: seconds,
72
+ continue: continue,
73
+ temp_output: temp_output
74
+ }
46
75
  end
47
76
 
48
77
  private
@@ -81,6 +110,14 @@ module Workato
81
110
  @retry_methods.include?(exception.method.to_s.downcase)
82
111
  end
83
112
 
113
+ def reinvoke_sleep
114
+ sleep((ENV['WAIT_REINVOKE_AFTER'].presence || @reinvoke_after[:seconds]).to_f)
115
+ end
116
+
117
+ def reinvoke_limit
118
+ @reinvoke_limit ||= (ENV['MAX_REINVOKES'].presence || MAX_REINVOKES).to_i
119
+ end
120
+
84
121
  alias action operation
85
122
  end
86
123
  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
- connection: connection_source,
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: connection_source,
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: connection_source,
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: connection_source,
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: connection_source,
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
@@ -10,23 +10,23 @@ module Workato
10
10
 
11
11
  def initialize(object_definitions:, connection:, methods:, settings:)
12
12
  @object_definitions_source = object_definitions
13
- @methods = methods
13
+ @methods_source = methods
14
14
  @connection = connection
15
15
  @settings = settings
16
16
  define_object_definition_methods(object_definitions)
17
17
  end
18
18
 
19
19
  def lazy(settings = nil, config_fields = {})
20
- object_definitions_lazy_hash = DupHashWithIndifferentAccess.new do |h, name|
21
- fields_proc = @object_definitions_source[name][:fields]
22
- h[name] = Action.new(
20
+ DupHashWithIndifferentAccess.new do |object_definitions, name|
21
+ fields_proc = object_definitions_source[name][:fields]
22
+ object_definitions[name] = Action.new(
23
23
  action: {
24
24
  execute: lambda do |connection, input|
25
- instance_exec(connection, input, object_definitions_lazy_hash, &fields_proc)
25
+ instance_exec(connection, input, object_definitions, &fields_proc)
26
26
  end
27
27
  },
28
- methods: @methods,
29
- connection: @connection,
28
+ methods: methods_source,
29
+ connection: connection,
30
30
  settings: @settings
31
31
  ).execute(settings, config_fields)
32
32
  end
@@ -34,10 +34,10 @@ module Workato
34
34
 
35
35
  private
36
36
 
37
- attr_reader :methods,
37
+ attr_reader :methods_source,
38
38
  :connection,
39
- :objects,
40
- :settings
39
+ :settings,
40
+ :object_definitions_source
41
41
 
42
42
  def define_object_definition_methods(object_definitions)
43
43
  object_definitions.each do |(object, _definition)|
@@ -16,21 +16,23 @@ module Workato
16
16
 
17
17
  cattr_accessor :on_settings_updated
18
18
 
19
- def initialize(operation:, connection: {}, methods: {}, settings: {}, object_definitions: nil)
19
+ def initialize(connection:, operation: {}, methods: {}, settings: {}, object_definitions: nil)
20
+ @connection = connection
20
21
  @settings = settings.with_indifferent_access
21
22
  @operation = operation.with_indifferent_access
22
- @connection = connection.with_indifferent_access
23
23
  @_methods = methods.with_indifferent_access
24
24
  @object_definitions = object_definitions
25
25
  end
26
26
 
27
- def execute(settings = nil, input = {}, extended_input_schema = [], extended_output_schema = [], &block)
27
+ def execute(settings = nil, input = {}, extended_input_schema = [], extended_output_schema = [], continue = {},
28
+ &block)
28
29
  @settings = settings.with_indifferent_access if settings # is being used in request for refresh tokens
29
30
  request_or_result = instance_exec(
30
31
  @settings.with_indifferent_access, # a copy of settings hash is being used in executable blocks
31
32
  input.with_indifferent_access,
32
33
  Array.wrap(extended_input_schema).map(&:with_indifferent_access),
33
34
  Array.wrap(extended_output_schema).map(&:with_indifferent_access),
35
+ continue.with_indifferent_access,
34
36
  &block
35
37
  )
36
38
  resolve_request(request_or_result)
@@ -69,9 +71,9 @@ module Workato
69
71
  def refresh_authorization!(http_code, http_body, exception, settings = {})
70
72
  return unless refresh_auth?(http_code, http_body, exception)
71
73
 
72
- new_settings = if /oauth2/i =~ connection[:authorization][:type]
74
+ new_settings = if /oauth2/i =~ connection.authorization.type
73
75
  refresh_oauth2_token(settings)
74
- elsif connection[:authorization][:acquire]
76
+ elsif connection.authorization.acquire?
75
77
  acquire_token(settings)
76
78
  end
77
79
  return unless new_settings
@@ -132,7 +134,7 @@ module Workato
132
134
  end
133
135
 
134
136
  def refresh_auth?(http_code, http_body, exception)
135
- refresh_on = Array.wrap(connection[:authorization][:refresh_on]).compact
137
+ refresh_on = connection.authorization.refresh_on
136
138
  refresh_on.blank? || refresh_on.any? do |pattern|
137
139
  pattern.is_a?(::Integer) && pattern == http_code ||
138
140
  pattern === exception&.to_s ||
@@ -141,48 +143,26 @@ module Workato
141
143
  end
142
144
 
143
145
  def acquire_token(settings)
144
- acquire = connection[:authorization][:acquire]
145
- raise InvalidDefinitionError, "'acquire' block is required for authorization" unless acquire
146
-
147
- Action.new(
148
- action: {
149
- execute: ->(connection) { instance_exec(connection, &acquire) }
150
- },
151
- connection: connection.merge(
152
- authorization: connection[:authorization].merge(
153
- apply: nil
154
- )
155
- ),
156
- methods: @_methods
157
- ).execute(settings)
146
+ connection.authorization.acquire(settings)
158
147
  end
159
148
 
160
149
  def refresh_oauth2_token_using_refresh(settings)
161
- refresh = connection[:authorization][:refresh]
162
- new_tokens, new_settings = Action.new(
163
- action: {
164
- execute: lambda do |connection|
165
- instance_exec(connection, connection[:refresh_token], &refresh)
166
- end
167
- },
168
- methods: @_methods
169
- ).execute(settings)
170
-
150
+ new_tokens, new_settings = connection.authorization.refresh(settings, settings[:refresh_token])
171
151
  new_tokens.with_indifferent_access.merge(new_settings || {})
172
152
  end
173
153
 
174
154
  def refresh_oauth2_token_using_token_url(settings)
175
155
  if settings[:refresh_token].blank?
176
- raise NotImplementedError, 'workato-connector-sdk does not support OAuth2 authorization process. '\
177
- 'Use Workato Debugger UI to acquire access_token and refresh_token'
156
+ raise NotImplementedError, 'refresh_token is empty. ' \
157
+ 'Use workato oauth2 command to acquire access_token and refresh_token'
178
158
  end
179
159
 
180
160
  response = RestClient::Request.execute(
181
- url: connection[:authorization][:token_url].call(settings),
161
+ url: connection.authorization.token_url(settings),
182
162
  method: :post,
183
163
  payload: {
184
- client_id: connection[:authorization][:client_id].call(settings),
185
- client_secret: connection[:authorization][:client_secret].call(settings),
164
+ client_id: connection.authorization.client_id(settings),
165
+ client_secret: connection.authorization.client_secret(settings),
186
166
  grant_type: :refresh_token,
187
167
  refresh_token: settings[:refresh_token]
188
168
  },
@@ -198,9 +178,9 @@ module Workato
198
178
  end
199
179
 
200
180
  def refresh_oauth2_token(settings)
201
- if connection[:authorization][:refresh]
181
+ if connection.authorization.refresh?
202
182
  refresh_oauth2_token_using_refresh(settings)
203
- elsif connection[:authorization][:token_url]
183
+ elsif connection.authorization.token_url?
204
184
  refresh_oauth2_token_using_token_url(settings)
205
185
  else
206
186
  raise InvalidDefinitionError, "'refresh' block or 'token_url' is required for refreshing the token"
@@ -15,13 +15,13 @@ module Workato
15
15
  class Request < SimpleDelegator
16
16
  using BlockInvocationRefinements
17
17
 
18
- def initialize(uri, method: 'GET', connection: {}, settings: {}, action: nil)
18
+ def initialize(uri, method: 'GET', settings: {}, connection: nil, action: nil)
19
19
  super(nil)
20
20
  @uri = uri
21
- @authorization = (connection[:authorization] || {}).with_indifferent_access
22
- @settings = settings
23
- @base_uri = connection[:base_uri]&.call(settings.with_indifferent_access)
24
21
  @method = method
22
+ @settings = settings
23
+ @authorization = connection&.authorization
24
+ @base_uri = connection&.base_uri(settings)
25
25
  @action = action
26
26
  @headers = {}
27
27
  @case_sensitive_headers = {}
@@ -321,7 +321,9 @@ module Workato
321
321
  end
322
322
 
323
323
  def detect_error!(response)
324
- error_patterns = Array.wrap(@authorization[:detect_on])
324
+ return unless @authorization
325
+
326
+ error_patterns = @authorization.detect_on
325
327
  return unless error_patterns.any? { |pattern| pattern === response rescue false }
326
328
 
327
329
  Kernel.raise(CustomRequestError, response.to_s)
@@ -357,13 +359,15 @@ module Workato
357
359
  end
358
360
 
359
361
  def authorized
360
- apply = @authorization[:apply] || @authorization[:credentials]
362
+ return yield unless @authorization
363
+
364
+ apply = @authorization.source[:apply] || @authorization.source[:credentials]
361
365
  return yield unless apply
362
366
 
363
367
  first = true
364
368
  begin
365
369
  settings = @settings.with_indifferent_access
366
- if /oauth2/i =~ @authorization[:type]
370
+ if /oauth2/i =~ @authorization.type
367
371
  instance_exec(settings, settings[:access_token], @auth_type, &apply)
368
372
  else
369
373
  instance_exec(settings, @auth_type, &apply)
@@ -45,6 +45,8 @@ module Workato
45
45
  begin
46
46
  @encrypted = false
47
47
  read_plain_file
48
+ rescue KeyError
49
+ raise
48
50
  rescue StandardError
49
51
  @encrypted = true
50
52
  read_encrypted_file
@@ -76,7 +78,7 @@ module Workato
76
78
  YAML.safe_load(f.read, [::Symbol]).to_hash.with_indifferent_access
77
79
  end
78
80
 
79
- name ? all_settings.fetch(name) : all_settings
81
+ (name ? all_settings.fetch(name) : all_settings) || {}
80
82
  end
81
83
 
82
84
  def update_plain_file(new_settings)
@@ -93,7 +95,7 @@ module Workato
93
95
  def read_encrypted_file
94
96
  all_settings = encrypted_configuration.config.with_indifferent_access
95
97
 
96
- name ? all_settings.fetch(name) : all_settings
98
+ (name ? all_settings.fetch(name) : all_settings) || {}
97
99
  end
98
100
 
99
101
  def update_encrypted_file(new_settings)
@@ -106,7 +108,8 @@ module Workato
106
108
 
107
109
  def merge_settings(all_settings, new_settings)
108
110
  if name
109
- all_settings[name] = new_settings
111
+ all_settings[name] ||= {}
112
+ all_settings[name].merge!(new_settings)
110
113
  else
111
114
  all_settings.merge!(new_settings)
112
115
  end
@@ -3,7 +3,7 @@
3
3
  module Workato
4
4
  module Connector
5
5
  module Sdk
6
- VERSION = '0.1.2'
6
+ VERSION = '0.2.0'
7
7
  end
8
8
  end
9
9
  end
@@ -43,6 +43,7 @@ require_relative '../extension/time'
43
43
 
44
44
  require_relative './sdk/account_properties'
45
45
  require_relative './sdk/action'
46
+ require_relative './sdk/connection'
46
47
  require_relative './sdk/connector'
47
48
  require_relative './sdk/dsl'
48
49
  require_relative './sdk/errors'
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workato
4
+ module Web
5
+ class App
6
+ CODE_PATH = '/code'
7
+ CALLBACK_PATH = '/oauth/callback'
8
+
9
+ def call(env)
10
+ req = Rack::Request.new(env)
11
+ case req.path_info
12
+ when /#{CODE_PATH}/
13
+ [200, { 'Content-Type' => 'text/plain' }, [@code.to_s]]
14
+ when /#{CALLBACK_PATH}/
15
+ @code = req.params['code']
16
+ [200, { 'Content-Type' => 'text/plain' }, ['We stored response code. Now you can close the browser window']]
17
+ else
18
+ [404, { 'Content-Type' => 'text/plain' }, ['404: Not Found']]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -11,7 +11,7 @@ RSpec.describe 'actions/<%= name %>', :vcr do
11
11
  let(:action) { connector.actions.<%= name %> }
12
12
 
13
13
  describe 'execute' do
14
- subject(:output) { action.execute(settings, input, extended_input_schema, extended_output_schema) }
14
+ subject(:output) { action.execute(settings, input, extended_input_schema, extended_output_schema, continue) }
15
15
 
16
16
  pending 'add some examples'
17
17
  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.1.2
4
+ version: 0.2.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-09-28 00:00:00.000000000 Z
11
+ date: 2021-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - '='
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.4.2
55
+ - !ruby/object:Gem::Dependency
56
+ name: em-http-request
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: gyoku
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +94,20 @@ dependencies:
80
94
  - - '='
81
95
  - !ruby/object:Gem::Version
82
96
  version: 1.5.6
97
+ - !ruby/object:Gem::Dependency
98
+ name: launchy
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.0'
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: loofah
85
113
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +150,34 @@ dependencies:
122
150
  - - '='
123
151
  - !ruby/object:Gem::Version
124
152
  version: 1.10.10
153
+ - !ruby/object:Gem::Dependency
154
+ name: oauth2
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '1.0'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '1.0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rack
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '2.0'
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '2.0'
125
181
  - !ruby/object:Gem::Dependency
126
182
  name: rest-client
127
183
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +234,20 @@ dependencies:
178
234
  - - "~>"
179
235
  - !ruby/object:Gem::Version
180
236
  version: '1.0'
237
+ - !ruby/object:Gem::Dependency
238
+ name: webrick
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - "~>"
242
+ - !ruby/object:Gem::Version
243
+ version: '1.0'
244
+ type: :runtime
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - "~>"
249
+ - !ruby/object:Gem::Version
250
+ version: '1.0'
181
251
  - !ruby/object:Gem::Dependency
182
252
  name: bundler
183
253
  requirement: !ruby/object:Gem::Requirement
@@ -294,11 +364,13 @@ files:
294
364
  - lib/workato/cli/generators/connector_generator.rb
295
365
  - lib/workato/cli/generators/master_key_generator.rb
296
366
  - lib/workato/cli/main.rb
367
+ - lib/workato/cli/oauth2_command.rb
297
368
  - lib/workato/cli/push_command.rb
298
369
  - lib/workato/connector/sdk.rb
299
370
  - lib/workato/connector/sdk/account_properties.rb
300
371
  - lib/workato/connector/sdk/action.rb
301
372
  - lib/workato/connector/sdk/block_invocation_refinements.rb
373
+ - lib/workato/connector/sdk/connection.rb
302
374
  - lib/workato/connector/sdk/connector.rb
303
375
  - lib/workato/connector/sdk/dsl.rb
304
376
  - lib/workato/connector/sdk/dsl/account_property.rb
@@ -336,6 +408,7 @@ files:
336
408
  - lib/workato/extension/time.rb
337
409
  - lib/workato/testing/vcr_encrypted_cassette_serializer.rb
338
410
  - lib/workato/testing/vcr_multipart_body_matcher.rb
411
+ - lib/workato/web/app.rb
339
412
  - templates/.rspec.erb
340
413
  - templates/Gemfile.erb
341
414
  - templates/connector.rb.erb
@@ -369,7 +442,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
369
442
  - !ruby/object:Gem::Version
370
443
  version: '0'
371
444
  requirements: []
372
- rubygems_version: 3.2.3
445
+ rubyforge_project:
446
+ rubygems_version: 2.6.14.4
373
447
  signing_key:
374
448
  specification_version: 4
375
449
  summary: Gem for running adapter's code outside Workato infrastructure