aspera-cli 4.17.0 → 4.18.1

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 (85) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +3 -4
  3. data/CHANGELOG.md +33 -0
  4. data/CONTRIBUTING.md +15 -1
  5. data/README.md +711 -432
  6. data/bin/ascli +5 -0
  7. data/bin/asession +2 -2
  8. data/examples/build_package.sh +28 -0
  9. data/lib/aspera/agent/alpha.rb +10 -8
  10. data/lib/aspera/agent/base.rb +9 -6
  11. data/lib/aspera/agent/connect.rb +7 -8
  12. data/lib/aspera/agent/direct.rb +56 -37
  13. data/lib/aspera/agent/httpgw.rb +23 -324
  14. data/lib/aspera/agent/node.rb +19 -20
  15. data/lib/aspera/agent/trsdk.rb +19 -20
  16. data/lib/aspera/api/aoc.rb +17 -14
  17. data/lib/aspera/api/cos_node.rb +4 -4
  18. data/lib/aspera/api/httpgw.rb +342 -0
  19. data/lib/aspera/api/node.rb +135 -89
  20. data/lib/aspera/ascmd.rb +4 -3
  21. data/lib/aspera/ascp/installation.rb +15 -7
  22. data/lib/aspera/ascp/management.rb +2 -2
  23. data/lib/aspera/ascp/products.rb +1 -1
  24. data/lib/aspera/cli/basic_auth_plugin.rb +5 -9
  25. data/lib/aspera/cli/extended_value.rb +35 -16
  26. data/lib/aspera/cli/formatter.rb +161 -70
  27. data/lib/aspera/cli/hints.rb +18 -0
  28. data/lib/aspera/cli/main.rb +32 -39
  29. data/lib/aspera/cli/manager.rb +151 -119
  30. data/lib/aspera/cli/plugin.rb +27 -21
  31. data/lib/aspera/cli/plugin_factory.rb +31 -20
  32. data/lib/aspera/cli/plugins/alee.rb +14 -2
  33. data/lib/aspera/cli/plugins/aoc.rb +152 -141
  34. data/lib/aspera/cli/plugins/ats.rb +1 -1
  35. data/lib/aspera/cli/plugins/config.rb +72 -65
  36. data/lib/aspera/cli/plugins/console.rb +8 -5
  37. data/lib/aspera/cli/plugins/faspex.rb +32 -23
  38. data/lib/aspera/cli/plugins/faspex5.rb +232 -156
  39. data/lib/aspera/cli/plugins/faspio.rb +85 -0
  40. data/lib/aspera/cli/plugins/httpgw.rb +55 -0
  41. data/lib/aspera/cli/plugins/node.rb +129 -64
  42. data/lib/aspera/cli/plugins/orchestrator.rb +33 -30
  43. data/lib/aspera/cli/plugins/preview.rb +7 -3
  44. data/lib/aspera/cli/plugins/server.rb +6 -6
  45. data/lib/aspera/cli/plugins/shares.rb +16 -14
  46. data/lib/aspera/cli/special_values.rb +13 -0
  47. data/lib/aspera/cli/sync_actions.rb +10 -10
  48. data/lib/aspera/cli/transfer_agent.rb +7 -6
  49. data/lib/aspera/cli/version.rb +1 -1
  50. data/lib/aspera/environment.rb +70 -9
  51. data/lib/aspera/faspex_gw.rb +5 -4
  52. data/lib/aspera/faspex_postproc.rb +2 -2
  53. data/lib/aspera/log.rb +6 -3
  54. data/lib/aspera/node_simulator.rb +2 -2
  55. data/lib/aspera/oauth/base.rb +31 -19
  56. data/lib/aspera/oauth/factory.rb +12 -13
  57. data/lib/aspera/oauth/generic.rb +1 -0
  58. data/lib/aspera/oauth/jwt.rb +18 -15
  59. data/lib/aspera/oauth/url_json.rb +8 -6
  60. data/lib/aspera/oauth/web.rb +2 -2
  61. data/lib/aspera/persistency_folder.rb +2 -2
  62. data/lib/aspera/preview/generator.rb +3 -3
  63. data/lib/aspera/preview/options.rb +3 -3
  64. data/lib/aspera/preview/terminal.rb +4 -4
  65. data/lib/aspera/preview/utils.rb +3 -3
  66. data/lib/aspera/proxy_auto_config.rb +5 -1
  67. data/lib/aspera/rest.rb +105 -88
  68. data/lib/aspera/rest_call_error.rb +1 -1
  69. data/lib/aspera/rest_error_analyzer.rb +2 -2
  70. data/lib/aspera/rest_errors_aspera.rb +1 -1
  71. data/lib/aspera/resumer.rb +1 -1
  72. data/lib/aspera/secret_hider.rb +2 -4
  73. data/lib/aspera/ssh.rb +1 -1
  74. data/lib/aspera/transfer/parameters.rb +39 -36
  75. data/lib/aspera/transfer/spec.rb +2 -0
  76. data/lib/aspera/transfer/sync.rb +2 -1
  77. data/lib/aspera/transfer/uri.rb +1 -1
  78. data/lib/aspera/uri_reader.rb +5 -4
  79. data/lib/aspera/web_auth.rb +1 -1
  80. data/lib/aspera/web_server_simple.rb +4 -3
  81. data.tar.gz.sig +0 -0
  82. metadata +7 -4
  83. metadata.gz.sig +0 -0
  84. data/lib/aspera/cli/plugins/bss.rb +0 -71
  85. data/lib/aspera/open_application.rb +0 -71
@@ -4,13 +4,13 @@
4
4
 
5
5
  require 'aspera/cli/basic_auth_plugin'
6
6
  require 'aspera/cli/extended_value'
7
+ require 'aspera/cli/special_values'
7
8
  require 'aspera/persistency_action_once'
8
9
  require 'aspera/id_generator'
9
10
  require 'aspera/nagios'
10
11
  require 'aspera/environment'
11
12
  require 'aspera/assert'
12
13
  require 'securerandom'
13
- require 'tty-spinner'
14
14
 
15
15
  module Aspera
16
16
  module Cli
@@ -24,8 +24,8 @@ module Aspera
24
24
  # Faspex API v5: get transfer spec for connect
25
25
  TRANSFER_CONNECT = 'connect'
26
26
  ADMIN_RESOURCES = %i[
27
- accounts contacts jobs workgroups shared_inboxes nodes oauth_clients registrations saml_configs
28
- metadata_profiles email_notifications alternate_addresses
27
+ accounts distribution_lists contacts jobs workgroups shared_inboxes nodes oauth_clients registrations saml_configs
28
+ metadata_profiles email_notifications alternate_addresses webhooks
29
29
  ].freeze
30
30
  # states for jobs not in final state
31
31
  JOB_RUNNING = %w[queued working].freeze
@@ -58,7 +58,7 @@ module Aspera
58
58
  path_api_detect = "#{PATH_API_V5}/#{PATH_HEALTH}"
59
59
  result = api.read(path_api_detect)
60
60
  next unless result[:http].code.start_with?('2') && result[:http].body.strip.empty?
61
- # end is at -1, and substract 1 for "/"
61
+ # end is at -1, and subtract 1 for "/"
62
62
  url_length = -2 - path_api_detect.length
63
63
  # take redirect if any
64
64
  return {
@@ -82,7 +82,7 @@ module Aspera
82
82
  if options.get_option(:client_id).nil? || options.get_option(:client_secret).nil?
83
83
  formatter.display_status('Ask the ascli client id and secret to your Administrator.'.red)
84
84
  formatter.display_status("Admin should login to: #{instance_url}")
85
- OpenApplication.instance.uri(instance_url)
85
+ Environment.instance.open_uri(instance_url)
86
86
  formatter.display_status('Navigate to: 𓃑 → Admin → Configurations → API clients')
87
87
  formatter.display_status('Create an API client with:')
88
88
  formatter.display_status('- name: ascli')
@@ -119,7 +119,7 @@ module Aspera
119
119
  options.declare(:auth, 'OAuth type of authentication', values: STD_AUTH_TYPES, default: :jwt)
120
120
  options.declare(:private_key, 'OAuth JWT RSA private key PEM value (prefix file path with @file:)')
121
121
  options.declare(:passphrase, 'OAuth JWT RSA private key passphrase')
122
- options.declare(:box, "Package inbox, either shared inbox name or one of: #{API_LIST_MAILBOX_TYPES.join(', ')} or #{ExtendedValue::ALL}", default: 'inbox')
122
+ options.declare(:box, "Package inbox, either shared inbox name or one of: #{API_LIST_MAILBOX_TYPES.join(', ')} or #{SpecialValues::ALL}", default: 'inbox')
123
123
  options.declare(:shared_folder, 'Send package with files from shared folder')
124
124
  options.declare(:group_type, 'Type of shared box', values: %i[shared_inboxes workgroups], default: :shared_inboxes)
125
125
  options.parse_options!
@@ -237,31 +237,27 @@ module Aspera
237
237
  end
238
238
 
239
239
  def wait_for_job(job_id)
240
- spinner = nil
241
240
  loop do
242
241
  status = @api_v5.read("jobs/#{job_id}", {type: :formatted})[:data]
243
242
  return status unless JOB_RUNNING.include?(status['status'])
244
- if spinner.nil?
245
- spinner = TTY::Spinner.new('[:spinner] :title', format: :classic)
246
- spinner.start
247
- end
248
- spinner.update(title: status['status'])
249
- spinner.spin
243
+ formatter.long_operation_running(status['status'])
250
244
  sleep(0.5)
251
245
  end
252
246
  Aspera.error_unreachable_line
253
247
  end
254
248
 
255
- # Get a (full or partial) list of all entities of a given type
249
+ # Get a (full or partial) list of all entities of a given type with query: offset/limit
256
250
  # @param type [String] the type of entity to list (just a name)
257
251
  # @param query [Hash,nil] additional query parameters
258
252
  # @param real_path [String] real path if it's n ot just the type
259
253
  # @param item_list_key [String] key in the result to get the list of items
260
- def list_entities(type:, real_path: nil, query: {}, item_list_key: nil)
254
+ def list_entities(type:, real_path: nil, item_list_key: nil, query: {})
255
+ Log.log.trace1{"list_entities t=#{type} p=#{real_path} k=#{item_list_key} q=#{query}"}
261
256
  type = type.to_s if type.is_a?(Symbol)
262
257
  Aspera.assert_type(type, String)
258
+ Aspera.assert_type(query, Hash)
263
259
  item_list_key = type if item_list_key.nil?
264
- full_path = real_path.nil? ? type : real_path
260
+ real_path = type if real_path.nil?
265
261
  result = []
266
262
  offset = 0
267
263
  max_items = query.delete(MAX_ITEMS)
@@ -270,7 +266,8 @@ module Aspera
270
266
  query = {'limit'=> PER_PAGE_DEFAULT}.merge(query)
271
267
  loop do
272
268
  query['offset'] = offset
273
- page_result = @api_v5.read(full_path, query)[:data]
269
+ page_result = @api_v5.read(real_path, query)[:data]
270
+ Aspera.assert_type(page_result[item_list_key], Array)
274
271
  result.concat(page_result[item_list_key])
275
272
  # reach the limit set by user ?
276
273
  if !max_items.nil? && (result.length >= max_items)
@@ -281,12 +278,19 @@ module Aspera
281
278
  remain_pages -= 1 unless remain_pages.nil?
282
279
  break if remain_pages == 0
283
280
  offset += page_result[item_list_key].length
281
+ formatter.long_operation_running
284
282
  end
285
283
  return result
286
284
  end
287
285
 
288
286
  # lookup an entity id from its name
289
- def lookup_entity_by_field(type:, value:, field: 'name', query: :default, real_path: nil, item_list_key: nil)
287
+ # @param type [String] the type of entity to lookup, by default it is the path, and it is also the field name in result
288
+ # @param value [String] the value to lookup
289
+ # @param field [String] the field to match, by default it is 'name'
290
+ # @param real_path [String] real path if it's not just the type (override type)
291
+ # @param item_list_key [String] key in the result to get the list of items (override type)
292
+ # @param query [Hash] additional query parameters
293
+ def lookup_entity_by_field(type:, value:, field: 'name', real_path: nil, item_list_key: nil, query: :default)
290
294
  if query.eql?(:default)
291
295
  Aspera.assert(field.eql?('name')){'Default query is on name only'}
292
296
  query = {'q'=> value}
@@ -301,12 +305,12 @@ module Aspera
301
305
 
302
306
  # list all packages with optional filter
303
307
  def list_packages_with_filter(query: {})
304
- filter = options.get_next_argument('filter', mandatory: false, type: Proc, default: ->(_x){true})
308
+ filter = options.get_next_argument('filter', mandatory: false, validation: Proc, default: ->(_x){true})
305
309
  # translate box name to API prefix (with ending slash)
306
310
  box = options.get_option(:box)
307
311
  real_path =
308
312
  case box
309
- when ExtendedValue::ALL then 'packages' # only admin can list all packages globally
313
+ when SpecialValues::ALL then 'packages' # only admin can list all packages globally
310
314
  when *API_LIST_MAILBOX_TYPES then "#{box}/packages"
311
315
  else
312
316
  group_type = options.get_option(:group_type)
@@ -335,12 +339,12 @@ module Aspera
335
339
  end
336
340
  packages = []
337
341
  case package_ids
338
- when ExtendedValue::INIT
342
+ when SpecialValues::INIT
339
343
  Aspera.assert(skip_ids_persistency){'Only with option once_only'}
340
344
  skip_ids_persistency.data.clear.concat(list_packages_with_filter.map{|p|p['id']})
341
345
  skip_ids_persistency.save
342
346
  return Main.result_status("Initialized skip for #{skip_ids_persistency.data.count} package(s)")
343
- when ExtendedValue::ALL
347
+ when SpecialValues::ALL
344
348
  # TODO: if packages have same name, they will overwrite ?
345
349
  packages = list_packages_with_filter(query: {'status' => 'completed'})
346
350
  Log.log.trace1{Log.dump(:package_ids, packages.map{|p|p['id']})}
@@ -381,8 +385,9 @@ module Aspera
381
385
  operation: 'POST',
382
386
  subpath: "packages/#{pkg_id}/transfer_spec/download",
383
387
  headers: {'Accept' => 'application/json'},
384
- url_params: download_params,
385
- json_params: param_file_list
388
+ query: download_params,
389
+ body: param_file_list,
390
+ body_type: :json
386
391
  )[:data]
387
392
  # delete flag for Connect Client
388
393
  transfer_spec.delete('authentication')
@@ -400,7 +405,7 @@ module Aspera
400
405
  # browse a folder
401
406
  # @param browse_endpoint [String] the endpoint to browse
402
407
  def browse_folder(browse_endpoint)
403
- path = options.get_next_argument('folder path', mandatory: false, default: '/')
408
+ folders_to_process = [options.get_next_argument('folder path', mandatory: false, default: '/')]
404
409
  query = query_read_delete(default: {})
405
410
  query['filters'] = {} unless query.key?('filters')
406
411
  filters = query.delete('filters')
@@ -409,17 +414,16 @@ module Aspera
409
414
  max_items = query.delete('max')
410
415
  recursive = query.delete('recursive')
411
416
  all_items = []
412
- folders_to_process = [path]
413
417
  until folders_to_process.empty?
414
418
  path = folders_to_process.shift
415
- query.delete('iteration_token')
416
419
  loop do
417
420
  response = @api_v5.call(
418
421
  operation: 'POST',
419
422
  subpath: browse_endpoint,
420
- headers: {'Accept' => 'application/json', 'Content-Type' => 'application/json'},
421
- url_params: query,
422
- json_params: {'path' => path, 'filters' => filters})
423
+ headers: {'Accept' => 'application/json'},
424
+ query: query,
425
+ body: {'path' => path, 'filters' => filters},
426
+ body_type: :json)
423
427
  all_items.concat(response[:data]['items'])
424
428
  if recursive
425
429
  folders_to_process.concat(response[:data]['items'].select{|i|i['type'].eql?('directory')}.map{|i|i['path']})
@@ -431,7 +435,9 @@ module Aspera
431
435
  iteration_token = response[:http][HEADER_ITERATION_TOKEN]
432
436
  break if iteration_token.nil? || iteration_token.empty?
433
437
  query['iteration_token'] = iteration_token
438
+ formatter.long_operation_running(all_items.count)
434
439
  end
440
+ query.delete('iteration_token')
435
441
  end
436
442
  return {type: :object_list, data: all_items}
437
443
  end
@@ -459,9 +465,14 @@ module Aspera
459
465
  ids = package_id
460
466
  ids = [ids] unless ids.is_a?(Array)
461
467
  Aspera.assert_type(ids, Array){'Package identifier'}
462
- Aspera.assert(ids.all?(String)){'Package id shall be String'}
468
+ Aspera.assert(ids.all?(String)){"Package id(s) shall be String, but have: #{ids.map(&:class).uniq.join(', ')}"}
463
469
  # API returns 204, empty on success
464
- @api_v5.call(operation: 'DELETE', subpath: 'packages', headers: {'Accept' => 'application/json'}, json_params: {ids: ids})
470
+ @api_v5.call(
471
+ operation: 'DELETE',
472
+ subpath: 'packages',
473
+ headers: {'Accept' => 'application/json'},
474
+ body: {ids: ids},
475
+ body_type: :json)
465
476
  return Main.result_status('Package(s) deleted')
466
477
  when :receive
467
478
  return package_receive(package_id)
@@ -483,8 +494,9 @@ module Aspera
483
494
  operation: 'POST',
484
495
  subpath: "packages/#{package['id']}/transfer_spec/upload",
485
496
  headers: {'Accept' => 'application/json'},
486
- url_params: {transfer_type: TRANSFER_CONNECT},
487
- json_params: {paths: transfer.source_list}
497
+ query: {transfer_type: TRANSFER_CONNECT},
498
+ body: {paths: transfer.source_list},
499
+ body_type: :json
488
500
  )[:data]
489
501
  # well, we asked a TS for connect, but we actually want a generic one
490
502
  transfer_spec.delete('authentication')
@@ -492,7 +504,10 @@ module Aspera
492
504
  else
493
505
  # send from remote shared folder
494
506
  if (m = shared_folder.match(REGEX_LOOKUP_ID_BY_FIELD))
495
- shared_folder = lookup_entity_by_field(type: 'shared_folders', value: m[2])['id']
507
+ shared_folder = lookup_entity_by_field(
508
+ type: 'shared_folders',
509
+ field: m[1],
510
+ value: ExtendedValue.instance.evaluate(m[2]))['id']
496
511
  end
497
512
  transfer_request = {shared_folder_id: shared_folder, paths: transfer.source_list}
498
513
  # start remote transfer and get first status
@@ -510,7 +525,176 @@ module Aspera
510
525
  data: list_packages_with_filter,
511
526
  fields: %w[id title release_date total_bytes total_files created_time state]
512
527
  }
513
- end # case package
528
+ end
529
+ end
530
+
531
+ def execute_resource(res_type)
532
+ list_key = res_path = res_type.to_s
533
+ id_as_arg = false
534
+ display_fields = nil
535
+ adm_api = @api_v5
536
+ res_id_query = :default
537
+ delete_style = nil
538
+ available_commands = [].concat(Plugin::ALL_OPS)
539
+ case res_type
540
+ when :metadata_profiles
541
+ res_path = 'configuration/metadata_profiles'
542
+ list_key = 'profiles'
543
+ when :alternate_addresses
544
+ res_path = 'configuration/alternate_addresses'
545
+ when :distribution_lists
546
+ res_path = 'account/distribution_lists'
547
+ list_key = 'distribution_lists'
548
+ delete_style = 'ids'
549
+ when :email_notifications
550
+ list_key = false
551
+ id_as_arg = 'type'
552
+ when :accounts
553
+ display_fields = Formatter.all_but('user_profile_data_attributes')
554
+ when :oauth_clients
555
+ display_fields = Formatter.all_but('public_key')
556
+ adm_api = Rest.new(**@api_v5.params.merge(base_url: "#{@faspex5_api_base_url}/#{PATH_AUTH}"))
557
+ when :shared_inboxes, :workgroups
558
+ available_commands.push(:members, :saml_groups, :invite_external_collaborator)
559
+ res_id_query = {'all': true}
560
+ when :nodes
561
+ available_commands.push(:shared_folders, :browse)
562
+ end
563
+ res_command = options.get_next_command(available_commands)
564
+ case res_command
565
+ when *Plugin::ALL_OPS
566
+ return entity_command(
567
+ res_command, adm_api, res_path, item_list_key: list_key, display_fields: display_fields, id_as_arg: id_as_arg,
568
+ delete_style: delete_style) do |field, value|
569
+ lookup_entity_by_field(
570
+ type: res_type, value: value, field: field, real_path: res_path, item_list_key: list_key, query: res_id_query)['id']
571
+ end
572
+ when :shared_folders
573
+ node_id = instance_identifier do |field, value|
574
+ lookup_entity_by_field(type: res_type, field: field, value: value)['id']
575
+ end
576
+ sh_path = "#{res_path}/#{node_id}/shared_folders"
577
+ sh_command = options.get_next_command([:user].concat(Plugin::ALL_OPS))
578
+ case sh_command
579
+ when *Plugin::ALL_OPS
580
+ return entity_command(sh_command, adm_api, sh_path, item_list_key: 'shared_folders') do |field, value|
581
+ lookup_entity_by_field(type: 'shared_folders', real_path: sh_path, field: field, value: value)['id']
582
+ end
583
+ when :user
584
+ sh_id = instance_identifier do |field, value|
585
+ lookup_entity_by_field(type: 'shared_folders', real_path: sh_path, field: field, value: value)['id']
586
+ end
587
+ user_path = "#{sh_path}/#{sh_id}/custom_access_users"
588
+ return entity_action(adm_api, user_path, item_list_key: 'users') do |field, value|
589
+ lookup_entity_by_field(type: 'users', real_path: user_path, field: field, value: value)['id']
590
+ end
591
+
592
+ end
593
+ when :browse
594
+ return browse_folder("#{res_path}/#{instance_identifier}/browse")
595
+ when :invite_external_collaborator
596
+ shared_inbox_id = instance_identifier { |field, value| lookup_entity_by_field(type: res_type.to_s, field: field, value: value, query: res_id_query)['id']}
597
+ creation_payload = value_create_modify(command: res_command, type: [Hash, String])
598
+ creation_payload = {'email_address' => creation_payload} if creation_payload.is_a?(String)
599
+ res_path = "#{res_type}/#{shared_inbox_id}/external_collaborator"
600
+ result = adm_api.create(res_path, creation_payload)[:data]
601
+ formatter.display_status(result['message'])
602
+ result = lookup_entity_by_field(
603
+ type: 'members',
604
+ real_path: "#{res_type}/#{shared_inbox_id}/members",
605
+ value: creation_payload['email_address'],
606
+ query: {})
607
+ return {type: :single_object, data: result}
608
+ when :members, :saml_groups
609
+ res_id = instance_identifier { |field, value| lookup_entity_by_field(type: res_type.to_s, field: field, value: value, query: res_id_query)['id']}
610
+ res_prefix = "#{res_type}/#{res_id}"
611
+ res_path = "#{res_prefix}/#{res_command}"
612
+ list_key = res_command.to_s
613
+ list_key = 'groups' if res_command.eql?(:saml_groups)
614
+ sub_command = options.get_next_command(%i[create list modify delete])
615
+ if sub_command.eql?(:create) && options.get_option(:value).nil?
616
+ raise "use option 'value' to provide saml group_id and access (refer to API)" unless res_command.eql?(:members)
617
+ # first arg is one user name or list of users
618
+ users = options.get_next_argument('user id, %name:, or Array')
619
+ users = [users] unless users.is_a?(Array)
620
+ users = users.map do |user|
621
+ if (m = user.match(REGEX_LOOKUP_ID_BY_FIELD))
622
+ lookup_entity_by_field(
623
+ type: 'accounts',
624
+ field: m[1],
625
+ value: ExtendedValue.instance.evaluate(m[2]),
626
+ query: {type: Rest.array_params(%w{local_user saml_user self_registered_user external_user})})['id']
627
+ else
628
+ # it's the user id (not member id...)
629
+ user
630
+ end
631
+ end
632
+ access = options.get_next_argument('level', mandatory: false, accept_list: %i[submit_only standard shared_inbox_admin], default: :standard)
633
+ # TODO: unshift to command line parameters instead of using deprecated option "value"
634
+ options.set_option(:value, {user: users.map{|u|{id: u, access: access}}})
635
+ end
636
+ return entity_command(sub_command, adm_api, res_path, item_list_key: list_key) do |field, value|
637
+ lookup_entity_by_field(
638
+ type: 'accounts',
639
+ field: field,
640
+ value: value,
641
+ query: {type: Rest.array_params(%w{local_user saml_user self_registered_user external_user})})['id']
642
+ end
643
+ end
644
+ end
645
+
646
+ def execute_admin
647
+ command = options.get_next_command(%i[configuration smtp resource events clean_deleted].concat(ADMIN_RESOURCES).freeze)
648
+ case command
649
+ when :resource
650
+ # resource will be deprecated
651
+ Log.log.warn('resource command is deprecated (4.18), directly use the specific command instead')
652
+ return execute_resource(options.get_next_command(ADMIN_RESOURCES))
653
+ when *ADMIN_RESOURCES
654
+ return execute_resource(command)
655
+ when :clean_deleted
656
+ delete_data = value_create_modify(command: command, default: {days_before_deleting_package_records: 365})
657
+ res = @api_v5.create('internal/packages/clean_deleted', delete_data)
658
+ return {type: :single_object, data: res[:data]}
659
+ when :events
660
+ event_type = options.get_next_command(%i[application webhook])
661
+ case event_type
662
+ when :application
663
+ return {type: :object_list, data: list_entities(type: 'application_events'), fields: %w[event_type created_at application user.name]}
664
+ when :webhook
665
+ return {type: :object_list, data: list_entities(type: 'all_webhooks_events', item_list_key: 'events')}
666
+ end
667
+ when :configuration
668
+ conf_path = 'configuration'
669
+ conf_cmd = options.get_next_command(%i[show modify])
670
+ case conf_cmd
671
+ when :show
672
+ return { type: :single_object, data: @api_v5.read(conf_path)[:data] }
673
+ when :modify
674
+ return { type: :single_object, data: @api_v5.update(conf_path, value_create_modify(command: conf_cmd))[:data] }
675
+ end
676
+ when :smtp
677
+ smtp_path = 'configuration/smtp'
678
+ smtp_cmd = options.get_next_command(%i[show create modify delete test])
679
+ case smtp_cmd
680
+ when :show
681
+ return { type: :single_object, data: @api_v5.read(smtp_path)[:data] }
682
+ when :create
683
+ return { type: :single_object, data: @api_v5.create(smtp_path, value_create_modify(command: smtp_cmd))[:data] }
684
+ when :modify
685
+ return { type: :single_object, data: @api_v5.update(smtp_path, value_create_modify(command: smtp_cmd))[:data] }
686
+ when :delete
687
+ @api_v5.delete(smtp_path)[:data]
688
+ return Main.result_status('SMTP configuration deleted')
689
+ when :test
690
+ test_data = options.get_next_argument('Email or test data, see API')
691
+ test_data = {test_email_recipient: test_data} if test_data.is_a?(String)
692
+ creation = @api_v5.create(File.join(smtp_path, 'test'), test_data)[:data]
693
+ result = wait_for_job(creation['job_id'])
694
+ result['serialized_args'] = JSON.parse(result['serialized_args']) rescue result['serialized_args']
695
+ return { type: :single_object, data: result }
696
+ end
697
+ end
514
698
  end
515
699
 
516
700
  ACTIONS = %i[health version user bearer_token packages shared_folders admin gateway postprocessing invitations].freeze
@@ -541,12 +725,12 @@ module Aspera
541
725
  when :show
542
726
  return { type: :single_object, data: @api_v5.read('account/preferences')[:data] }
543
727
  when :modify
544
- @api_v5.update('account/preferences', options.get_next_argument('modified parameters', type: Hash))
728
+ @api_v5.update('account/preferences', options.get_next_argument('modified parameters', validation: Hash))
545
729
  return Main.result_status('modified')
546
730
  end
547
731
  end
548
732
  when :bearer_token
549
- return {type: :text, data: @api_v5.oauth_token}
733
+ return {type: :text, data: @api_v5.oauth.token}
550
734
  when :packages
551
735
  return package_action
552
736
  when :shared_folders
@@ -566,117 +750,7 @@ module Aspera
566
750
  return browse_folder("nodes/#{node['node_id']}/shared_folders/#{shared_folder_id}/browse")
567
751
  end
568
752
  when :admin
569
- case options.get_next_command(%i[resource smtp].freeze)
570
- when :resource
571
- res_type = options.get_next_command(ADMIN_RESOURCES)
572
- res_path = list_key = res_type.to_s
573
- id_as_arg = false
574
- display_fields = nil
575
- adm_api = @api_v5
576
- special_query = :default
577
- available_commands = [].concat(Plugin::ALL_OPS)
578
- case res_type
579
- when :metadata_profiles
580
- res_path = 'configuration/metadata_profiles'
581
- list_key = 'profiles'
582
- when :alternate_addresses
583
- res_path = 'configuration/alternate_addresses'
584
- when :email_notifications
585
- list_key = false
586
- id_as_arg = 'type'
587
- when :accounts
588
- display_fields = Formatter.all_but('user_profile_data_attributes')
589
- when :oauth_clients
590
- display_fields = Formatter.all_but('public_key')
591
- adm_api = Rest.new(**@api_v5.params.merge(base_url: "#{@faspex5_api_base_url}/#{PATH_AUTH}"))
592
- when :shared_inboxes, :workgroups
593
- available_commands.push(:members, :saml_groups, :invite_external_collaborator)
594
- special_query = {'all': true}
595
- when :nodes
596
- available_commands.push(:shared_folders, :browse)
597
- end
598
- res_command = options.get_next_command(available_commands)
599
- case res_command
600
- when *Plugin::ALL_OPS
601
- return entity_command(res_command, adm_api, res_path, item_list_key: list_key, display_fields: display_fields, id_as_arg: id_as_arg) do |field, value|
602
- lookup_entity_by_field(
603
- type: res_type, real_path: res_path, field: field, value: value, query: special_query)['id']
604
- end
605
- when :shared_folders
606
- node_id = instance_identifier do |field, value|
607
- lookup_entity_by_field(type: res_type.to_s, field: field, value: value)['id']
608
- end
609
- sh_path = "#{res_path}/#{node_id}/shared_folders"
610
- return entity_action(adm_api, sh_path, item_list_key: 'shared_folders') do |field, value|
611
- lookup_entity_by_field(
612
- type: 'shared_folders', real_path: sh_path, field: field, value: value)['id']
613
- end
614
- when :browse
615
- return browse_folder("#{res_path}/#{instance_identifier}/browse")
616
- when :invite_external_collaborator
617
- shared_inbox_id = instance_identifier { |field, value| lookup_entity_by_field(type: res_type.to_s, field: field, value: value)['id']}
618
- creation_payload = value_create_modify(command: res_command, type: [Hash, String])
619
- creation_payload = {'email_address' => creation_payload} if creation_payload.is_a?(String)
620
- res_path = "#{res_type}/#{shared_inbox_id}/external_collaborator"
621
- result = adm_api.create(res_path, creation_payload)[:data]
622
- formatter.display_status(result['message'])
623
- result = lookup_entity_by_field(
624
- type: 'members', real_path: "#{res_type}/#{shared_inbox_id}/members", value: creation_payload['email_address'],
625
- query: {})
626
- return {type: :single_object, data: result}
627
- when :members, :saml_groups
628
- res_id = instance_identifier { |field, value| lookup_entity_by_field(type: res_type.to_s, field: field, value: value)['id']}
629
- res_prefix = "#{res_type}/#{res_id}"
630
- res_path = "#{res_prefix}/#{res_command}"
631
- list_key = res_command.to_s
632
- list_key = 'groups' if res_command.eql?(:saml_groups)
633
- sub_command = options.get_next_command(%i[create list modify delete])
634
- if sub_command.eql?(:create) && options.get_option(:value).nil?
635
- raise "use option 'value' to provide saml group_id and access (refer to API)" unless res_command.eql?(:members)
636
- # first arg is one user name or list of users
637
- users = options.get_next_argument('user id, or email, or list of')
638
- users = [users] unless users.is_a?(Array)
639
- users = users.map do |user|
640
- if (m = user.match(REGEX_LOOKUP_ID_BY_FIELD))
641
- lookup_entity_by_field(
642
- type: 'accounts', field: m[1], value: m[2],
643
- query: {type: Rest.array_params(%w{local_user saml_user self_registered_user external_user})})['id']
644
- else
645
- # it's the user id (not member id...)
646
- user
647
- end
648
- end
649
- access = options.get_next_argument('level', mandatory: false, expected: %i[submit_only standard shared_inbox_admin], default: :standard)
650
- # TODO: unshift to command line parameters instead of using deprecated option "value"
651
- options.set_option(:value, {user: users.map{|u|{id: u, access: access}}})
652
- end
653
- return entity_command(sub_command, adm_api, res_path, item_list_key: list_key) do |field, value|
654
- lookup_entity_by_field(
655
- type: 'accounts', field: field, value: value,
656
- query: {type: Rest.array_params(%w{local_user saml_user self_registered_user external_user})})['id']
657
- end
658
- end
659
- when :smtp
660
- smtp_path = 'configuration/smtp'
661
- smtp_cmd = options.get_next_command(%i[show create modify delete test])
662
- case smtp_cmd
663
- when :show
664
- return { type: :single_object, data: @api_v5.read(smtp_path)[:data] }
665
- when :create
666
- return { type: :single_object, data: @api_v5.create(smtp_path, value_create_modify(command: smtp_cmd))[:data] }
667
- when :modify
668
- return { type: :single_object, data: @api_v5.update(smtp_path, value_create_modify(command: smtp_cmd))[:data] }
669
- when :delete
670
- return { type: :single_object, data: @api_v5.delete(smtp_path)[:data] }
671
- when :test
672
- test_data = options.get_next_argument('Email or test data, see API')
673
- test_data = {test_email_recipient: test_data} if test_data.is_a?(String)
674
- creation = @api_v5.create(File.join(smtp_path, 'test'), test_data)[:data]
675
- result = wait_for_job(creation['job_id'])
676
- result['serialized_args'] = JSON.parse(result['serialized_args']) rescue result['serialized_args']
677
- return { type: :single_object, data: result }
678
- end
679
- end
753
+ return execute_admin
680
754
  when :invitations
681
755
  invitation_endpoint = 'invitations'
682
756
  invitation_command = options.get_next_command(%i[resend].concat(Plugin::ALL_OPS))
@@ -692,7 +766,9 @@ module Aspera
692
766
  else
693
767
  return entity_command(
694
768
  invitation_command, @api_v5, invitation_endpoint, item_list_key: invitation_endpoint,
695
- display_fields: %w[id public recipient_type recipient_name email_address])
769
+ display_fields: %w[id public recipient_type recipient_name email_address]) do |field, value|
770
+ lookup_entity_by_field(type: invitation_endpoint, field: field, value: value, query: {})['id']
771
+ end
696
772
  end
697
773
  when :gateway
698
774
  require 'aspera/faspex_gw'
@@ -714,9 +790,9 @@ module Aspera
714
790
  server.mount(uri.path, Faspex4PostProcServlet, parameters[:processing])
715
791
  server.start
716
792
  return Main.result_status('Gateway terminated')
717
- end # case command
718
- end # action
719
- end # Faspex5
720
- end # Plugins
721
- end # Cli
722
- end # Aspera
793
+ end
794
+ end
795
+ end
796
+ end
797
+ end
798
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/rest'
4
+ require 'aspera/nagios'
5
+ require 'aspera/cli/basic_auth_plugin'
6
+
7
+ module Aspera
8
+ module Cli
9
+ module Plugins
10
+ class Faspio < BasicAuthPlugin
11
+ class << self
12
+ def application_name
13
+ 'faspio Gateway'
14
+ end
15
+
16
+ def detect(base_url)
17
+ api = Rest.new(base_url: base_url)
18
+ ping_result = api.read('ping')
19
+ server_type = ping_result[:http]['Server']
20
+ return nil unless ping_result[:data].is_a?(Hash) && ping_result[:data].empty?
21
+ return nil unless server_type.is_a?(String) && server_type.include?('faspio')
22
+ return {
23
+ version: server_type.gsub(%r{^.*/}, ''),
24
+ url: base_url
25
+ }
26
+ end
27
+ end
28
+ ACTIONS = %i[health bridges].freeze
29
+
30
+ def initialize(**env)
31
+ super
32
+ options.declare(:auth, 'OAuth type of authentication', values: %i[jwt basic])
33
+ options.declare(:client_id, 'OAuth client identifier')
34
+ options.declare(:private_key, 'OAuth JWT RSA private key PEM value (prefix file path with @file:)')
35
+ options.declare(:passphrase, 'OAuth JWT RSA private key passphrase')
36
+ options.parse_options!
37
+ end
38
+
39
+ def execute_action
40
+ base_url = options.get_option(:url, mandatory: true)
41
+ api =
42
+ case options.get_option(:auth, mandatory: true)
43
+ when :basic
44
+ basic_auth_api
45
+ when :jwt
46
+ app_client_id = options.get_option(:client_id, mandatory: true)
47
+ Rest.new(
48
+ base_url: base_url,
49
+ auth: {
50
+ type: :oauth2,
51
+ grant_method: :jwt,
52
+ base_url: "#{base_url}/auth",
53
+ client_id: app_client_id,
54
+ use_query: true,
55
+ payload: {
56
+ iss: app_client_id, # issuer
57
+ sub: app_client_id # subject
58
+ },
59
+ private_key_obj: OpenSSL::PKey::RSA.new(options.get_option(:private_key, mandatory: true), options.get_option(:passphrase)),
60
+ headers: {typ: 'JWT'}
61
+ })
62
+ end
63
+ command = options.get_next_command(ACTIONS)
64
+ case command
65
+ when :health
66
+ nagios = Nagios.new
67
+ begin
68
+ result = api.read('ping')[:data]
69
+ if result.is_a?(Hash) && result.empty?
70
+ nagios.add_ok('api', 'answered ok')
71
+ else
72
+ nagios.add_critical('api', 'not expected answer')
73
+ end
74
+ rescue StandardError => e
75
+ nagios.add_critical('api', e.to_s)
76
+ end
77
+ return nagios.result
78
+ when :bridges
79
+ return entity_action(api, 'bridges')
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end