aspera-cli 4.24.1 → 4.25.0.pre

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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +1064 -745
  4. data/CONTRIBUTING.md +43 -100
  5. data/README.md +1281 -720
  6. data/bin/ascli +20 -1
  7. data/bin/asession +23 -27
  8. data/lib/aspera/agent/base.rb +10 -21
  9. data/lib/aspera/agent/connect.rb +2 -3
  10. data/lib/aspera/agent/desktop.rb +2 -2
  11. data/lib/aspera/agent/direct.rb +49 -32
  12. data/lib/aspera/agent/factory.rb +31 -0
  13. data/lib/aspera/api/aoc.rb +134 -76
  14. data/lib/aspera/api/cos_node.rb +3 -2
  15. data/lib/aspera/api/faspex.rb +213 -0
  16. data/lib/aspera/api/node.rb +107 -94
  17. data/lib/aspera/ascmd.rb +1 -2
  18. data/lib/aspera/ascp/installation.rb +73 -58
  19. data/lib/aspera/ascp/management.rb +119 -23
  20. data/lib/aspera/assert.rb +39 -11
  21. data/lib/aspera/cli/error.rb +4 -2
  22. data/lib/aspera/cli/extended_value.rb +91 -67
  23. data/lib/aspera/cli/formatter.rb +62 -27
  24. data/lib/aspera/cli/hints.rb +8 -0
  25. data/lib/aspera/cli/info.rb +4 -4
  26. data/lib/aspera/cli/main.rb +76 -84
  27. data/lib/aspera/cli/manager.rb +352 -248
  28. data/lib/aspera/cli/plugins/alee.rb +5 -4
  29. data/lib/aspera/cli/plugins/aoc.rb +175 -195
  30. data/lib/aspera/cli/plugins/ats.rb +4 -4
  31. data/lib/aspera/cli/plugins/base.rb +343 -0
  32. data/lib/aspera/cli/plugins/basic_auth.rb +45 -0
  33. data/lib/aspera/cli/plugins/config.rb +283 -269
  34. data/lib/aspera/cli/plugins/console.rb +27 -22
  35. data/lib/aspera/cli/plugins/cos.rb +3 -3
  36. data/lib/aspera/cli/plugins/factory.rb +78 -0
  37. data/lib/aspera/cli/plugins/faspex.rb +49 -46
  38. data/lib/aspera/cli/plugins/faspex5.rb +113 -225
  39. data/lib/aspera/cli/plugins/faspio.rb +19 -18
  40. data/lib/aspera/cli/plugins/httpgw.rb +14 -13
  41. data/lib/aspera/cli/plugins/node.rb +162 -149
  42. data/lib/aspera/cli/plugins/oauth.rb +48 -0
  43. data/lib/aspera/cli/plugins/orchestrator.rb +129 -45
  44. data/lib/aspera/cli/plugins/preview.rb +30 -50
  45. data/lib/aspera/cli/plugins/server.rb +21 -21
  46. data/lib/aspera/cli/plugins/shares.rb +45 -47
  47. data/lib/aspera/cli/sync_actions.rb +50 -39
  48. data/lib/aspera/cli/transfer_agent.rb +35 -49
  49. data/lib/aspera/cli/transfer_progress.rb +6 -6
  50. data/lib/aspera/cli/version.rb +3 -3
  51. data/lib/aspera/cli/wizard.rb +70 -55
  52. data/lib/aspera/colors.rb +6 -0
  53. data/lib/aspera/command_line_builder.rb +59 -61
  54. data/lib/aspera/command_line_converter.rb +2 -1
  55. data/lib/aspera/coverage.rb +2 -2
  56. data/lib/aspera/data_repository.rb +1 -1
  57. data/lib/aspera/environment.rb +51 -41
  58. data/lib/aspera/faspex_gw.rb +7 -5
  59. data/lib/aspera/faspex_postproc.rb +1 -1
  60. data/lib/aspera/keychain/factory.rb +1 -2
  61. data/lib/aspera/keychain/macos_security.rb +1 -1
  62. data/lib/aspera/log.rb +37 -9
  63. data/lib/aspera/markdown.rb +31 -0
  64. data/lib/aspera/nagios.rb +7 -6
  65. data/lib/aspera/oauth/base.rb +25 -28
  66. data/lib/aspera/oauth/factory.rb +9 -9
  67. data/lib/aspera/oauth/url_json.rb +2 -1
  68. data/lib/aspera/oauth/web.rb +2 -2
  69. data/lib/aspera/preview/file_types.rb +23 -37
  70. data/lib/aspera/products/connect.rb +7 -6
  71. data/lib/aspera/products/desktop.rb +1 -4
  72. data/lib/aspera/products/other.rb +9 -1
  73. data/lib/aspera/products/transferd.rb +0 -1
  74. data/lib/aspera/rest.rb +168 -113
  75. data/lib/aspera/rest_error_analyzer.rb +4 -4
  76. data/lib/aspera/ssh.rb +7 -4
  77. data/lib/aspera/ssl.rb +41 -0
  78. data/lib/aspera/sync/args.schema.yaml +46 -3
  79. data/lib/aspera/sync/conf.schema.yaml +307 -123
  80. data/lib/aspera/sync/database.rb +2 -1
  81. data/lib/aspera/sync/operations.rb +135 -79
  82. data/lib/aspera/temp_file_manager.rb +17 -5
  83. data/lib/aspera/transfer/error.rb +16 -7
  84. data/lib/aspera/transfer/parameters.rb +35 -22
  85. data/lib/aspera/transfer/resumer.rb +74 -0
  86. data/lib/aspera/transfer/spec.rb +5 -5
  87. data/lib/aspera/transfer/spec.schema.yaml +170 -59
  88. data/lib/aspera/transfer/spec_doc.rb +49 -43
  89. data/lib/aspera/uri_reader.rb +2 -2
  90. data/lib/aspera/web_auth.rb +6 -6
  91. data/lib/transferd_pb.rb +2 -2
  92. data.tar.gz.sig +0 -0
  93. metadata +26 -11
  94. metadata.gz.sig +0 -0
  95. data/lib/aspera/cli/basic_auth_plugin.rb +0 -43
  96. data/lib/aspera/cli/plugin.rb +0 -333
  97. data/lib/aspera/cli/plugin_factory.rb +0 -81
  98. data/lib/aspera/resumer.rb +0 -77
  99. data/lib/aspera/transfer/error_info.rb +0 -91
@@ -8,11 +8,10 @@ require 'aspera/data_repository'
8
8
  require 'aspera/transfer/spec'
9
9
  require 'aspera/api/node'
10
10
  require 'base64'
11
- require 'cgi'
12
11
 
13
12
  module Aspera
14
13
  module Api
15
- class AoC < Aspera::Rest
14
+ class AoC < Rest
16
15
  PRODUCT_NAME = 'Aspera on Cloud'
17
16
  # use default workspace if it is set, else none
18
17
  DEFAULT_WORKSPACE = ''
@@ -74,7 +73,7 @@ module Aspera
74
73
  # split host of URL into organization and domain
75
74
  def split_org_domain(uri)
76
75
  Aspera.assert_type(uri, URI)
77
- raise "No host found in URL.Please check URL format: https://myorg.#{SAAS_DOMAIN_PROD}" if uri.host.nil?
76
+ Aspera.assert(!uri.host.nil?){"No host found in URL. Please check URL format: https://myorg.#{SAAS_DOMAIN_PROD}"}
78
77
  parts = uri.host.split('.', 2)
79
78
  Aspera.assert(parts.length == 2){"expecting a public FQDN for #{PRODUCT_NAME}"}
80
79
  parts[0] = nil if parts[0].eql?('api')
@@ -90,7 +89,7 @@ module Aspera
90
89
  # @param url [String] URL of AoC public link
91
90
  # @return [Hash] information about public link, or nil if not a public link
92
91
  def link_info(url)
93
- final_uri = Rest.new(base_url: url, redirect_max: MAX_AOC_URL_REDIRECT).call(operation: 'GET')[:http].uri
92
+ final_uri = Rest.new(base_url: url, redirect_max: MAX_AOC_URL_REDIRECT).call(operation: 'GET', ret: :resp).uri
94
93
  Log.dump(:final_uri, final_uri, level: :trace1)
95
94
  org_domain = split_org_domain(final_uri)
96
95
  if (m = final_uri.path.match(%r{/oauth2/([^/]+)/login$}))
@@ -98,7 +97,7 @@ module Aspera
98
97
  else
99
98
  Log.log.debug{"path=#{final_uri.path} does not end with /login"}
100
99
  end
101
- raise Error, 'AoC shall redirect to login page with a query' if final_uri.query.nil?
100
+ Aspera.assert(!final_uri.query.nil?, 'AoC shall redirect to login page with a query', type: Error)
102
101
  query = Rest.query_to_h(final_uri.query)
103
102
  Log.dump(:query, query, level: :trace1)
104
103
  # is that a public link ?
@@ -140,15 +139,82 @@ module Aspera
140
139
  organization: org_domain[:organization]
141
140
  }
142
141
  end
142
+
143
+ # Call block with same query using paging and response information.
144
+ # Block must return an Array with data and http response
145
+ # @return [Hash] {items: , total: }
146
+ def call_paging(query: nil, formatter: nil)
147
+ query = {} if query.nil?
148
+ Aspera.assert_type(query, Hash){'query'}
149
+ Aspera.assert(block_given?)
150
+ # set default large page if user does not specify own parameters. AoC Caps to 1000 anyway
151
+ query['per_page'] = 1000 unless query.key?('per_page')
152
+ max_items = query.delete(Rest::MAX_ITEMS)
153
+ max_pages = query.delete(Rest::MAX_PAGES)
154
+ item_list = []
155
+ total_count = nil
156
+ current_page = query['page']
157
+ current_page = 1 if current_page.nil?
158
+ page_count = 0
159
+ loop do
160
+ new_query = query.clone
161
+ new_query['page'] = current_page
162
+ result_data, result_http = yield(new_query)
163
+ Aspera.assert(result_http)
164
+ total_count = result_http['X-Total-Count']&.to_i
165
+ page_count += 1
166
+ current_page += 1
167
+ add_items = result_data
168
+ break if add_items.nil?
169
+ break if add_items.empty?
170
+ # append new items to full list
171
+ item_list += add_items
172
+ break if !max_items.nil? && item_list.count >= max_items
173
+ break if !max_pages.nil? && page_count >= max_pages
174
+ break if total_count&.<=(item_list.count)
175
+ formatter&.long_operation_running("#{item_list.count} / #{total_count}") unless total_count.eql?(item_list.count.to_s)
176
+ end
177
+ formatter&.long_operation_terminated
178
+ item_list = item_list[0..max_items - 1] if !max_items.nil? && item_list.count > max_items
179
+ return {items: item_list, total: total_count}
180
+ end
181
+
182
+ # @param id [String] Identifier or workspace
183
+ # @return [Hash] suitable for permission filtering
184
+ def workspace_access(id)
185
+ {
186
+ 'access_type' => 'user',
187
+ 'access_id' => "#{ID_AK_ADMIN}_WS_#{id}"
188
+ }
189
+ end
190
+
191
+ # @param permission [Hash] Shared folder information
192
+ # @return [Boolean] `true` if internal access
193
+ def workspace_access?(permission)
194
+ permission['access_id'].start_with?("#{ID_AK_ADMIN}_WS_")
195
+ end
143
196
  end
144
197
 
145
198
  attr_reader :private_link
146
199
 
147
- def initialize(url:, auth:, subpath: API_V1, client_id: nil, client_secret: nil, scope: nil, redirect_uri: nil, private_key: nil, passphrase: nil, username: nil,
148
- password: nil, workspace: nil, secret_finder: nil)
149
- # test here because link may set url
150
- raise ArgumentError, 'Missing mandatory option: url' if url.nil?
151
- raise ArgumentError, 'Missing mandatory option: scope' if scope.nil?
200
+ def initialize(
201
+ url:,
202
+ auth:,
203
+ subpath: API_V1,
204
+ client_id: nil,
205
+ client_secret: nil,
206
+ scope: nil,
207
+ redirect_uri: nil,
208
+ private_key: nil,
209
+ passphrase: nil,
210
+ username: nil,
211
+ password: nil,
212
+ workspace: nil,
213
+ secret_finder: nil
214
+ )
215
+ # Test here because link may set url
216
+ Aspera.assert(url, 'Missing mandatory option: url', type: ParameterError)
217
+ Aspera.assert(scope, 'Missing mandatory option: scope', type: ParameterError)
152
218
  # default values for client id
153
219
  client_id, client_secret = self.class.get_client_info if client_id.nil?
154
220
  # access key secrets are provided out of band to get node api access
@@ -173,22 +239,21 @@ module Aspera
173
239
  auth_params[:grant_method] = if url_info.key?(:token)
174
240
  :url_json
175
241
  else
176
- raise ArgumentError, 'Missing mandatory option: auth' if auth.nil?
242
+ Aspera.assert(auth, 'Missing mandatory option: auth', type: ParameterError)
177
243
  auth
178
244
  end
179
245
  # this is the base API url
180
246
  api_url_base = self.class.api_base_url(api_domain: url_info[:instance_domain])
181
247
  # auth URL
182
248
  auth_params[:base_url] = "#{api_url_base}/#{OAUTH_API_SUBPATH}/#{url_info[:organization]}"
183
-
184
249
  # fill other auth parameters based on OAuth method
185
250
  case auth_params[:grant_method]
186
251
  when :web
187
- raise ArgumentError, 'Missing mandatory option: redirect_uri' if redirect_uri.nil?
252
+ Aspera.assert(redirect_uri, 'Missing mandatory option: redirect_uri', type: ParameterError)
188
253
  auth_params[:redirect_uri] = redirect_uri
189
254
  when :jwt
190
- raise ArgumentError, 'Missing mandatory option: private_key' if private_key.nil?
191
- raise ArgumentError, 'Missing mandatory option: username' if username.nil?
255
+ Aspera.assert(private_key, 'Missing mandatory option: private_key', type: ParameterError)
256
+ Aspera.assert(username, 'Missing mandatory option: username', type: ParameterError)
192
257
  auth_params[:private_key_obj] = OpenSSL::PKey::RSA.new(private_key, passphrase)
193
258
  auth_params[:payload] = {
194
259
  iss: auth_params[:client_id], # issuer
@@ -205,7 +270,7 @@ module Aspera
205
270
  auth_params[:json][:password] = password unless password.nil?
206
271
  # basic auth required for /token
207
272
  auth_params[:auth] = {type: :basic, username: auth_params[:client_id], password: auth_params[:client_secret]}
208
- else Aspera.error_unexpected_value(auth_params[:grant_method])
273
+ else Aspera.error_unexpected_value(auth_params[:grant_method]){'auth, use one of: :web, :jwt'}
209
274
  end
210
275
  super(
211
276
  base_url: "#{api_url_base}/#{subpath}",
@@ -213,12 +278,12 @@ module Aspera
213
278
  )
214
279
  end
215
280
 
216
- def public_link
217
- return unless auth_params[:grant_method].eql?(:url_json)
218
- return @cache_url_token_info unless @cache_url_token_info.nil?
219
- # TODO: can there be several in list ?
220
- @cache_url_token_info = read('url_tokens').first
221
- return @cache_url_token_info
281
+ # read using the query and paging
282
+ # @return [Hash] {items: , total: }
283
+ def read_with_paging(subpath, query = nil, formatter: nil)
284
+ return self.class.call_paging(query: query, formatter: formatter) do |paged_query|
285
+ read(subpath, query: paged_query, ret: :both)
286
+ end
222
287
  end
223
288
 
224
289
  def assert_public_link_types(expected)
@@ -230,7 +295,17 @@ module Aspera
230
295
  return [] # TODO : public_link['id'] ?
231
296
  end
232
297
 
233
- # cached user information
298
+ # Cached public link information
299
+ # @return [Hash, nil] token info if public link or nil
300
+ def public_link
301
+ return unless auth_params[:grant_method].eql?(:url_json)
302
+ return @cache_url_token_info unless @cache_url_token_info.nil?
303
+ # TODO: can there be several in list ?
304
+ @cache_url_token_info = read('url_tokens').first
305
+ return @cache_url_token_info
306
+ end
307
+
308
+ # Cached user information
234
309
  def current_user_info(exception: false)
235
310
  return @cache_user_info unless @cache_user_info.nil?
236
311
  # get our user's default information
@@ -246,21 +321,9 @@ module Aspera
246
321
  return @cache_user_info
247
322
  end
248
323
 
324
+ # Cached workspace information
249
325
  def workspace
250
- Aspera.assert(!@workspace_info.nil?){'AoC workspace context is not set'}
251
- @workspace_info
252
- end
253
-
254
- def home
255
- Aspera.assert(!@home_info.nil?){'AoC home context is not set'}
256
- @home_info
257
- end
258
-
259
- # Set the application context
260
- # @param application [Symbol,NilClass] :files or :packages
261
- # @return [Hash] current context information: workspace, and home node/file if app is "Files"
262
- def context=(application)
263
- Aspera.assert_values(application, %i[files packages])
326
+ return @workspace_info unless @workspace_info.nil?
264
327
  ws_id =
265
328
  if !public_link.nil?
266
329
  Log.log.debug('Using workspace of public link')
@@ -278,26 +341,21 @@ module Aspera
278
341
  else
279
342
  lookup_by_name('workspaces', @workspace_name)['id']
280
343
  end
281
- ws_info =
282
- if ws_id.nil?
283
- nil
284
- else
285
- read("workspaces/#{ws_id}")
286
- end
287
344
  @workspace_info =
288
- if ws_info.nil?
345
+ if ws_id.nil?
289
346
  {
290
- id: nil,
291
- name: "Shared #{application}"
347
+ name: 'Shared (no workspace)'
292
348
  }
293
349
  else
294
- {
295
- id: ws_info['id'],
296
- name: ws_info['name']
297
- }
350
+ read("workspaces/#{ws_id}").slice('id', 'name', 'home_node_id', 'home_file_id').symbolize_keys
298
351
  end
299
- Log.dump(:context, @workspace_info)
300
- return unless application.eql?(:files)
352
+ Log.dump(:workspace_info, @workspace_info)
353
+ @workspace_info
354
+ end
355
+
356
+ # Cached Home information for current user in Files app
357
+ def home
358
+ return @home_info unless @home_info.nil?
301
359
  @home_info =
302
360
  if !public_link.nil?
303
361
  assert_public_link_types(['view_shared_file'])
@@ -310,10 +368,10 @@ module Aspera
310
368
  node_id: private_link[:node_id],
311
369
  file_id: private_link[:file_id]
312
370
  }
313
- elsif ws_info
371
+ elsif workspace[:home_node_id] && workspace[:home_file_id]
314
372
  {
315
- node_id: ws_info['home_node_id'],
316
- file_id: ws_info['home_file_id']
373
+ node_id: workspace[:home_node_id],
374
+ file_id: workspace[:home_file_id]
317
375
  }
318
376
  else
319
377
  # not part of any workspace, but has some folder shared
@@ -323,8 +381,9 @@ module Aspera
323
381
  file_id: user_info['read_only_home_file_id']
324
382
  }
325
383
  end
326
- raise "Cannot get user's home node id, check your default workspace or specify one" if @home_info[:node_id].to_s.empty?
384
+ Aspera.assert(!@home_info[:node_id].to_s.empty?, "Cannot get user's home node id, check your default workspace or specify one", type: Error)
327
385
  Log.dump(:context, @home_info)
386
+ @home_info
328
387
  end
329
388
 
330
389
  # @param node_id [String] identifier of node in AoC
@@ -393,37 +452,38 @@ module Aspera
393
452
  end
394
453
  meta_schema.each do |field|
395
454
  provided = pkg_meta.select{ |i| i['name'].eql?(field['name'])}
396
- raise "only one field with name #{field['name']} allowed" if provided.count > 1
397
- raise "missing mandatory field: #{field['name']}" if field['required'] && provided.empty?
455
+ Aspera.assert(provided.count <= 1, type: Error){"only one field with name #{field['name']} allowed"}
456
+ Aspera.assert(!provided.empty?, type: Error){"missing mandatory field: #{field['name']}"} if field['required']
398
457
  end
399
458
  end
400
459
 
401
460
  # Normalize package creation recipient lists as expected by AoC API
402
461
  # AoC expects {type: , id: }, but ascli allows providing either the native values or just a name
403
462
  # in that case, the name is resolved and replaced with {type: , id: }
404
- # @param package_data The whole package creation payload
405
- # @param recipient_list_field The field in structure, i.e. recipients or bcc_recipients
406
- # @return nil package_data is modified
407
- def resolve_package_recipients(package_data, ws_id, recipient_list_field, new_user_option)
408
- return unless package_data.key?(recipient_list_field)
409
- Aspera.assert_type(package_data[recipient_list_field], Array){recipient_list_field}
463
+ # @param package_data [Hash] The whole package creation payload
464
+ # @param rcpt_lst_field [String] The field in structure, i.e. recipients or bcc_recipients
465
+ # @param new_user_option [Hash] Additionnal fields for contact creation
466
+ # @return nil, `package_data` is modified
467
+ def resolve_package_recipients(package_data, rcpt_lst_field, new_user_option)
468
+ return unless package_data.key?(rcpt_lst_field)
469
+ Aspera.assert_type(package_data[rcpt_lst_field], Array){rcpt_lst_field}
410
470
  new_user_option = {'package_contact' => true} if new_user_option.nil?
411
471
  Aspera.assert_type(new_user_option, Hash){'new_user_option'}
472
+ ws_id = package_data['workspace_id']
412
473
  # list with resolved elements
413
474
  resolved_list = []
414
- package_data[recipient_list_field].each do |short_recipient_info|
475
+ package_data[rcpt_lst_field].each do |short_recipient_info|
415
476
  case short_recipient_info
416
477
  when Hash # native API information, check keys
417
- Aspera.assert(short_recipient_info.keys.sort.eql?(%w[id type])){"#{recipient_list_field} element shall have fields: id and type"}
478
+ Aspera.assert(short_recipient_info.keys.sort.eql?(%w[id type])){"#{rcpt_lst_field} element shall have fields: id and type"}
418
479
  when String # CLI helper: need to resolve provided name to type/id
419
480
  # email: user, else dropbox
420
481
  entity_type = short_recipient_info.include?('@') ? 'contacts' : 'dropboxes'
421
482
  begin
422
483
  full_recipient_info = lookup_by_name(entity_type, short_recipient_info, query: {'current_workspace_id' => ws_id})
423
- rescue RuntimeError => e
424
- raise e unless e.message.start_with?(ENTITY_NOT_FOUND)
484
+ rescue EntityNotFound
425
485
  # dropboxes cannot be created on the fly
426
- raise "No such shared inbox in workspace #{ws_id}" if entity_type.eql?('dropboxes')
486
+ Aspera.assert_values(entity_type, 'contacts', type: Error){"No such shared inbox in workspace #{ws_id}"}
427
487
  # unknown user: create it as external user
428
488
  full_recipient_info = create('contacts', {
429
489
  'current_workspace_id' => ws_id,
@@ -435,14 +495,13 @@ module Aspera
435
495
  else
436
496
  {'id' => full_recipient_info['source_id'], 'type' => full_recipient_info['source_type']}
437
497
  end
438
- else # unexpected extended value, must be String or Hash
439
- raise "#{recipient_list_field} item must be a String (email, shared inbox) or Hash (id,type)"
498
+ else Aspera.error_unexpected_value(short_recipient_info.class.name){"#{rcpt_lst_field} item must be a String (email, shared inbox) or Hash (id,type)"}
440
499
  end
441
500
  # add original or resolved recipient info
442
501
  resolved_list.push(short_recipient_info)
443
502
  end
444
503
  # replace with resolved elements
445
- package_data[recipient_list_field] = resolved_list
504
+ package_data[rcpt_lst_field] = resolved_list
446
505
  return
447
506
  end
448
507
 
@@ -475,8 +534,8 @@ module Aspera
475
534
  # package_data['file_names']||=[..list of filenames to transfer...]
476
535
 
477
536
  # lookup users
478
- resolve_package_recipients(package_data, package_data['workspace_id'], 'recipients', new_user_option)
479
- resolve_package_recipients(package_data, package_data['workspace_id'], 'bcc_recipients', new_user_option)
537
+ resolve_package_recipients(package_data, 'recipients', new_user_option)
538
+ resolve_package_recipients(package_data, 'bcc_recipients', new_user_option)
480
539
 
481
540
  validate_metadata(package_data) if validate_meta
482
541
 
@@ -592,8 +651,7 @@ module Aspera
592
651
  when NilClass
593
652
  when ''
594
653
  # workspace shared folder
595
- perm_data['access_type'] = 'user'
596
- perm_data['access_id'] = "#{ID_AK_ADMIN}_WS_#{app_info[:workspace_id]}"
654
+ perm_data.merge!(self.class.workspace_access(app_info[:workspace_id]))
597
655
  tag_workspace['shared_with_name'] = perm_data['access_id']
598
656
  else
599
657
  entity_info = lookup_by_name('contacts', shared_with, query: {'current_workspace_id' => app_info[:workspace_id]})
@@ -55,8 +55,9 @@ module Aspera
55
55
  operation: 'GET',
56
56
  subpath: bucket,
57
57
  headers: {'Accept' => 'application/xml'},
58
- query: {'faspConnectionInfo' => nil}
59
- )[:http].body
58
+ query: {'faspConnectionInfo' => nil},
59
+ ret: :resp
60
+ ).body
60
61
  ats_info = XmlSimple.xml_in(xml_result_text, {'ForceArray' => false})
61
62
  Log.dump(:ats_info, ats_info)
62
63
  @storage_credentials = {
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/rest'
4
+ require 'aspera/oauth/base'
5
+ require 'digest'
6
+
7
+ module Aspera
8
+ # Implement OAuth for Faspex public link
9
+ class FaspexPubLink < OAuth::Base
10
+ class << self
11
+ attr_accessor :additional_info
12
+ end
13
+ # @param context The `context` query parameter in public link
14
+ # @param redirect_uri URI of web UI login
15
+ # @param path_authorize Path to provide passcode
16
+ def initialize(
17
+ context:,
18
+ redirect_uri:,
19
+ path_authorize: 'authorize_public_link',
20
+ **base_params
21
+ )
22
+ # a unique identifier could also be the passcode inside
23
+ super(**base_params, cache_ids: [Digest::SHA256.hexdigest(context)[0..23]])
24
+ @context = context
25
+ @redirect_uri = redirect_uri
26
+ @path_authorize = path_authorize
27
+ end
28
+
29
+ def create_token
30
+ # Exchange context (passcode) for code
31
+ http = api.call(
32
+ operation: 'GET',
33
+ subpath: @path_authorize,
34
+ query: {
35
+ response_type: :code,
36
+ state: @context,
37
+ client_id: client_id,
38
+ redirect_uri: @redirect_uri
39
+ },
40
+ exception: false,
41
+ ret: :resp
42
+ )
43
+ # code / state located in redirected URL query
44
+ info = Rest.query_to_h(URI.parse(http['Location']).query)
45
+ Log.dump(:info, info)
46
+ raise Error, info['action_message'] if info['action_message']
47
+ Aspera.assert(info['code']){'Missing code in answer'}
48
+ # Exchange code for token
49
+ return create_token_call(optional_scope_client_id.merge(
50
+ grant_type: 'authorization_code',
51
+ code: info['code'],
52
+ redirect_uri: @redirect_uri
53
+ ))
54
+ end
55
+ end
56
+ OAuth::Factory.instance.register_token_creator(FaspexPubLink)
57
+ module Api
58
+ class Faspex < Aspera::Rest
59
+ # endpoint for authentication API
60
+ PATH_AUTH = 'auth'
61
+ PATH_API_V5 = 'api/v5'
62
+ PATH_HEALTH = 'configuration/ping'
63
+ private_constant :PATH_API_V5,
64
+ :PATH_HEALTH,
65
+ :PATH_AUTH
66
+ RECIPIENT_TYPES = %w[user workgroup external_user distribution_list shared_inbox].freeze
67
+ PACKAGE_TERMINATED = %w[completed failed].freeze
68
+ # list of supported mailbox types (to list packages)
69
+ API_LIST_MAILBOX_TYPES = %w[inbox inbox_history inbox_all inbox_all_history outbox outbox_history pending pending_history all].freeze
70
+ # PACKAGE_SEND_FROM_REMOTE_SOURCE = 'remote_source'
71
+ # Faspex API v5: get transfer spec for connect
72
+ TRANSFER_CONNECT = 'connect'
73
+ ADMIN_RESOURCES = %i[
74
+ accounts distribution_lists contacts jobs workgroups shared_inboxes nodes oauth_clients registrations saml_configs
75
+ metadata_profiles email_notifications alternate_addresses webhooks
76
+ ].freeze
77
+ # states for jobs not in final state
78
+ JOB_RUNNING = %w[queued working].freeze
79
+ PATH_STANDARD_ROOT = '/aspera/faspex'
80
+ PATH_API_DETECT = "#{PATH_API_V5}/#{PATH_HEALTH}"
81
+ HEADER_ITERATION_TOKEN = 'X-Aspera-Next-Iteration-Token'
82
+ HEADER_FASPEX_VERSION = 'X-IBM-Aspera'
83
+ EMAIL_NOTIF_LIST = %w[
84
+ welcome_email
85
+ forgot_password
86
+ package_received
87
+ package_received_cc
88
+ package_sent_cc
89
+ package_downloaded
90
+ package_downloaded_cc
91
+ workgroup_package
92
+ upload_result
93
+ upload_result_cc
94
+ relay_started_cc
95
+ relay_finished_cc
96
+ relay_error_cc
97
+ shared_inbox_invitation
98
+ shared_inbox_submit
99
+ personal_invitation
100
+ personal_submit
101
+ account_approved
102
+ account_denied
103
+ package_file_processing_failed_sender
104
+ package_file_processing_failed_recipient
105
+ relay_failed_admin
106
+ relay_failed
107
+ admin_sync_failed
108
+ sync_failed
109
+ account_exist
110
+ mfa_code
111
+ ]
112
+ class << self
113
+ # @return true if the URL is a public link
114
+ def public_link?(url)
115
+ url.include?('?context=')
116
+ end
117
+ end
118
+ attr_reader :pub_link_context
119
+
120
+ def initialize(
121
+ url:,
122
+ auth:,
123
+ password: nil,
124
+ client_id: nil,
125
+ client_secret: nil,
126
+ redirect_uri: nil,
127
+ username: nil,
128
+ private_key: nil,
129
+ passphrase: nil
130
+ )
131
+ auth = :public_link if self.class.public_link?(url)
132
+ @pub_link_context = nil
133
+ super(**
134
+ case auth
135
+ when :public_link
136
+ # Get URL of final redirect of public link
137
+ redir_url = Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', ret: :resp).uri.to_s
138
+ Log.dump(:redir_url, redir_url, level: :trace1)
139
+ # get context from query
140
+ encoded_context = Rest.query_to_h(URI.parse(redir_url).query)['context']
141
+ raise ParameterError, 'Bad faspex5 public link, missing context in query' if encoded_context.nil?
142
+ # public link information (contains passcode and allowed usage)
143
+ @pub_link_context = JSON.parse(Base64.decode64(encoded_context))
144
+ Log.dump(:pub_link_context, @pub_link_context, level: :trace1)
145
+ # Get the base url, i.e. .../aspera/faspex
146
+ base_url = redir_url.gsub(%r{/public/.*}, '').gsub(/\?.*/, '')
147
+ # Get web UI client_id and redirect_uri
148
+ # TODO: change this for something more reliable
149
+ config = JSON.parse(Rest.new(base_url: "#{base_url}/config.js", redirect_max: 3).call(operation: 'GET').sub(/^[^=]+=/, '').gsub(/([a-z_]+):/, '"\1":').delete("\n ").tr("'", '"')).symbolize_keys
150
+ Log.dump(:configjs, config)
151
+ {
152
+ base_url: "#{base_url}/#{PATH_API_V5}",
153
+ auth: {
154
+ type: :oauth2,
155
+ base_url: "#{base_url}/#{PATH_AUTH}",
156
+ grant_method: :faspex_pub_link,
157
+ context: encoded_context,
158
+ client_id: config[:client_id],
159
+ redirect_uri: config[:redirect_uri]
160
+ }
161
+ }
162
+ # old: headers: {'Passcode' => @pub_link_context['passcode']}
163
+ when :boot
164
+ Aspera.assert(password, type: ParameterError){'Missing password'}
165
+ # the password here is the token copied directly from browser in developer mode
166
+ {
167
+ base_url: "#{url}/#{PATH_API_V5}",
168
+ headers: {'Authorization' => password}
169
+ }
170
+ when :web
171
+ Aspera.assert(client_id, type: ParameterError){'Missing client_id'}
172
+ Aspera.assert(redirect_uri, type: ParameterError){'Missing redirect_uri'}
173
+ # opens a browser and ask user to auth using web
174
+ {
175
+ base_url: "#{url}/#{PATH_API_V5}",
176
+ auth: {
177
+ type: :oauth2,
178
+ base_url: "#{url}/#{PATH_AUTH}",
179
+ grant_method: :web,
180
+ client_id: client_id,
181
+ redirect_uri: redirect_uri
182
+ }
183
+ }
184
+ when :jwt
185
+ Aspera.assert(client_id, type: ParameterError){'Missing client_id'}
186
+ Aspera.assert(private_key, type: ParameterError){'Missing private_key'}
187
+ {
188
+ base_url: "#{url}/#{PATH_API_V5}",
189
+ auth: {
190
+ type: :oauth2,
191
+ base_url: "#{url}/#{PATH_AUTH}",
192
+ grant_method: :jwt,
193
+ client_id: client_id,
194
+ payload: {
195
+ iss: client_id, # issuer
196
+ aud: client_id, # audience (this field is not clear...)
197
+ sub: "user:#{username}" # subject is a user
198
+ },
199
+ private_key_obj: OpenSSL::PKey::RSA.new(private_key, passphrase),
200
+ headers: {typ: 'JWT'}
201
+ }
202
+ }
203
+ else Aspera.error_unexpected_value(auth, type: ParameterError){'auth'}
204
+ end
205
+ )
206
+ end
207
+
208
+ def auth_api
209
+ Rest.new(**params, base_url: base_url.sub(PATH_API_V5, PATH_AUTH))
210
+ end
211
+ end
212
+ end
213
+ end