aspera-cli 4.24.2 → 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 (81) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +1070 -758
  4. data/CONTRIBUTING.md +130 -115
  5. data/README.md +961 -623
  6. data/lib/aspera/agent/direct.rb +14 -12
  7. data/lib/aspera/agent/factory.rb +9 -6
  8. data/lib/aspera/agent/transferd.rb +8 -8
  9. data/lib/aspera/api/aoc.rb +104 -67
  10. data/lib/aspera/api/ats.rb +1 -0
  11. data/lib/aspera/api/cos_node.rb +3 -2
  12. data/lib/aspera/api/faspex.rb +17 -10
  13. data/lib/aspera/api/node.rb +10 -12
  14. data/lib/aspera/ascmd.rb +2 -3
  15. data/lib/aspera/ascp/installation.rb +60 -46
  16. data/lib/aspera/ascp/management.rb +9 -5
  17. data/lib/aspera/assert.rb +28 -6
  18. data/lib/aspera/cli/error.rb +4 -2
  19. data/lib/aspera/cli/extended_value.rb +94 -62
  20. data/lib/aspera/cli/formatter.rb +44 -58
  21. data/lib/aspera/cli/main.rb +21 -14
  22. data/lib/aspera/cli/manager.rb +317 -250
  23. data/lib/aspera/cli/plugins/alee.rb +3 -3
  24. data/lib/aspera/cli/plugins/aoc.rb +139 -78
  25. data/lib/aspera/cli/plugins/ats.rb +30 -36
  26. data/lib/aspera/cli/plugins/base.rb +68 -55
  27. data/lib/aspera/cli/plugins/config.rb +90 -100
  28. data/lib/aspera/cli/plugins/console.rb +15 -9
  29. data/lib/aspera/cli/plugins/cos.rb +1 -1
  30. data/lib/aspera/cli/plugins/faspex.rb +39 -30
  31. data/lib/aspera/cli/plugins/faspex5.rb +57 -52
  32. data/lib/aspera/cli/plugins/faspio.rb +10 -7
  33. data/lib/aspera/cli/plugins/httpgw.rb +3 -2
  34. data/lib/aspera/cli/plugins/node.rb +140 -125
  35. data/lib/aspera/cli/plugins/oauth.rb +13 -12
  36. data/lib/aspera/cli/plugins/orchestrator.rb +116 -33
  37. data/lib/aspera/cli/plugins/preview.rb +28 -48
  38. data/lib/aspera/cli/plugins/server.rb +9 -10
  39. data/lib/aspera/cli/plugins/shares.rb +77 -43
  40. data/lib/aspera/cli/sync_actions.rb +49 -38
  41. data/lib/aspera/cli/transfer_agent.rb +16 -35
  42. data/lib/aspera/cli/version.rb +1 -1
  43. data/lib/aspera/cli/wizard.rb +8 -5
  44. data/lib/aspera/command_line_builder.rb +24 -21
  45. data/lib/aspera/coverage.rb +6 -2
  46. data/lib/aspera/dot_container.rb +108 -0
  47. data/lib/aspera/environment.rb +71 -84
  48. data/lib/aspera/faspex_gw.rb +1 -1
  49. data/lib/aspera/faspex_postproc.rb +1 -1
  50. data/lib/aspera/id_generator.rb +7 -10
  51. data/lib/aspera/keychain/factory.rb +1 -2
  52. data/lib/aspera/keychain/macos_security.rb +2 -2
  53. data/lib/aspera/log.rb +2 -1
  54. data/lib/aspera/markdown.rb +31 -0
  55. data/lib/aspera/nagios.rb +6 -5
  56. data/lib/aspera/oauth/base.rb +41 -64
  57. data/lib/aspera/oauth/factory.rb +6 -7
  58. data/lib/aspera/oauth/generic.rb +1 -1
  59. data/lib/aspera/oauth/jwt.rb +1 -1
  60. data/lib/aspera/oauth/url_json.rb +6 -4
  61. data/lib/aspera/oauth/web.rb +2 -2
  62. data/lib/aspera/preview/file_types.rb +24 -38
  63. data/lib/aspera/preview/terminal.rb +95 -29
  64. data/lib/aspera/preview/utils.rb +6 -5
  65. data/lib/aspera/products/connect.rb +3 -3
  66. data/lib/aspera/rest.rb +54 -39
  67. data/lib/aspera/rest_error_analyzer.rb +4 -4
  68. data/lib/aspera/ssh.rb +10 -6
  69. data/lib/aspera/ssl.rb +41 -0
  70. data/lib/aspera/sync/conf.schema.yaml +184 -36
  71. data/lib/aspera/sync/database.rb +2 -1
  72. data/lib/aspera/sync/operations.rb +128 -72
  73. data/lib/aspera/transfer/parameters.rb +9 -10
  74. data/lib/aspera/transfer/spec.rb +2 -3
  75. data/lib/aspera/transfer/spec.schema.yaml +52 -22
  76. data/lib/aspera/transfer/spec_doc.rb +20 -30
  77. data/lib/aspera/uri_reader.rb +18 -4
  78. data/lib/transferd_pb.rb +2 -2
  79. data.tar.gz.sig +0 -0
  80. metadata +34 -6
  81. metadata.gz.sig +0 -0
@@ -5,7 +5,9 @@ require 'aspera/log'
5
5
  require 'aspera/assert'
6
6
  require 'rbconfig'
7
7
  require 'singleton'
8
+ require 'open3'
8
9
  require 'English'
10
+ require 'shellwords'
9
11
 
10
12
  # cspell:words MEBI mswin bccwin
11
13
 
@@ -40,100 +42,85 @@ module Aspera
40
42
 
41
43
  RB_EXT = '.rb'
42
44
 
45
+ PROCESS_MODES = %i[execute background capture].freeze
46
+
43
47
  class << self
44
48
  def ruby_version
45
49
  return RbConfig::CONFIG['RUBY_PROGRAM_VERSION']
46
50
  end
47
51
 
48
- # empty variable binding for secure eval
52
+ # Empty variable binding for secure eval
49
53
  def empty_binding
50
54
  return Kernel.binding
51
55
  end
52
56
 
53
- # secure execution of Ruby code
57
+ # Secure execution of Ruby code
58
+ # @param code [String] Ruby code to execute
59
+ # @param file [String] File name for error reporting
60
+ # @param line [Integer] Line number for error reporting
54
61
  def secure_eval(code, file, line)
55
62
  Kernel.send('lave'.reverse, code, empty_binding, file, line)
56
63
  end
57
64
 
58
- # Generate log line for external program with arguments
59
- # @param env [Hash, nil] environment variables
60
- # @param exec [String] path to executable
61
- # @param args [Array, nil] arguments
62
- # @return [String] log line with environment, program and arguments
63
- def log_spawn(exec:, args: nil, env: nil)
64
- [
65
- 'execute:'.red,
66
- env&.map{ |k, v| "#{k}=#{Shellwords.shellescape(v)}"},
67
- Shellwords.shellescape(exec),
68
- args&.map{ |a| Shellwords.shellescape(a)}
69
- ].compact.flatten.join(' ')
70
- end
71
-
72
- # Start process in background
73
- # caller can call Process.wait on returned value
74
- # @param exec [String] path to executable
75
- # @param args [Array, nil] arguments for executable
76
- # @param env [Hash, nil] environment variables
77
- # @param options [Hash, nil] spawn options
78
- # @return [String] PID of process
79
- # @raise [Exception] if problem
80
- def secure_spawn(exec:, args: nil, env: nil, **options)
81
- Aspera.assert_type(exec, String)
82
- Aspera.assert_type(args, Array, NilClass)
83
- Aspera.assert_type(env, Hash, NilClass)
84
- Aspera.assert_type(options, Hash, NilClass)
85
- Log.log.debug{log_spawn(exec: exec, args: args, env: env)}
86
- # start ascp in separate process
87
- spawn_args = []
88
- spawn_args.push(env) unless env.nil?
89
- spawn_args.push([exec, exec])
90
- spawn_args.concat(args) unless args.nil?
91
- opts = {close_others: true}
92
- opts.merge!(options) unless options.nil?
93
- ascp_pid = Process.spawn(*spawn_args, **opts)
94
- Log.log.debug{"pid: #{ascp_pid}"}
95
- return ascp_pid
65
+ # Build argv for Process.spawn / Kernel.system (no shell)
66
+ # @param cmd [Array] Command and arguments
67
+ # @param kwargs [Hash] Additional arguments to `secure_execute`
68
+ def build_spawn_argv(cmd, kwargs)
69
+ env = kwargs.delete(:env)
70
+ argv = []
71
+ argv << env if env
72
+ argv << [cmd.first, cmd.first] # no shell, preserve argv[0]
73
+ argv.concat(cmd.drop(1))
74
+ argv
96
75
  end
97
76
 
98
- # start process and wait for completion
99
- # @param env [Hash, nil] environment variables
100
- # @param exec [String] path to executable
101
- # @param args [Array, nil] arguments
102
- # @return [String] PID of process
103
- def secure_execute(exec:, args: nil, env: nil, **system_args)
104
- Aspera.assert_type(exec, String)
105
- Aspera.assert_type(args, Array, NilClass)
106
- Aspera.assert_type(env, Hash, NilClass)
107
- Log.log.debug{log_spawn(exec: exec, args: args, env: env)}
108
- # start in separate process
109
- spawn_args = []
110
- spawn_args.push(env) unless env.nil?
111
- # ensure no shell expansion
112
- spawn_args.push([exec, exec])
113
- spawn_args.concat(args) unless args.nil?
114
- kwargs = {exception: true}
115
- kwargs.merge!(system_args)
116
- Kernel.system(*spawn_args, **kwargs)
117
- nil
118
- end
119
-
120
- # Execute process and capture stdout
121
- # @param exec [String] path to executable
122
- # @param args [Array] arguments to executable
123
- # @param opts [Hash] options to capture3
124
- # @return stdout of executable or raise exception
125
- def secure_capture(exec:, args: [], exception: true, **opts)
126
- Aspera.assert_type(exec, String)
127
- Aspera.assert_type(args, Array)
128
- Aspera.assert_type(opts, Hash)
129
- Log.log.debug{log_spawn(exec: exec, args: args)}
130
- Log.dump(:opts, opts, level: :trace2)
131
- Log.dump(:ENV, ENV.to_h, level: :trace1)
132
- stdout, stderr, status = Open3.capture3(exec, *args, **opts)
133
- Log.log.debug{"status=#{status}, stderr=#{stderr}"}
134
- Log.log.trace1{"stdout=#{stdout}"}
135
- raise "process failed: #{status.exitstatus} (#{stderr})" if !status.success? && exception
136
- return stdout
77
+ # Execute a process securely (no shell)
78
+ # mode:
79
+ # :execute -> Kernel.system, return nil
80
+ # :background -> Process.spawn, return pid
81
+ # :capture -> Open3.capture3, return stdout
82
+ # @param cmd [Array] Command and arguments
83
+ # @param env [Hash, nil] Environment variables
84
+ # @param mode [Symbol] Execution mode (see above)
85
+ # @param kwargs [Hash] Additional arguments to underlying method, includes:
86
+ # :exception [Boolean] for :capture mode, raise error if process fails
87
+ # :close_others [Boolean] for :background mode
88
+ # :env [Hash] for :execute mode
89
+ def secure_execute(*cmd, mode: :execute, **kwargs)
90
+ cmd = cmd.map(&:to_s)
91
+ Aspera.assert(cmd.size.positive?, type: ArgumentError){'executable must be present'}
92
+ Aspera.assert_values(mode, PROCESS_MODES, type: ArgumentError){'mode'}
93
+ Log.log.debug do
94
+ parts = [mode.to_s, 'command:']
95
+ kwargs[:env]&.each{ |k, v| parts << "#{k}=#{Shellwords.shellescape(v.to_s)}"}
96
+ cmd.each{ |a| parts << Shellwords.shellescape(a)}
97
+ parts.join(' ')
98
+ end
99
+ case mode
100
+ when :execute
101
+ # https://docs.ruby-lang.org/en/master/Kernel.html#method-i-system
102
+ # https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options
103
+ kwargs[:exception] = true unless kwargs.key?(:exception)
104
+ Kernel.system(*build_spawn_argv(cmd, kwargs), **kwargs)
105
+ when :background
106
+ # https://docs.ruby-lang.org/en/master/Process.html#method-c-spawn
107
+ # https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options
108
+ kwargs[:close_others] = true unless kwargs.key?(:close_others)
109
+ pid = Process.spawn(*build_spawn_argv(cmd, kwargs), **kwargs)
110
+ Log.dump(:pid, pid)
111
+ pid
112
+ when :capture
113
+ # https://docs.ruby-lang.org/en/master/Open3.html#method-c-capture3
114
+ # https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options
115
+ argv = [kwargs.delete(:env)].compact + cmd
116
+ exception = kwargs.delete(:exception){true}
117
+ stdout, stderr, status = Open3.capture3(*argv, **kwargs)
118
+ Log.log.debug{"status=#{status}, stderr=#{stderr}"}
119
+ Log.log.trace1{"stdout=#{stdout}"}
120
+ raise "Process failed: #{status.exitstatus} (#{stderr})" if exception && !status.success?
121
+ stdout
122
+ else Aspera.error_unreachable_line
123
+ end
137
124
  end
138
125
 
139
126
  # Write content to a file, with restricted access
@@ -266,9 +253,9 @@ module Aspera
266
253
  # @param uri [String] the URI to open
267
254
  def open_uri_graphical(uri)
268
255
  case @os
269
- when Environment::OS_MACOS then return self.class.secure_execute(exec: 'open', args: [uri.to_s])
270
- when Environment::OS_WINDOWS then return self.class.secure_execute(exec: 'start', args: ['explorer', %Q{"#{uri}"}])
271
- when Environment::OS_LINUX then return self.class.secure_execute(exec: 'xdg-open', args: [uri.to_s])
256
+ when Environment::OS_MACOS then return self.class.secure_execute('open', uri.to_s)
257
+ when Environment::OS_WINDOWS then return self.class.secure_execute('start', 'explorer', %Q{"#{uri}"})
258
+ when Environment::OS_LINUX then return self.class.secure_execute('xdg-open', uri.to_s)
272
259
  else Assert.error_unexpected_value(os){'no graphical open method'}
273
260
  end
274
261
  end
@@ -276,9 +263,9 @@ module Aspera
276
263
  # open a file in an editor
277
264
  def open_editor(file_path)
278
265
  if ENV.key?('EDITOR')
279
- self.class.secure_execute(exec: ENV['EDITOR'], args: [file_path.to_s])
266
+ self.class.secure_execute(ENV['EDITOR'], file_path.to_s)
280
267
  elsif @os.eql?(Environment::OS_WINDOWS)
281
- self.class.secure_execute(exec: 'notepad.exe', args: [%Q{"#{file_path}"}])
268
+ self.class.secure_execute('notepad.exe', %Q{"#{file_path}"})
282
269
  else
283
270
  open_uri_graphical(file_path.to_s)
284
271
  end
@@ -55,7 +55,7 @@ module Aspera
55
55
  content_type: Rest::MIME_JSON,
56
56
  body: {paths: [{'destination'=>'/'}]},
57
57
  headers: {'Accept' => Rest::MIME_JSON}
58
- )[:data]
58
+ )
59
59
  transfer_spec.delete('authentication')
60
60
  # but we place it in a Faspex package creation response
61
61
  return {
@@ -50,7 +50,7 @@ module Aspera
50
50
  Log.dump(:webhook_parameters, webhook_parameters)
51
51
  # env expects only strings
52
52
  environment = webhook_parameters.each_with_object({}){ |(k, v), h| h[k] = v.to_s}
53
- post_proc_pid = Environment.secure_spawn(env: environment, exec: script_path)
53
+ post_proc_pid = Environment.secure_execute(script_path, mode: :background, env: environment)
54
54
  Timeout.timeout(@parameters[:timeout_seconds]) do
55
55
  # "wait" for process to avoid zombie
56
56
  Process.wait(post_proc_pid)
@@ -9,19 +9,16 @@ module Aspera
9
9
  class << self
10
10
  # Generate an ID from a list of object IDs
11
11
  # The generated ID is safe as file name
12
- # @param object_id [Array<String>, String] the object IDs
12
+ # @param ids [Array] the object IDs (can be nested, will be flattened, and nils removed)
13
13
  # @return [String] the generated ID
14
- def from_list(object_id)
14
+ def from_list(*ids)
15
15
  safe_char = Environment.instance.safe_filename_character
16
- if object_id.is_a?(Array)
17
- # compact: remove nils
18
- object_id = object_id.flatten.compact.map do |i|
19
- i.is_a?(String) && i.start_with?('https://') ? URI.parse(i).host : i.to_s
20
- end.join(safe_char)
21
- end
22
- Aspera.assert_type(object_id, String)
16
+ # compact: remove nils
17
+ id = ids.flatten.compact.map do |i|
18
+ i.is_a?(String) && i.start_with?('https://') ? URI.parse(i).host : i.to_s
19
+ end.join(safe_char)
23
20
  # keep dot for extension only (nicer)
24
- return Environment.instance.sanitized_filename(object_id.gsub('.', safe_char)).downcase
21
+ return Environment.instance.sanitized_filename(id.gsub('.', safe_char)).downcase
25
22
  end
26
23
  end
27
24
  end
@@ -12,8 +12,7 @@ module Aspera
12
12
  # @param folder [String] folder to store the vault (if needed)
13
13
  # @param password [String] password to open the vault
14
14
  def create(info, name, folder, password)
15
- Aspera.assert_type(info, Hash)
16
- Aspera.assert(info.values.all?(String)){'vault info shall have only string values'}
15
+ Aspera.assert_hash_all(info, Symbol, String){'vault info shall have only string values'}
17
16
  info = info.symbolize_keys
18
17
  vault_type = info.delete(:type)
19
18
  Aspera.assert_values(vault_type, LIST.map(&:to_s)){'vault.type'}
@@ -49,7 +49,7 @@ module Aspera
49
49
  options[:path] = uri.path unless ['', '/'].include?(uri.path)
50
50
  options[:port] = uri.port unless uri.port.eql?(443) && !url.include?(':443/')
51
51
  end
52
- command_args = [command]
52
+ command_args = [SECURITY_UTILITY, command]
53
53
  options&.each do |k, v|
54
54
  Aspera.assert(supported.key?(k)){"unknown option: #{k}"}
55
55
  next if v.nil?
@@ -57,7 +57,7 @@ module Aspera
57
57
  command_args.push(v.shellescape) unless v.empty?
58
58
  end
59
59
  command_args.push(last_opt) unless last_opt.nil?
60
- return Environment.secure_capture(exec: SECURITY_UTILITY, args: command_args)
60
+ return Environment.secure_execute(*command_args, mode: :capture)
61
61
  end
62
62
 
63
63
  def key_chains(output)
data/lib/aspera/log.rb CHANGED
@@ -81,8 +81,9 @@ module Aspera
81
81
  # @param object [Hash, nil] Data to dump
82
82
  # @param level [Symbol] Debug level
83
83
  # @param block [Proc, nil] Give computed object
84
- def dump(name, object = nil, level: :debug)
84
+ def dump(name, object = nil, level: :debug, &block)
85
85
  return unless instance.logger.send(:"#{level}?")
86
+ Aspera.assert(object.nil? || block.nil?){'Use either object, or block, not both'}
86
87
  object = yield if block_given?
87
88
  instance.logger.send(level, obj_dump(name, object))
88
89
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aspera
4
+ # Formatting for Markdown
5
+ class Markdown
6
+ # Matches: **bold**, `code`, or an HTML entity (&amp;, &#169;, &#x1F4A9;)
7
+ FORMATS = /(?:\*\*(?<bold>[^*]+?)\*\*)|(?:`(?<code>[^`]+)`)|&(?<entity>(?:[A-Za-z][A-Za-z0-9]{1,31}|#\d{1,7}|#x[0-9A-Fa-f]{1,6}));/m
8
+ HTML_BREAK = '<br/>'
9
+
10
+ class << self
11
+ # Generate markdown from the provided 2D table
12
+ def table(table)
13
+ # get max width of each columns
14
+ col_widths = table.transpose.map do |col|
15
+ [col.flat_map{ |c| c.to_s.delete('`').split(HTML_BREAK).map(&:size)}.max, 80].min
16
+ end
17
+ headings = table.shift
18
+ table.unshift(col_widths.map{ |col_width| '-' * col_width})
19
+ table.unshift(headings)
20
+ lines = table.map{ |line| "| #{line.map{ |i| i.to_s.gsub('\\', '\\\\').gsub('|', '\|')}.join(' | ')} |\n"}
21
+ lines[1] = lines[1].tr(' ', '-')
22
+ return lines.join.chomp
23
+ end
24
+
25
+ # Generate markdown list from the provided list
26
+ def list(items)
27
+ items.map{ |i| "- #{i}"}.join("\n")
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/aspera/nagios.rb CHANGED
@@ -21,7 +21,7 @@ module Aspera
21
21
  end
22
22
 
23
23
  class << self
24
- # process results of a analysis and display status and exit with code
24
+ # Process results of a analysis and display status and exit with code
25
25
  def process(data)
26
26
  Aspera.assert_type(data, Array)
27
27
  Aspera.assert(!data.empty?){'data is empty'}
@@ -54,7 +54,7 @@ module Aspera
54
54
  @data = []
55
55
  end
56
56
 
57
- # compare remote time with local time
57
+ # Compare remote time with local time
58
58
  def check_time_offset(remote_date, component)
59
59
  # check date if specified : 2015-10-13T07:32:01Z
60
60
  remote_time = Time.parse(remote_date)
@@ -76,10 +76,11 @@ module Aspera
76
76
  # TODO: check on database if latest version
77
77
  end
78
78
 
79
- # translate for display
80
- def result
79
+ # Readable status list
80
+ # @return [Array] of Hash
81
+ def status_list
81
82
  Aspera.assert(!@data.empty?){'missing result'}
82
- {type: :object_list, data: @data.map{ |i| {'status' => LEVELS[i[:code]].to_s, 'component' => i[:comp], 'message' => i[:msg]}}}
83
+ @data.map{ |i| {'status' => LEVELS[i[:code]].to_s, 'component' => i[:comp], 'message' => i[:msg]}}
83
84
  end
84
85
  end
85
86
  end
@@ -14,83 +14,58 @@ module Aspera
14
14
  # Bearer Token Usage: https://tools.ietf.org/html/rfc6750
15
15
  class Base
16
16
  Aspera.require_method!(:create_token)
17
- # @param ** Parameters for REST
18
- # @param client_id [String, nil]
19
- # @param client_secret [String, nil]
20
- # @param scope [String, nil]
21
- # @param use_query [bool] Provide parameters in query instead of body
22
- # @param path_token [String] API end point to create a token from base URL
23
- # @param token_field [String] Field in result that contains the token
24
- # @param cache_ids [Array, nil] List of unique identifiers for cache id generation
17
+ # @param params [Hash] Parameters for token creation (client_id, client_secret, scope, etc...)
18
+ # @param use_query [Boolean] Provide parameters in query instead of body
19
+ # @param path_token [String] API end point to create a token from base URL
20
+ # @param token_field [String] Field in result that contains the token
21
+ # @param cache_ids [Array] List of unique identifiers for cache id generation
22
+ # @param **rest_params [Hash] Parameters for REST
25
23
  def initialize(
26
- client_id: nil,
27
- client_secret: nil,
28
- scope: nil,
24
+ params: {},
29
25
  use_query: false,
30
26
  path_token: 'token',
31
27
  token_field: Factory::TOKEN_FIELD,
32
- cache_ids: nil,
28
+ cache_ids: [],
33
29
  **rest_params
34
30
  )
31
+ Aspera.assert_type(params, Hash)
32
+ Aspera.assert_type(cache_ids, Array)
35
33
  # This is the OAuth API
36
34
  @api = Rest.new(**rest_params)
37
- @scope = nil
38
- @token_cache_id = nil
35
+ @params = params.dup.freeze
39
36
  @path_token = path_token
40
37
  @token_field = token_field
41
- @client_id = client_id
42
- @client_secret = client_secret
43
38
  @use_query = use_query
44
- @base_cache_ids = cache_ids.nil? ? [] : cache_ids.clone
45
- Aspera.assert_type(@base_cache_ids, Array)
46
- # TODO: this shall be done in class, using cache_ids
47
- @base_cache_ids.push(@api.auth_params[:username]) if @api.auth_params.key?(:username)
48
- @base_cache_ids.compact!
49
- @base_cache_ids.freeze
50
- self.scope = scope
39
+ # TODO: :username and :scope shall be done in class, using cache_ids
40
+ @token_cache_id = Factory.cache_id(@api.base_url, self.class, cache_ids, rest_params[:username], @params[:scope])
51
41
  end
52
42
 
53
- # Scope can be modified after creation, then update identifier for cache
54
- def scope=(scope)
55
- @scope = scope
56
- # generate token unique identifier for persistency (memory/disk cache)
57
- @token_cache_id = Factory.cache_id(@api.base_url, self.class, @base_cache_ids, @scope)
58
- end
59
-
60
- attr_reader :scope, :api, :path_token, :client_id
43
+ # The OAuth API Object
44
+ attr_reader :api
45
+ # Sub path to generate token
46
+ attr_reader :path_token
47
+ # Parameters to generate token
48
+ attr_reader :params
61
49
 
62
- # helper method to create token as per RFC
50
+ # Helper method to create token as per RFC
51
+ # @return [HTTPResponse]
52
+ # @raise RestError if not 2XX code
63
53
  def create_token_call(creation_params)
64
54
  Log.log.debug{'Generating a new token'.bg_green}
65
- payload = if @use_query
66
- {
67
- query: creation_params
68
- }
69
- else
70
- {
71
- content_type: Rest::MIME_WWW,
72
- body: creation_params
73
- }
74
- end
75
- return @api.call(
76
- operation: 'POST',
77
- subpath: @path_token,
78
- headers: {'Accept' => Rest::MIME_JSON},
79
- **payload
80
- )
55
+ return @api.create(@path_token, nil, query: creation_params, ret: :resp) if @use_query
56
+ return @api.create(@path_token, creation_params, content_type: Rest::MIME_WWW, ret: :resp)
81
57
  end
82
58
 
59
+ # Create base parameters for token creation calls
83
60
  # @param add_secret [Boolean] Add secret in default call parameters
84
61
  # @return [Hash] Optional general parameters
85
- def optional_scope_client_id(add_secret: false)
86
- call_params = {}
87
- call_params[:scope] = @scope unless @scope.nil?
88
- call_params[:client_id] = @client_id unless @client_id.nil?
89
- call_params[:client_secret] = @client_secret unless !add_secret || @client_id.nil? || @client_secret.nil?
62
+ def base_params(add_secret: false)
63
+ call_params = @params.dup
64
+ call_params.delete(:client_secret) unless add_secret
90
65
  return call_params
91
66
  end
92
67
 
93
- # @return value suitable for Authorization header
68
+ # @return [String] value suitable for Authorization header
94
69
  def authorization(**kwargs)
95
70
  return OAuth::Factory.bearer_authorization(token(**kwargs))
96
71
  end
@@ -122,17 +97,18 @@ module Aspera
122
97
  Factory.instance.persist_mgr.delete(@token_cache_id)
123
98
  token_data = nil
124
99
  # lets try the existing refresh token
100
+ # NOTE: AoC admin token has no refresh, and lives by default 1800secs
125
101
  if !refresh_token.nil?
126
- Log.log.info{"refresh=[#{refresh_token}]".bg_green}
127
- # NOTE: AoC admin token has no refresh, and lives by default 1800secs
128
- resp = create_token_call(optional_scope_client_id(add_secret: true).merge(grant_type: 'refresh_token', refresh_token: refresh_token))
129
- if resp[:http].code.start_with?('2')
130
- # save only if success
131
- json_data = resp[:http].body
102
+ Log.log.info{"refresh token=[#{refresh_token}]".bg_green}
103
+ begin
104
+ http = create_token_call(base_params(add_secret: true).merge(grant_type: 'refresh_token', refresh_token: refresh_token))
105
+ # Save only if success
106
+ json_data = http.body
132
107
  token_data = JSON.parse(json_data)
133
108
  Factory.instance.persist_mgr.put(@token_cache_id, json_data)
134
- else
135
- Log.log.debug{"refresh failed: #{resp[:http].body}".bg_red}
109
+ rescue => e
110
+ # Refresh token can fail.
111
+ Log.log.warn{"Refresh failed: #{e}"}
136
112
  end
137
113
  end
138
114
  end
@@ -140,8 +116,9 @@ module Aspera
140
116
 
141
117
  # no cache, nor refresh: generate a token
142
118
  if token_data.nil?
143
- resp = create_token
144
- json_data = resp[:http].body
119
+ # Call the method-specific token creation
120
+ # which returns the result of create_token_call
121
+ json_data = create_token.body
145
122
  token_data = JSON.parse(json_data)
146
123
  Factory.instance.persist_mgr.put(@token_cache_id, json_data)
147
124
  end
@@ -36,14 +36,13 @@ module Aspera
36
36
  return authorization[SPACE_BEARER_AUTH_SCHEME.length..-1]
37
37
  end
38
38
 
39
+ # Generate a unique cache id for a token creator
40
+ # @param url [String] Base URL of the OAuth server
41
+ # @param creator_class [Class] Class of the token creator
42
+ # @param params [Array] List of parameters (can be nested) to uniquely identify the token
39
43
  # @return a unique cache identifier
40
44
  def cache_id(url, creator_class, *params)
41
- return IdGenerator.from_list([
42
- PERSIST_CATEGORY_TOKEN,
43
- url,
44
- Factory.class_to_id(creator_class)
45
- ] +
46
- params)
45
+ return IdGenerator.from_list(PERSIST_CATEGORY_TOKEN, url, Factory.class_to_id(creator_class), params)
47
46
  end
48
47
 
49
48
  # @return snake version of class name
@@ -156,7 +155,7 @@ module Aspera
156
155
  def register_token_creator(creator_class)
157
156
  Aspera.assert_type(creator_class, Class)
158
157
  id = Factory.class_to_id(creator_class)
159
- Log.log.debug{"registering token creator #{id}"}
158
+ Log.log.debug{"registering creator for #{id}"}
160
159
  @token_type_classes[id] = creator_class
161
160
  end
162
161
 
@@ -23,7 +23,7 @@ module Aspera
23
23
  end
24
24
 
25
25
  def create_token
26
- return create_token_call(optional_scope_client_id.merge(@create_params))
26
+ return create_token_call(base_params.merge(@create_params))
27
27
  end
28
28
  end
29
29
  Factory.instance.register_token_creator(Generic)
@@ -61,7 +61,7 @@ module Aspera
61
61
  Log.log.debug{"private=[#{@private_key_obj}]"}
62
62
  assertion = JWT.encode(jwt_payload, @private_key_obj, 'RS256', @headers)
63
63
  Log.log.debug{"assertion=[#{assertion}]"}
64
- return create_token_call(optional_scope_client_id.merge(grant_type: GRANT_TYPE, assertion: assertion))
64
+ return create_token_call(base_params.merge(grant_type: GRANT_TYPE, assertion: assertion))
65
65
  end
66
66
  end
67
67
  Factory.instance.register_token_creator(Jwt)
@@ -6,8 +6,9 @@ module Aspera
6
6
  module OAuth
7
7
  # This class is used to create a token using a JSON body and a URL
8
8
  class UrlJson < Base
9
- # @param url URL to send the JSON body
10
- # @param json JSON body to send
9
+ # @param url [Hash] Query parameters to send
10
+ # @param json [Hash] Body parameters to send as JSON
11
+ # @param generic_params [Hash] Generic parameters for OAuth::Base
11
12
  def initialize(
12
13
  url:,
13
14
  json:,
@@ -22,10 +23,11 @@ module Aspera
22
23
  api.call(
23
24
  operation: 'POST',
24
25
  subpath: path_token,
25
- query: @query.merge(scope: scope), # scope is here because it may change over time (node)
26
+ query: @query.merge(scope: params[:scope]), # scope is here because it may change over time (node)
26
27
  content_type: Rest::MIME_JSON,
27
28
  body: @body,
28
- headers: {'Accept' => Rest::MIME_JSON}
29
+ headers: {'Accept' => Rest::MIME_JSON},
30
+ ret: :resp
29
31
  )
30
32
  end
31
33
  end
@@ -32,7 +32,7 @@ module Aspera
32
32
  random_state = SecureRandom.uuid
33
33
  login_page_url = Rest.build_uri(
34
34
  "#{api.base_url}/#{@path_authorize}",
35
- optional_scope_client_id.merge(response_type: 'code', redirect_uri: @redirect_uri, state: random_state)
35
+ base_params.merge(response_type: 'code', redirect_uri: @redirect_uri, state: random_state)
36
36
  )
37
37
  # here, we need a human to authorize on a web page
38
38
  Log.log.info{"login_page_url=#{login_page_url}".bg_red.gray}
@@ -44,7 +44,7 @@ module Aspera
44
44
  received_params = web_server.received_request
45
45
  Aspera.assert(random_state.eql?(received_params['state'])){'wrong received state'}
46
46
  # exchange code for token
47
- return create_token_call(optional_scope_client_id(add_secret: true).merge(
47
+ return create_token_call(base_params(add_secret: true).merge(
48
48
  grant_type: 'authorization_code',
49
49
  code: received_params['code'],
50
50
  redirect_uri: @redirect_uri