aspera-cli 4.25.0.pre2 → 4.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +3 -0
  4. data/CONTRIBUTING.md +60 -18
  5. data/README.md +164 -133
  6. data/lib/aspera/agent/factory.rb +9 -6
  7. data/lib/aspera/agent/transferd.rb +4 -4
  8. data/lib/aspera/api/aoc.rb +33 -24
  9. data/lib/aspera/api/ats.rb +1 -0
  10. data/lib/aspera/api/faspex.rb +11 -5
  11. data/lib/aspera/ascmd.rb +1 -1
  12. data/lib/aspera/ascp/installation.rb +5 -5
  13. data/lib/aspera/cli/formatter.rb +15 -62
  14. data/lib/aspera/cli/manager.rb +8 -42
  15. data/lib/aspera/cli/plugins/aoc.rb +48 -30
  16. data/lib/aspera/cli/plugins/ats.rb +30 -36
  17. data/lib/aspera/cli/plugins/base.rb +6 -6
  18. data/lib/aspera/cli/plugins/config.rb +5 -4
  19. data/lib/aspera/cli/plugins/faspex.rb +5 -3
  20. data/lib/aspera/cli/plugins/faspex5.rb +10 -8
  21. data/lib/aspera/cli/plugins/faspio.rb +3 -1
  22. data/lib/aspera/cli/plugins/node.rb +9 -6
  23. data/lib/aspera/cli/plugins/oauth.rb +12 -11
  24. data/lib/aspera/cli/plugins/preview.rb +2 -2
  25. data/lib/aspera/cli/transfer_agent.rb +1 -2
  26. data/lib/aspera/cli/version.rb +1 -1
  27. data/lib/aspera/command_line_builder.rb +5 -5
  28. data/lib/aspera/dot_container.rb +108 -0
  29. data/lib/aspera/id_generator.rb +7 -10
  30. data/lib/aspera/oauth/base.rb +25 -38
  31. data/lib/aspera/oauth/factory.rb +5 -6
  32. data/lib/aspera/oauth/generic.rb +1 -1
  33. data/lib/aspera/oauth/jwt.rb +1 -1
  34. data/lib/aspera/oauth/url_json.rb +4 -3
  35. data/lib/aspera/oauth/web.rb +2 -2
  36. data/lib/aspera/preview/file_types.rb +1 -1
  37. data/lib/aspera/rest.rb +5 -2
  38. data/lib/aspera/ssh.rb +6 -5
  39. data/lib/aspera/sync/conf.schema.yaml +2 -2
  40. data/lib/aspera/transfer/parameters.rb +6 -6
  41. data/lib/aspera/transfer/spec.schema.yaml +3 -3
  42. data/lib/aspera/transfer/spec_doc.rb +11 -21
  43. data/lib/aspera/uri_reader.rb +17 -3
  44. data.tar.gz.sig +0 -0
  45. metadata +2 -1
  46. metadata.gz.sig +0 -0
@@ -33,7 +33,7 @@ module Aspera
33
33
  # types of events for shared folder creation
34
34
  # Node events: permission.created permission.modified permission.deleted
35
35
  PERMISSIONS_CREATED = ['permission.created'].freeze
36
- # Special name when creating workspace shared folders
36
+ # Special user identifier when creating workspace shared folders
37
37
  ID_AK_ADMIN = 'ASPERA_ACCESS_KEY_ADMIN'
38
38
 
39
39
  private_constant :MAX_AOC_URL_REDIRECT,
@@ -48,11 +48,13 @@ module Aspera
48
48
  :ID_AK_ADMIN
49
49
 
50
50
  # various API scopes supported
51
- SCOPE_FILES_SELF = 'self'
52
- SCOPE_FILES_USER = 'user:all'
53
- SCOPE_FILES_ADMIN = 'admin:all'
54
- SCOPE_FILES_ADMIN_USER = 'admin-user:all'
55
- SCOPE_FILES_ADMIN_USER_USER = "#{SCOPE_FILES_ADMIN_USER}+#{SCOPE_FILES_USER}"
51
+ module Scope
52
+ SELF = 'self'
53
+ USER = 'user:all'
54
+ ADMIN = 'admin:all'
55
+ ADMIN_USER = 'admin-user:all'
56
+ ADMIN_USER_USER = "#{ADMIN_USER}+#{USER}"
57
+ end
56
58
  FILES_APP = 'files'
57
59
  PACKAGES_APP = 'packages'
58
60
  API_V1 = 'api/v1'
@@ -227,10 +229,12 @@ module Aspera
227
229
  @workspace_info = nil
228
230
  @home_info = nil
229
231
  auth_params = {
230
- type: :oauth2,
231
- client_id: client_id,
232
- client_secret: client_secret,
233
- scope: scope
232
+ type: :oauth2,
233
+ params: {
234
+ client_id: client_id,
235
+ client_secret: client_secret,
236
+ scope: scope
237
+ }
234
238
  }
235
239
  # analyze type of url
236
240
  url_info = AoC.link_info(url)
@@ -256,20 +260,20 @@ module Aspera
256
260
  Aspera.assert(username, 'Missing mandatory option: username', type: ParameterError)
257
261
  auth_params[:private_key_obj] = OpenSSL::PKey::RSA.new(private_key, passphrase)
258
262
  auth_params[:payload] = {
259
- iss: auth_params[:client_id], # issuer
263
+ iss: client_id, # issuer
260
264
  sub: username, # subject
261
265
  aud: JWT_AUDIENCE
262
266
  }
263
267
  # add jwt payload for global client id
264
- auth_params[:payload][:org] = url_info[:organization] if GLOBAL_CLIENT_APPS.include?(auth_params[:client_id])
268
+ auth_params[:payload][:org] = url_info[:organization] if GLOBAL_CLIENT_APPS.include?(client_id)
265
269
  auth_params[:cache_ids] = [url_info[:organization]]
266
270
  when :url_json
267
- auth_params[:url] = {grant_type: 'url_token'} # URL arguments
271
+ auth_params[:url] = {grant_type: 'url_token'} # Query arguments
268
272
  auth_params[:json] = {url_token: url_info[:token]} # JSON body
269
273
  # password protection of link
270
274
  auth_params[:json][:password] = password unless password.nil?
271
275
  # basic auth required for /token
272
- auth_params[:auth] = {type: :basic, username: auth_params[:client_id], password: auth_params[:client_secret]}
276
+ auth_params[:auth] = {type: :basic, username: client_id, password: client_secret}
273
277
  else Aspera.error_unexpected_value(auth_params[:grant_method]){'auth, use one of: :web, :jwt'}
274
278
  end
275
279
  super(
@@ -386,11 +390,12 @@ module Aspera
386
390
  @home_info
387
391
  end
388
392
 
389
- # @param node_id [String] identifier of node in AoC
390
- # @param workspace_id [String] workspace identifier
391
- # @param workspace_name [String] workspace name
392
- # @param scope e.g. Node::SCOPE_USER, or nil (requires secret)
393
- # @param package_info [Hash] created package information
393
+ # Return a Node API for given node id, in a given context (files, packages), for the given scope.
394
+ # @param node_id [String] identifier of node in AoC
395
+ # @param workspace_id [String,nil] workspace identifier
396
+ # @param workspace_name [String,nil] workspace name
397
+ # @param scope [String,nil] e.g. Node::SCOPE_USER, or Node::SCOPE_ADMIN, or nil (requires secret)
398
+ # @param package_info [Hash,nil] created package information
394
399
  # @returns [Node] a node API for access key
395
400
  def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: Node::SCOPE_USER, package_info: nil)
396
401
  Aspera.assert_type(node_id, String)
@@ -409,18 +414,22 @@ module Aspera
409
414
  app_info[:package_name] = package_info['name']
410
415
  end
411
416
  node_params = {base_url: node_info['url']}
412
- # if secret is available
413
- if scope.nil?
417
+ ak_secret = @secret_finder&.lookup_secret(url: node_info['url'], username: node_info['access_key'])
418
+ # If secret is available, or no scope, use basic auth
419
+ if scope.nil? || ak_secret
420
+ Aspera.assert(ak_secret, "Secret not found for access key #{node_info['access_key']}@#{node_info['url']}", type: Error)
414
421
  node_params[:auth] = {
415
422
  type: :basic,
416
423
  username: node_info['access_key'],
417
- password: @secret_finder&.lookup_secret(url: node_info['url'], username: node_info['access_key'], mandatory: true)
424
+ password: ak_secret
418
425
  }
419
426
  else
420
427
  # OAuth bearer token
421
428
  node_params[:auth] = auth_params.clone
422
- node_params[:auth][:scope] = Node.token_scope(node_info['access_key'], scope)
423
- # special header required for bearer token only
429
+ node_params[:auth][:params] ||= {}
430
+ node_params[:auth][:params][:scope] = Node.token_scope(node_info['access_key'], scope)
431
+ node_params[:auth][:params][:owner_access] = true if scope.eql?(Node::SCOPE_ADMIN)
432
+ # Special header required for bearer token only
424
433
  node_params[:headers] = {Node::HEADER_X_ASPERA_ACCESS_KEY => node_info['access_key']}
425
434
  end
426
435
  node_params[:app_info] = app_info
@@ -5,6 +5,7 @@ require 'aspera/rest'
5
5
 
6
6
  module Aspera
7
7
  module Api
8
+ # ATS API without authentication
8
9
  class Ats < Aspera::Rest
9
10
  SERVICE_BASE_URL = 'https://ats.aspera.io'
10
11
  # currently supported clouds
@@ -34,7 +34,7 @@ module Aspera
34
34
  query: {
35
35
  response_type: :code,
36
36
  state: @context,
37
- client_id: client_id,
37
+ client_id: params[:client_id],
38
38
  redirect_uri: @redirect_uri
39
39
  },
40
40
  exception: false,
@@ -46,7 +46,7 @@ module Aspera
46
46
  raise Error, info['action_message'] if info['action_message']
47
47
  Aspera.assert(info['code']){'Missing code in answer'}
48
48
  # Exchange code for token
49
- return create_token_call(optional_scope_client_id.merge(
49
+ return create_token_call(base_params.merge(
50
50
  grant_type: 'authorization_code',
51
51
  code: info['code'],
52
52
  redirect_uri: @redirect_uri
@@ -155,7 +155,9 @@ module Aspera
155
155
  base_url: "#{base_url}/#{PATH_AUTH}",
156
156
  grant_method: :faspex_pub_link,
157
157
  context: encoded_context,
158
- client_id: config[:client_id],
158
+ params: {
159
+ client_id: config[:client_id]
160
+ },
159
161
  redirect_uri: config[:redirect_uri]
160
162
  }
161
163
  }
@@ -177,7 +179,9 @@ module Aspera
177
179
  type: :oauth2,
178
180
  base_url: "#{url}/#{PATH_AUTH}",
179
181
  grant_method: :web,
180
- client_id: client_id,
182
+ params: {
183
+ client_id: client_id
184
+ },
181
185
  redirect_uri: redirect_uri
182
186
  }
183
187
  }
@@ -190,7 +194,9 @@ module Aspera
190
194
  type: :oauth2,
191
195
  base_url: "#{url}/#{PATH_AUTH}",
192
196
  grant_method: :jwt,
193
- client_id: client_id,
197
+ params: {
198
+ client_id: client_id
199
+ },
194
200
  payload: {
195
201
  iss: client_id, # issuer
196
202
  aud: client_id, # audience (this field is not clear...)
data/lib/aspera/ascmd.rb CHANGED
@@ -89,7 +89,7 @@ module Aspera
89
89
  # Version 2 allows use of reverse proxy with multiple addresses.
90
90
  # @param [Symbol] one of OPERATIONS
91
91
  # @param [Array] parameters for "as" command
92
- # @return result of command, type depends on command (bool, array, hash)
92
+ # @return [Boolean,Array,Hash] result of command, type depends on command
93
93
  def execute_single(action_sym, arguments, version: 1, host: nil)
94
94
  arguments = [] if arguments.nil?
95
95
  Log.log.debug{"execute_single:#{action_sym}:#{arguments}"}
@@ -294,11 +294,11 @@ module Aspera
294
294
  end
295
295
 
296
296
  # Retrieves ascp binary for current system architecture from URL or file
297
- # @param url [String] URL to SDK archive, or SpecialValues::DEF
298
- # @param folder [String] Destination folder path
299
- # @param backup [Bool] If destination folder exists, then rename
300
- # @param with_exe [Bool] If false, only retrieves files, but do not generate or restrict access
301
- # @param &block [Proc] A lambda that receives a file path from archive and tells destination sub folder(end with /) or file, or nil to not extract
297
+ # @param url [String] URL to SDK archive, or SpecialValues::DEF
298
+ # @param folder [String] Destination folder path
299
+ # @param backup [Boolean] If destination folder exists, then rename
300
+ # @param with_exe [Boolean] If false, only retrieves files, but do not generate or restrict access
301
+ # @param &block [Proc] A lambda that receives a file path from archive and tells destination sub folder(end with /) or file, or nil to not extract
302
302
  # @return [Array] name, ascp version (from execution), folder
303
303
  def install_sdk(url: nil, version: nil, folder: nil, backup: true, with_exe: true, &block)
304
304
  url = sdk_url_for_platform(version: version) if url.nil? || url.eql?('DEF')
@@ -8,6 +8,7 @@ require 'aspera/environment'
8
8
  require 'aspera/log'
9
9
  require 'aspera/assert'
10
10
  require 'aspera/markdown'
11
+ require 'aspera/dot_container'
11
12
  require 'terminal-table'
12
13
  require 'tty-spinner'
13
14
  require 'yaml'
@@ -64,9 +65,10 @@ module Aspera
64
65
  end
65
66
  end
66
67
 
68
+ # Give Markdown String, or matched data, return formatted string for terminal
67
69
  # used by spec_doc
68
70
  # @param match [MatchData,String]
69
- def markdown(match)
71
+ def markdown_text(match)
70
72
  if match.is_a?(String)
71
73
  match = Markdown::FORMATS.match(match)
72
74
  Aspera.assert(match)
@@ -84,11 +86,11 @@ module Aspera
84
86
  end
85
87
  end
86
88
 
87
- # replace empty values with a readable version on terminal
88
- def enhance_display_values_hash(input_hash)
89
- stack = [input_hash]
90
- until stack.empty?
91
- current = stack.pop
89
+ # Replace special values with a readable version on terminal
90
+ def replace_specific_for_terminal(input_hash)
91
+ hash_to_process = [input_hash]
92
+ until hash_to_process.empty?
93
+ current = hash_to_process.pop
92
94
  current.each do |key, value|
93
95
  case value
94
96
  when NilClass
@@ -100,73 +102,24 @@ module Aspera
100
102
  when Array
101
103
  if value.empty?
102
104
  current[key] = special_format('empty list')
105
+ elsif value.all?(String)
106
+ current[key] = value.join(',')
103
107
  else
104
108
  value.each do |item|
105
- stack.push(item) if item.is_a?(Hash)
109
+ hash_to_process.push(item) if item.is_a?(Hash)
106
110
  end
107
111
  end
108
112
  when Hash
109
113
  if value.empty?
110
114
  current[key] = special_format('empty dict')
111
115
  else
112
- stack.push(value)
116
+ hash_to_process.push(value)
113
117
  end
114
118
  end
115
119
  end
116
120
  end
117
121
  end
118
122
 
119
- # Given a list of string, display that list in a single cell
120
- def list_to_string(list)
121
- list.join(',')
122
- end
123
-
124
- # Build new prefix
125
- def add_prefix(prefix, key)
126
- [prefix, key].compact.join('.')
127
- end
128
-
129
- # Add elements of enumerator to the stack, in reverse order
130
- def add_elements(stack, prefix, enum)
131
- enum.reverse_each do |key, value|
132
- stack.push([add_prefix(prefix, key), value])
133
- end
134
- end
135
-
136
- # Flatten a Hash into single level hash
137
- def flatten_hash(input)
138
- Aspera.assert_type(input, Hash)
139
- return input if input.empty?
140
- flat = {}
141
- # tail (pop,push) contains the next element to display
142
- stack = [[nil, input]]
143
- until stack.empty?
144
- prefix, current = stack.pop
145
- # empty things will be displayed as such
146
- if current.respond_to?(:empty?) && current.empty?
147
- flat[prefix] = current
148
- next
149
- end
150
- case current
151
- when Hash
152
- add_elements(stack, prefix, current)
153
- when Array
154
- if current.none?{ |i| i.is_a?(Array) || i.is_a?(Hash)}
155
- flat[prefix] = list_to_string(current.map(&:to_s))
156
- elsif current.all?{ |i| i.is_a?(Hash) && i.keys == ['name']}
157
- flat[prefix] = list_to_string(current.map{ |i| i['name']})
158
- elsif current.all?{ |i| i.is_a?(Hash) && i.keys.sort == %w[name value]}
159
- add_elements(stack, prefix, current.each_with_object({}){ |i, h| h[i['name']] = i['value']})
160
- else
161
- add_elements(stack, prefix, current.each_with_index.map{ |v, i| [i, v]})
162
- end
163
- else
164
- flat[prefix] = current
165
- end
166
- end
167
- flat
168
- end
169
-
170
123
  def all_but(list)
171
124
  list = [list] unless list.is_a?(Array)
172
125
  return list.map{ |i| "#{FIELDS_LESS}#{i}"}.unshift(SpecialValues::ALL)
@@ -356,13 +309,13 @@ module Aspera
356
309
  if data.empty?
357
310
  display_message(:data, self.class.special_format('empty dict'))
358
311
  else
359
- data = self.class.flatten_hash(data) if @options[:flat_hash]
312
+ data = DotContainer.new(data).to_dotted if @options[:flat_hash]
360
313
  display_table([data], compute_fields([data], fields), single: true)
361
314
  end
362
315
  when :object_list
363
316
  # :object_list is an Array of Hash, where key=column name
364
317
  Aspera.assert_array_all(data, Hash){'result'}
365
- data = data.map{ |obj| self.class.flatten_hash(obj)} if @options[:flat_hash]
318
+ data = data.map{ |obj| DotContainer.new(obj).to_dotted} if @options[:flat_hash]
366
319
  display_table(data, compute_fields(data, fields), single: type.eql?(:single_object))
367
320
  when :value_list
368
321
  # :value_list is a simple array of values, name of column provided in `name`
@@ -474,7 +427,7 @@ module Aspera
474
427
  return
475
428
  end
476
429
  filter_columns_on_select(object_array)
477
- object_array.each{ |i| self.class.enhance_display_values_hash(i)}
430
+ object_array.each{ |i| self.class.replace_specific_for_terminal(i)}
478
431
  # if table has only one element, and only one field, display the value
479
432
  if object_array.length == 1 && fields.length == 1
480
433
  Log.log.debug("display_table: single element, field: #{fields.first}")
@@ -6,6 +6,7 @@ require 'aspera/colors'
6
6
  require 'aspera/secret_hider'
7
7
  require 'aspera/log'
8
8
  require 'aspera/assert'
9
+ require 'aspera/dot_container'
9
10
  require 'io/console'
10
11
  require 'optparse'
11
12
 
@@ -486,11 +487,12 @@ module Aspera
486
487
  Log.log.trace1('After parse')
487
488
  rescue OptionParser::InvalidOption => e
488
489
  Log.log.trace1{"InvalidOption #{e}".red}
490
+ # An option like --a.b.c=d does: a={"b":{"c":ext_val(d)}}
489
491
  if (m = e.args.first.match(/^--([a-z\-]+)\.([^=]+)=(.+)$/))
490
492
  option, path, value = m.captures
491
493
  option_sym = self.class.option_line_to_name(option).to_sym
492
494
  if @declared_options.key?(option_sym)
493
- set_option(option_sym, dotted_to_extended(path, value, get_option(option_sym)), where: 'dotted')
495
+ set_option(option_sym, DotContainer.dotted_to_container(path, smart_convert(value), get_option(option_sym)), where: 'dotted')
494
496
  retry
495
497
  end
496
498
  end
@@ -561,14 +563,14 @@ module Aspera
561
563
 
562
564
  # Read remaining args and build an Array or Hash
563
565
  # @param value [nil] Argument to `@:` extended value
564
- def args_as_extended(value)
566
+ def args_as_extended(arg)
565
567
  # This extended value does not take args (`@:`)
566
- ExtendedValue.assert_no_value(value, :p)
568
+ ExtendedValue.assert_no_value(arg, :p)
567
569
  result = nil
568
570
  get_next_argument(:args, multiple: true).each do |arg|
569
571
  Aspera.assert(arg.include?(OPTION_VALUE_SEPARATOR)){"Positional argument: #{arg} does not inlude #{OPTION_VALUE_SEPARATOR}"}
570
- path, raw = arg.split(OPTION_VALUE_SEPARATOR, 2)
571
- result = dotted_to_extended(path, raw, result)
572
+ path, value = arg.split(OPTION_VALUE_SEPARATOR, 2)
573
+ result = DotContainer.dotted_to_container(path, smart_convert(value), result)
572
574
  end
573
575
  result
574
576
  end
@@ -590,40 +592,6 @@ module Aspera
590
592
  end
591
593
  end
592
594
 
593
- # Convert `String` to `Integer`, or keep `String` if not `Integer`
594
- def int_or_string(value)
595
- Integer(value, exception: false) || value
596
- end
597
-
598
- def new_hash_or_array_from_key(key)
599
- key.is_a?(Integer) ? [] : {}
600
- end
601
-
602
- def array_requires_integer_index!(container, index)
603
- Aspera.assert(container.is_a?(Hash) || index.is_a?(Integer)){'Using String index when Integer index used previously'}
604
- end
605
-
606
- # Insert extended value `value` into struct `result` at `path`
607
- # @param path [String] dotted path
608
- # @param value [String] Smart expression
609
- # @param result [NilClass, Hash, Array] current value
610
- # @return [Hash, Array]
611
- def dotted_to_extended(path, value, result = nil)
612
- # Typed keys
613
- keys = path.split(OPTION_DOTTED_SEPARATOR).map{ |k| int_or_string(k)}
614
- # Create, or re-use first level container
615
- current = (result ||= new_hash_or_array_from_key(keys.first))
616
- # walk the path, and create sub-containers if necessary
617
- keys.each_cons(2) do |k, next_k|
618
- array_requires_integer_index!(current, k)
619
- current = (current[k] ||= new_hash_or_array_from_key(next_k))
620
- end
621
- # Assign value at last index
622
- array_requires_integer_index!(current, keys.last)
623
- current[keys.last] = smart_convert(value)
624
- result
625
- end
626
-
627
595
  # generate command line option from option symbol
628
596
  def symbol_to_option(symbol, opt_val = nil)
629
597
  result = [OPTION_PREFIX, symbol.to_s.gsub(OPTION_SEP_SYMBOL, OPTION_SEP_LINE)].join
@@ -677,15 +645,13 @@ module Aspera
677
645
  OPTION_SEP_SYMBOL = '_'
678
646
  # Option value separator on command line, e.g. in --option-blah=foo, the "="
679
647
  OPTION_VALUE_SEPARATOR = '='
680
- # "." : An option like --a.b.c=d does: a={"b":{"c":ext_val(d)}}
681
- OPTION_DOTTED_SEPARATOR = '.'
682
648
  # Starts an option, e.g. in --option-blah, the two first "--"
683
649
  OPTION_PREFIX = '--'
684
650
  # when this is alone, this stops option processing
685
651
  OPTIONS_STOP = '--'
686
652
  SOURCE_USER = 'cmdline' # cspell:disable-line
687
653
 
688
- private_constant :BOOL_YES, :BOOL_NO, :FALSE_VALUES, :TRUE_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_DOTTED_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER
654
+ private_constant :BOOL_YES, :BOOL_NO, :FALSE_VALUES, :TRUE_VALUES, :OPTION_SEP_LINE, :OPTION_SEP_SYMBOL, :OPTION_VALUE_SEPARATOR, :OPTION_PREFIX, :OPTIONS_STOP, :SOURCE_USER
689
655
  end
690
656
  end
691
657
  end
@@ -82,9 +82,10 @@ module Aspera
82
82
  }
83
83
  end
84
84
 
85
- # @param base [String] Base folder path
86
- # @param always [Bool] `true` always add number, `false` only if base folder already exists
87
- # @return [String] Folder path that does jot exist, with possible .<number> extension
85
+ # Get folder path that does not exist
86
+ # @param base [String] Base folder path
87
+ # @param always [Boolean] `true` always add number, `false` only if base folder already exists
88
+ # @return [String] Folder path that does not exist, with possible .<number> extension
88
89
  def next_available_folder(base, always: false)
89
90
  counter = always ? 1 : 0
90
91
  loop do
@@ -175,7 +176,7 @@ module Aspera
175
176
  auto_set_jwt = true
176
177
  Aspera.error_not_implemented
177
178
  # aoc_api.oauth.grant_method = :web
178
- # aoc_api.oauth.scope = Api::AoC::SCOPE_FILES_ADMIN
179
+ # aoc_api.oauth.scope = Api::AoC::Scope::ADMIN
179
180
  # aoc_api.oauth.specific_parameters[:redirect_uri] = REDIRECT_LOCALHOST
180
181
  end
181
182
  myself = aoc_api.read('self')
@@ -206,6 +207,7 @@ module Aspera
206
207
  @cache_workspace_info = nil
207
208
  @cache_home_node_file = nil
208
209
  @cache_api_aoc = nil
210
+ @scope = Api::AoC::Scope::USER
209
211
  options.declare(:workspace, 'Name of workspace', allowed: [String, NilClass], default: Api::AoC::DEFAULT_WORKSPACE)
210
212
  options.declare(:new_user_option, 'New user creation option for unknown package recipients', allowed: Hash)
211
213
  options.declare(:validate_metadata, 'Validate shared inbox metadata', allowed: Allowed::TYPES_BOOLEAN, default: true)
@@ -215,39 +217,48 @@ module Aspera
215
217
  Node.declare_options(options)
216
218
  end
217
219
 
220
+ # Change API scope for subsequent calls, re-instantiate API object
221
+ # @param new_scope [String] New scope
222
+ def change_api_scope(new_scope)
223
+ @cache_api_aoc = nil
224
+ @scope = new_scope
225
+ end
226
+
227
+ # create an API object with the same options, but with a different subpath
228
+ # @param aoc_base_path [String] New subpath
229
+ # @return [Api::AoC] API object for AoC (is Rest)
218
230
  def api_from_options(aoc_base_path)
219
- # create an API object with the same options, but with a different subpath
220
231
  return new_with_options(
221
232
  Api::AoC,
222
- base: {
233
+ kwargs: {
234
+ scope: @scope,
223
235
  subpath: aoc_base_path,
224
236
  secret_finder: config
225
237
  },
226
- add: {
227
- scope: Api::AoC::SCOPE_FILES_USER,
238
+ option: {
228
239
  workspace: nil
229
240
  }
230
241
  )
231
242
  end
232
243
 
233
244
  # AoC Rest object
234
- # @return [Rest]
245
+ # @return [Api::AoC] API object for AoC (is Rest)
235
246
  def aoc_api
236
247
  if @cache_api_aoc.nil?
237
248
  @cache_api_aoc = api_from_options(Api::AoC::API_V1)
238
- organization = @cache_api_aoc.read('organization')
239
- if organization['http_gateway_enabled'] && organization['http_gateway_server_url']
240
- transfer.httpgw_url_cb = lambda{organization['http_gateway_server_url']}
249
+ transfer.httpgw_url_cb = lambda do
250
+ organization = @cache_api_aoc.read('organization')
241
251
  # @cache_api_aoc.current_user_info['connect_disabled']
252
+ organization['http_gateway_server_url'] if organization['http_gateway_enabled'] && organization['http_gateway_server_url']
242
253
  end
243
254
  end
244
255
  return @cache_api_aoc
245
256
  end
246
257
 
247
258
  # Generate or update Hash with workspace id and name (option), if not already set
248
- # @param hash [Hash, Nil] set in provided hash
249
- # @param string [TrueClass,FalseClass] true to set key as string, else as symbol
250
- # @param name [Bool] include name
259
+ # @param hash [Hash,nil] Optional base hash (modified)
260
+ # @param string [Boolean] true to set key as string, else as symbol
261
+ # @param name [Boolean] include name
251
262
  # @return [Hash] with key `workspace_[id,name]` (symbol or string) only if defined
252
263
  def workspace_id_hash(hash: nil, string: false, name: false)
253
264
  info = aoc_api.workspace
@@ -320,8 +331,11 @@ module Aspera
320
331
  NODE4_EXT_COMMANDS = %i[transfer].concat(Node::COMMANDS_GEN4).freeze
321
332
  private_constant :NODE4_EXT_COMMANDS
322
333
 
323
- # @param file_id [String] root file id for the operation (can be AK root, or other, e.g. package, or link)
324
- # @param scope [String] node scope, or nil (admin)
334
+ # Execute a node gen4 command
335
+ # @param command_repo [Symbol] command to execute
336
+ # @param node_id [String] Node identifier
337
+ # @param file_id [String] Root file id for the operation (can be AK root, or other, e.g. package, or link). If `nil` use AK root file id.
338
+ # @param scope [String] node scope (Node::SCOPE_<USER|ADMIN>), or nil (requires secret)
325
339
  def execute_nodegen4_command(command_repo, node_id, file_id: nil, scope: nil)
326
340
  top_node_api = aoc_api.node_api_from(
327
341
  node_id: node_id,
@@ -423,7 +437,7 @@ module Aspera
423
437
  when :client_registration_token then default_fields.push('value', 'data.client_subject_scopes', 'created_at')
424
438
  when :contact
425
439
  default_fields = %w[source_type source_id name email]
426
- default_query = {'include_only_user_personal_contacts' => true} if aoc_api.oauth.scope == Api::AoC::SCOPE_FILES_USER
440
+ default_query = {'include_only_user_personal_contacts' => true} if @scope == Api::AoC::Scope::USER
427
441
  when :node then default_fields.push('name', 'host', 'access_key')
428
442
  when :operation then default_fields = nil
429
443
  when :short_link then default_fields.push('short_url', 'data.url_token_data.purpose')
@@ -456,7 +470,7 @@ module Aspera
456
470
  return Main.result_success
457
471
  when :do
458
472
  command_repo = options.get_next_command(NODE4_EXT_COMMANDS)
459
- return execute_nodegen4_command(command_repo, res_id)
473
+ return execute_nodegen4_command(command_repo, res_id, scope: Api::Node::SCOPE_ADMIN)
460
474
  when :bearer_token
461
475
  node_api = aoc_api.node_api_from(
462
476
  node_id: res_id,
@@ -515,13 +529,15 @@ module Aspera
515
529
  end
516
530
  end
517
531
 
518
- ADMIN_ACTIONS = %i[ats resource usage_reports analytics subscription auth_providers].concat(ADMIN_OBJECTS).freeze
532
+ ADMIN_ACTIONS = %i[ats bearer_token resource usage_reports analytics subscription auth_providers].concat(ADMIN_OBJECTS).freeze
519
533
 
520
534
  def execute_admin_action
521
- # default scope to admin
522
- aoc_api.oauth.scope = Api::AoC::SCOPE_FILES_ADMIN if options.get_option(:scope).nil?
535
+ # change scope to admin
536
+ change_api_scope(Api::AoC::Scope::ADMIN)
523
537
  command_admin = options.get_next_command(ADMIN_ACTIONS)
524
538
  case command_admin
539
+ when :bearer_token
540
+ return Main.result_text(aoc_api.oauth.authorization)
525
541
  when :resource
526
542
  Log.log.warn('resource command is deprecated (4.18), directly use the specific command instead')
527
543
  return execute_resource_action(options.get_next_argument('resource', accept_list: ADMIN_OBJECTS))
@@ -650,13 +666,13 @@ module Aspera
650
666
  when :ats
651
667
  ats_api = Rest.new(**aoc_api.params.deep_merge({
652
668
  base_url: "#{aoc_api.base_url}/admin/ats/pub/v1",
653
- auth: {scope: Api::AoC::SCOPE_FILES_ADMIN_USER}
669
+ auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
654
670
  }))
655
- return Ats.new(context: context).execute_action_gen(ats_api)
671
+ return Ats.new(context: context, api: ats_api).execute_action
656
672
  when :analytics
657
673
  analytics_api = Rest.new(**aoc_api.params.deep_merge({
658
674
  base_url: "#{aoc_api.base_url.gsub('/api/v1', '')}/analytics/v2",
659
- auth: {scope: Api::AoC::SCOPE_FILES_ADMIN_USER}
675
+ auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
660
676
  }))
661
677
  command_analytics = options.get_next_command(%i[application_events transfers files])
662
678
  case command_analytics
@@ -681,13 +697,13 @@ module Aspera
681
697
  start_date_persistency = PersistencyActionOnce.new(
682
698
  manager: persistency,
683
699
  data: saved_date,
684
- id: IdGenerator.from_list([
700
+ id: IdGenerator.from_list(
685
701
  'aoc_ana_date',
686
702
  options.get_option(:url, mandatory: true),
687
703
  aoc_api.workspace[:name],
688
704
  event_resource_type.to_s,
689
705
  event_resource_id
690
- ])
706
+ )
691
707
  )
692
708
  start_date_time = saved_date.first
693
709
  stop_date_time = Time.now.utc.strftime('%FT%T.%LZ')
@@ -840,9 +856,10 @@ module Aspera
840
856
  manager: persistency,
841
857
  data: [],
842
858
  id: IdGenerator.from_list(
843
- ['aoc_recv',
844
- options.get_option(:url, mandatory: true),
845
- aoc_api.workspace[:id]].concat(aoc_api.additional_persistence_ids)
859
+ 'aoc_recv',
860
+ options.get_option(:url, mandatory: true),
861
+ aoc_api.workspace[:id],
862
+ aoc_api.additional_persistence_ids
846
863
  )
847
864
  )
848
865
  end
@@ -1079,6 +1096,7 @@ module Aspera
1079
1096
  end
1080
1097
  end
1081
1098
  when :automation
1099
+ change_api_scope(Api::AoC::Scope::ADMIN_USER)
1082
1100
  Log.log.warn('BETA: work under progress')
1083
1101
  # automation api is not in the same place
1084
1102
  automation_api = Rest.new(**aoc_api.params, base_url: aoc_api.base_url.gsub('/api/', '/automation/'))