aspera-cli 4.21.1 → 4.22.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 (105) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +1 -1
  4. data/CHANGELOG.md +52 -22
  5. data/CONTRIBUTING.md +69 -148
  6. data/README.md +929 -668
  7. data/bin/ascli +5 -14
  8. data/bin/asession +1 -3
  9. data/examples/get_proto_file.rb +4 -3
  10. data/examples/proxy.pac +20 -20
  11. data/lib/aspera/agent/base.rb +11 -5
  12. data/lib/aspera/agent/connect.rb +30 -28
  13. data/lib/aspera/agent/{alpha.rb → desktop.rb} +35 -31
  14. data/lib/aspera/agent/direct.rb +141 -121
  15. data/lib/aspera/agent/httpgw.rb +22 -26
  16. data/lib/aspera/agent/node.rb +14 -11
  17. data/lib/aspera/agent/transferd.rb +30 -19
  18. data/lib/aspera/api/alee.rb +1 -1
  19. data/lib/aspera/api/aoc.rb +6 -6
  20. data/lib/aspera/api/cos_node.rb +2 -2
  21. data/lib/aspera/api/httpgw.rb +7 -3
  22. data/lib/aspera/api/node.rb +10 -8
  23. data/lib/aspera/ascmd.rb +3 -3
  24. data/lib/aspera/ascp/installation.rb +53 -72
  25. data/lib/aspera/ascp/management.rb +1 -1
  26. data/lib/aspera/assert.rb +11 -2
  27. data/lib/aspera/cli/error.rb +2 -2
  28. data/lib/aspera/cli/extended_value.rb +46 -21
  29. data/lib/aspera/cli/formatter.rb +55 -48
  30. data/lib/aspera/cli/hints.rb +1 -1
  31. data/lib/aspera/cli/info.rb +1 -0
  32. data/lib/aspera/cli/main.rb +192 -170
  33. data/lib/aspera/cli/manager.rb +18 -18
  34. data/lib/aspera/cli/plugin.rb +23 -20
  35. data/lib/aspera/cli/plugin_factory.rb +1 -1
  36. data/lib/aspera/cli/plugins/alee.rb +1 -1
  37. data/lib/aspera/cli/plugins/aoc.rb +247 -159
  38. data/lib/aspera/cli/plugins/ats.rb +19 -17
  39. data/lib/aspera/cli/plugins/config.rb +76 -113
  40. data/lib/aspera/cli/plugins/console.rb +5 -3
  41. data/lib/aspera/cli/plugins/faspex.rb +39 -35
  42. data/lib/aspera/cli/plugins/faspex5.rb +111 -84
  43. data/lib/aspera/cli/plugins/faspio.rb +13 -1
  44. data/lib/aspera/cli/plugins/httpgw.rb +13 -1
  45. data/lib/aspera/cli/plugins/node.rb +312 -182
  46. data/lib/aspera/cli/plugins/orchestrator.rb +34 -40
  47. data/lib/aspera/cli/plugins/preview.rb +3 -3
  48. data/lib/aspera/cli/plugins/server.rb +6 -6
  49. data/lib/aspera/cli/plugins/shares.rb +5 -5
  50. data/lib/aspera/cli/sync_actions.rb +19 -18
  51. data/lib/aspera/cli/transfer_agent.rb +5 -5
  52. data/lib/aspera/cli/transfer_progress.rb +2 -2
  53. data/lib/aspera/cli/version.rb +1 -1
  54. data/lib/aspera/command_line_builder.rb +116 -95
  55. data/lib/aspera/coverage.rb +8 -5
  56. data/lib/aspera/environment.rb +26 -17
  57. data/lib/aspera/faspex_gw.rb +14 -14
  58. data/lib/aspera/faspex_postproc.rb +10 -11
  59. data/lib/aspera/hash_ext.rb +4 -14
  60. data/lib/aspera/json_rpc.rb +1 -1
  61. data/lib/aspera/keychain/encrypted_hash.rb +47 -34
  62. data/lib/aspera/keychain/factory.rb +41 -0
  63. data/lib/aspera/keychain/hashicorp_vault.rb +71 -0
  64. data/lib/aspera/keychain/macos_security.rb +19 -11
  65. data/lib/aspera/log.rb +28 -34
  66. data/lib/aspera/nagios.rb +6 -6
  67. data/lib/aspera/node_simulator.rb +8 -8
  68. data/lib/aspera/oauth/base.rb +14 -7
  69. data/lib/aspera/oauth/factory.rb +5 -6
  70. data/lib/aspera/oauth/url_json.rb +6 -6
  71. data/lib/aspera/persistency_action_once.rb +6 -4
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/generator.rb +13 -10
  74. data/lib/aspera/preview/options.rb +16 -16
  75. data/lib/aspera/preview/terminal.rb +4 -4
  76. data/lib/aspera/preview/utils.rb +15 -17
  77. data/lib/aspera/products/connect.rb +35 -1
  78. data/lib/aspera/products/{alpha.rb → desktop.rb} +3 -3
  79. data/lib/aspera/products/transferd.rb +9 -2
  80. data/lib/aspera/proxy_auto_config.rb +2 -2
  81. data/lib/aspera/rest.rb +56 -47
  82. data/lib/aspera/rest_errors_aspera.rb +1 -1
  83. data/lib/aspera/secret_hider.rb +12 -5
  84. data/lib/aspera/ssh.rb +4 -4
  85. data/lib/aspera/temp_file_manager.rb +5 -4
  86. data/lib/aspera/transfer/convert.rb +29 -0
  87. data/lib/aspera/transfer/error_info.rb +66 -66
  88. data/lib/aspera/transfer/parameters.rb +13 -68
  89. data/lib/aspera/transfer/spec.rb +5 -6
  90. data/lib/aspera/transfer/spec.schema.yaml +753 -0
  91. data/lib/aspera/transfer/spec_doc.rb +62 -0
  92. data/lib/aspera/transfer/sync.rb +23 -72
  93. data/lib/aspera/transfer/sync_instance.schema.yaml +13 -0
  94. data/lib/aspera/transfer/sync_session.schema.yaml +79 -0
  95. data/lib/aspera/transfer/uri.rb +6 -6
  96. data/lib/aspera/uri_reader.rb +18 -1
  97. data/lib/aspera/web_auth.rb +1 -1
  98. data/lib/aspera/web_server_simple.rb +53 -44
  99. data.tar.gz.sig +0 -0
  100. metadata +28 -165
  101. metadata.gz.sig +0 -0
  102. data/examples/build_exec +0 -74
  103. data/examples/build_exec_rubyc +0 -40
  104. data/examples/build_package.sh +0 -28
  105. data/lib/aspera/transfer/spec.yaml +0 -718
@@ -17,9 +17,9 @@ module Aspera
17
17
  CONTENT_KEYS = %i[label username password url description].freeze
18
18
  FILE_KEYS = %w[version type cipher data].sort.freeze
19
19
  private_constant :LEGACY_CIPHER_NAME, :DEFAULT_CIPHER_NAME, :FILE_TYPE, :CONTENT_KEYS, :FILE_KEYS
20
- def initialize(path, current_password)
21
- Aspera.assert_type(path, String){'path to vault file'}
22
- @path = path
20
+ def initialize(file:, password:)
21
+ Aspera.assert_type(file, String){'path to vault file'}
22
+ @path = file
23
23
  @all_secrets = {}
24
24
  @cipher_name = DEFAULT_CIPHER_NAME
25
25
  vault_encrypted_data = nil
@@ -37,31 +37,27 @@ module Aspera
37
37
  end
38
38
  end
39
39
  # setting password also creates the cipher
40
- self.password = current_password
40
+ @cipher = cipher(password)
41
41
  if !vault_encrypted_data.nil?
42
42
  @all_secrets = YAML.load_stream(@cipher.decrypt(vault_encrypted_data)).first
43
43
  end
44
44
  end
45
45
 
46
- # set the password and cipher
47
- def password=(new_password)
48
- # number of bits in second position
49
- key_bytes = DEFAULT_CIPHER_NAME.split('-')[1].to_i / Environment::BITS_PER_BYTE
50
- # derive key from passphrase, add trailing zeros
51
- key = "#{new_password}#{"\x0" * key_bytes}"[0..(key_bytes - 1)]
52
- Log.log.trace1{"secret=[#{key}],#{key.length}"}
53
- @cipher = SymmetricEncryption.cipher = SymmetricEncryption::Cipher.new(cipher_name: DEFAULT_CIPHER_NAME, key: key, encoding: :none)
46
+ def info
47
+ return {
48
+ file: @path
49
+ }
54
50
  end
55
51
 
56
- # save current data to file with format
57
- def save
58
- vault_info = {
59
- 'version' => '1.0.0',
60
- 'type' => FILE_TYPE,
61
- 'cipher' => @cipher_name,
62
- 'data' => @cipher.encrypt(YAML.dump(@all_secrets))
63
- }
64
- File.write(@path, YAML.dump(vault_info))
52
+ def list
53
+ result = []
54
+ @all_secrets.each do |label, values|
55
+ normal = values.symbolize_keys
56
+ normal[:label] = label
57
+ CONTENT_KEYS.each{ |k| normal[k] = '' unless normal.key?(k)}
58
+ result.push(normal)
59
+ end
60
+ return result
65
61
  end
66
62
 
67
63
  # set a secret
@@ -79,14 +75,10 @@ module Aspera
79
75
  save
80
76
  end
81
77
 
82
- def list
83
- result = []
84
- @all_secrets.each do |label, values|
85
- normal = values.symbolize_keys
86
- normal[:label] = label
87
- CONTENT_KEYS.each{|k|normal[k] = '' unless normal.key?(k)}
88
- result.push(normal)
89
- end
78
+ def get(label:, exception: true)
79
+ Aspera.assert(@all_secrets.key?(label)){"Label not found: #{label}"} if exception
80
+ result = @all_secrets[label].clone
81
+ result[:label] = label if result.is_a?(Hash)
90
82
  return result
91
83
  end
92
84
 
@@ -95,11 +87,32 @@ module Aspera
95
87
  save
96
88
  end
97
89
 
98
- def get(label:, exception: true)
99
- Aspera.assert(@all_secrets.key?(label)){"Label not found: #{label}"} if exception
100
- result = @all_secrets[label].clone
101
- result[:label] = label if result.is_a?(Hash)
102
- return result
90
+ def change_password(password)
91
+ @cipher = cipher(password)
92
+ save
93
+ end
94
+
95
+ private
96
+
97
+ # set the password and cipher
98
+ def cipher(new_password)
99
+ # number of bits in second position
100
+ key_bytes = @cipher_name.split('-')[1].to_i / Environment::BITS_PER_BYTE
101
+ # derive key from passphrase, add trailing zeros
102
+ key = "#{new_password}#{"\x0" * key_bytes}"[0..(key_bytes - 1)]
103
+ Log.log.trace1{"secret=[#{key}],#{key.length}"}
104
+ SymmetricEncryption.cipher = SymmetricEncryption::Cipher.new(cipher_name: @cipher_name, key: key, encoding: :none)
105
+ end
106
+
107
+ # save current data to file with format
108
+ def save
109
+ vault_info = {
110
+ 'version' => '1.0.0',
111
+ 'type' => FILE_TYPE,
112
+ 'cipher' => @cipher_name,
113
+ 'data' => @cipher.encrypt(YAML.dump(@all_secrets))
114
+ }
115
+ File.write(@path, YAML.dump(vault_info))
103
116
  end
104
117
  end
105
118
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aspera
4
+ module Keychain
5
+ # Manage secrets in a Hashicorp Vault
6
+ class Factory
7
+ LIST = %i[file system vault].freeze
8
+ class << self
9
+ def create(info, name, folder, password)
10
+ Aspera.assert_type(info, Hash)
11
+ Aspera.assert(info.values.all?(String)){'vault info shall have only string values'}
12
+ info = info.symbolize_keys
13
+ vault_type = info.delete(:type)
14
+ Aspera.assert_values(vault_type, LIST.map(&:to_s)){'vault.type'}
15
+ case vault_type
16
+ when 'file'
17
+ info[:file] ||= 'vault.bin'
18
+ info[:file] = File.join(folder, info[:file]) unless File.absolute_path?(info[:file])
19
+ Aspera.assert(!password.nil?){'please provide password'}
20
+ info[:password] = password
21
+ # this module requires compilation, so it is optional
22
+ require 'aspera/keychain/encrypted_hash'
23
+ @vault = Keychain::EncryptedHash.new(**info)
24
+ when 'system'
25
+ case Environment.os
26
+ when Environment::OS_MACOS
27
+ info[:name] ||= name
28
+ @vault = Keychain::MacosSystem.new(**info)
29
+ else
30
+ raise 'not implemented for this OS'
31
+ end
32
+ when 'vault'
33
+ require 'aspera/keychain/hashicorp_vault'
34
+ @vault = Keychain::HashicorpVault.new(**info)
35
+ else Aspera.error_unexpected_value(vault_type)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/environment'
4
+ require 'aspera/log'
5
+ require 'aspera/assert'
6
+ require 'vault'
7
+
8
+ module Aspera
9
+ module Keychain
10
+ # Manage secrets in a Hashicorp Vault
11
+ class HashicorpVault
12
+ SECRET_PATH = 'secret/data/'
13
+
14
+ private_constant :SECRET_PATH
15
+
16
+ def initialize(url:, token:)
17
+ Vault.configure do |config|
18
+ config.address = url
19
+ config.token = token
20
+ end
21
+ end
22
+
23
+ def info
24
+ {
25
+ url: Vault.address,
26
+ password: Vault.auth_token
27
+ }
28
+ end
29
+
30
+ def list
31
+ metadata_path = SECRET_PATH.sub('/data/', '/metadata/')
32
+ return Vault.logical.list(metadata_path).filter_map do |label|
33
+ get(label: label).merge(label: label)
34
+ end
35
+ end
36
+
37
+ # Set a secret
38
+ # @param options [Hash] with keys :label, :username, :password, :url, :description
39
+ def set(options)
40
+ label = options.fetch(:label)
41
+ data = {
42
+ username: options[:username],
43
+ password: options[:password],
44
+ url: options[:url],
45
+ description: options[:description]
46
+ }.compact
47
+ Vault.logical.write(path(label), data: data)
48
+ end
49
+
50
+ def get(label:, exception: true)
51
+ secret = Vault.logical.read(path(label))
52
+ if secret.nil?
53
+ raise "Secret '#{label}' not found" if exception
54
+ return nil
55
+ end
56
+ return secret.data[:data]
57
+ end
58
+
59
+ def delete(label:)
60
+ path = path(label)
61
+ Vault.logical.delete(path)
62
+ end
63
+
64
+ private
65
+
66
+ def path(label)
67
+ "#{SECRET_PATH}#{label}"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -60,7 +60,7 @@ module Aspera
60
60
  end
61
61
 
62
62
  def key_chains(output)
63
- output.split("\n").collect { |line| new(line.strip.gsub(/^"|"$/, '')) }
63
+ output.split("\n").collect{ |line| new(line.strip.gsub(/^"|"$/, ''))}
64
64
  end
65
65
 
66
66
  def default
@@ -77,7 +77,7 @@ module Aspera
77
77
  end
78
78
 
79
79
  def by_name(name)
80
- list.find{|kc|kc.path.end_with?("/#{name}.keychain-db")}
80
+ list.find{ |kc| kc.path.end_with?("/#{name}.keychain-db")}
81
81
  end
82
82
  end
83
83
  attr_reader :path
@@ -123,15 +123,28 @@ module Aspera
123
123
  end
124
124
 
125
125
  class MacosSystem
126
- def initialize(name=nil, _password=nil)
127
- @keychain = name.nil? ? MacosSecurity::Keychain.default_keychain : MacosSecurity::Keychain.by_name(name)
126
+ OPTIONS = %i[label username password url description].freeze
127
+ def initialize(name: nil)
128
+ @keychain_name = name.nil? ? 'default keychain' : name
129
+ @keychain = name.nil? ? MacosSecurity::Keychain.default : MacosSecurity::Keychain.by_name(name)
128
130
  raise "no such keychain #{name}" if @keychain.nil?
129
131
  end
130
132
 
133
+ def info
134
+ return {
135
+ keychain: @keychain_name
136
+ }
137
+ end
138
+
139
+ def list
140
+ # the only way to list is `dump-keychain` which triggers security alert
141
+ raise 'list not implemented, use macos keychain app'
142
+ end
143
+
131
144
  def set(options)
132
145
  Aspera.assert_type(options, Hash){'options'}
133
- unsupported = options.keys - %i[label username password url description]
134
- Aspera.assert(unsupported.empty?){"unsupported options: #{unsupported}"}
146
+ unsupported = options.keys - OPTIONS
147
+ Aspera.assert(unsupported.empty?){"unsupported options: #{unsupported}, use #{OPTIONS.join(', ')}"}
135
148
  @keychain.password(
136
149
  :add, :generic, service: options[:label],
137
150
  account: options[:username] || 'none', password: options[:password], comment: options[:description])
@@ -149,11 +162,6 @@ module Aspera
149
162
  return result
150
163
  end
151
164
 
152
- def list
153
- # the only way to list is `dump-keychain` which triggers security alert
154
- raise 'list not implemented, use macos keychain app'
155
- end
156
-
157
165
  def delete(options)
158
166
  Aspera.assert_type(options, Hash){'options'}
159
167
  unsupported = options.keys - %i[label]
data/lib/aspera/log.rb CHANGED
@@ -9,57 +9,52 @@ require 'json'
9
9
  require 'singleton'
10
10
  require 'stringio'
11
11
 
12
+ # Ignore warnings
12
13
  old_verbose = $VERBOSE
13
14
  $VERBOSE = nil
14
15
 
15
- # extend Ruby logger with trace levels
16
+ # Extend Ruby logger with trace levels
16
17
  class Logger
18
+ # Two additionnal trace levels
17
19
  TRACE_MAX = 2
18
- # add custom level to logger severity
20
+ # Add custom level to logger severity, below debug level
19
21
  module Severity
20
- 1.upto(TRACE_MAX).each { |level| const_set(:"TRACE#{level}", - level)}
22
+ 1.upto(TRACE_MAX).each{ |level| const_set(:"TRACE#{level}", - level)}
21
23
  end
22
- # quick access to label
23
- SEVERITY_LABEL = Severity.constants.each_with_object({}) { |name, hash| hash[Severity.const_get(name)] = name}
24
+ # Quick access to label
25
+ SEVERITY_LABEL = Severity.constants.each_with_object({}){ |name, hash| hash[Severity.const_get(name)] = name}
24
26
  def format_severity(severity)
25
27
  SEVERITY_LABEL[severity] || 'ANY'
26
28
  end
27
29
 
28
- # define methods for a given level
29
- def self.make_methods(str_level) # rubocop:disable Style/ClassMethodsDefinitions
30
- int_level = ::Logger.const_get(str_level.upcase)
31
- str_level = str_level.downcase
32
- Kernel.send('lave'.reverse, <<-EOM, nil, __FILE__, __LINE__ + 1)
33
- def #{str_level}(message = nil, &block)
34
- add(#{int_level}, message, &block)
35
- end
36
-
37
- def #{str_level}?
38
- level <= #{int_level}
39
- end
40
-
41
- def #{str_level}!
42
- self.level = #{int_level}
43
- end
44
- EOM
30
+ class << self
31
+ # Define methods for a given log level
32
+ def make_methods(str_level)
33
+ int_level = ::Logger.const_get(str_level.upcase)
34
+ method_base = str_level.downcase
35
+ define_method(method_base, ->(message = nil, &block){add(int_level, message, &block)})
36
+ define_method("#{method_base}?", ->{level <= int_level})
37
+ define_method("#{method_base}!", ->{self.level = int_level})
38
+ end
45
39
  end
46
- # declare methods for all levels
47
- Logger::Severity.constants.each { |severity| make_methods(severity) }
40
+ # Declare methods for all levels
41
+ Logger::Severity.constants.each{ |severity| make_methods(severity)}
48
42
  end
49
43
 
44
+ # Restore warnings
50
45
  $VERBOSE = old_verbose
51
46
 
52
47
  module Aspera
53
48
  # Singleton object for logging
54
49
  class Log
55
50
  include Singleton
56
- # where logs are sent to
51
+ # Where logs are sent to
57
52
  LOG_TYPES = %i[stderr stdout syslog].freeze
58
53
  @@format = :json # rubocop:disable Style/ClassVars
59
- # class methods
54
+ # Class methods
60
55
  class << self
61
56
  # levels are :debug,:info,:warn,:error,fatal,:unknown
62
- def levels; Logger::Severity.constants.sort{|a, b|Logger::Severity.const_get(a) <=> Logger::Severity.const_get(b)}.map{|c|c.downcase.to_sym}; end
57
+ def levels; Logger::Severity.constants.sort{ |a, b| Logger::Severity.const_get(a) <=> Logger::Severity.const_get(b)}.map{ |c| c.downcase.to_sym}; end
63
58
 
64
59
  # get the logger object of singleton
65
60
  def log; instance.logger; end
@@ -83,7 +78,7 @@ module Aspera
83
78
  def capture_stderr
84
79
  real_stderr = $stderr
85
80
  $stderr = StringIO.new
86
- yield
81
+ yield if block_given?
87
82
  log.debug($stderr.string)
88
83
  ensure
89
84
  $stderr = real_stderr
@@ -93,12 +88,12 @@ module Aspera
93
88
  attr_reader :logger_type, :logger
94
89
  attr_writer :program_name
95
90
 
96
- # set log level of underlying logger given symbol level
91
+ # Set log level of underlying logger given symbol level
97
92
  def level=(new_level)
98
93
  @logger.level = Logger::Severity.const_get(new_level.to_sym.upcase)
99
94
  end
100
95
 
101
- # get symbol of debug level of underlying logger
96
+ # Get symbol of debug level of underlying logger
102
97
  def level
103
98
  Logger::Severity.constants.each do |name|
104
99
  return name.downcase.to_sym if @logger.level.eql?(Logger::Severity.const_get(name))
@@ -106,7 +101,7 @@ module Aspera
106
101
  Aspera.error_unexpected_value(@logger.level){'log level'}
107
102
  end
108
103
 
109
- # change underlying logger, but keep log level
104
+ # Change underlying logger, but keep log level
110
105
  def logger_type=(new_log_type)
111
106
  current_severity_integer = @logger.level unless @logger.nil?
112
107
  current_severity_integer = ENV.fetch('AS_LOG_LEVEL', nil) if current_severity_integer.nil? && ENV.key?('AS_LOG_LEVEL')
@@ -131,7 +126,7 @@ module Aspera
131
126
  end
132
127
  @logger.level = current_severity_integer
133
128
  @logger_type = new_log_type
134
- # update formatter with password hiding
129
+ # Update formatter with password hiding
135
130
  @logger.formatter = SecretHider.log_formatter(@logger.formatter)
136
131
  end
137
132
 
@@ -140,9 +135,8 @@ module Aspera
140
135
  def initialize
141
136
  @logger = nil
142
137
  @program_name = 'aspera'
143
- # this sets @logger and @logger_type (self needed to call method instead of local var)
138
+ # This sets @logger and @logger_type (self needed to call method instead of local var)
144
139
  self.logger_type = :stderr
145
- raise 'error logger shall be defined' if @logger.nil?
146
140
  end
147
141
  end
148
142
  end
data/lib/aspera/nagios.rb CHANGED
@@ -17,7 +17,7 @@ module Aspera
17
17
  # add methods to add nagios error levels, each take component name and message
18
18
  LEVELS.each_index do |code|
19
19
  name = "#{ADD_PREFIX}#{LEVELS[code]}".to_sym
20
- define_method(name){|comp, msg|@data.push({code: code, comp: comp, msg: msg})}
20
+ define_method(name){ |comp, msg| @data.push({code: code, comp: comp, msg: msg})}
21
21
  end
22
22
 
23
23
  class << self
@@ -28,17 +28,17 @@ module Aspera
28
28
  %w[status component message].each do |c|
29
29
  Aspera.assert(data.first.key?(c)){"result must have #{c}"}
30
30
  end
31
- res_errors = data.reject{|s|s['status'].eql?('ok')}
31
+ res_errors = data.reject{ |s| s['status'].eql?('ok')}
32
32
  # keep only errors in case of problem, other ok are assumed so
33
33
  data = res_errors unless res_errors.empty?
34
34
  # first is most critical
35
- data.sort!{|a, b|LEVELS.index(a['status'].to_sym) <=> LEVELS.index(b['status'].to_sym)}
35
+ data.sort!{ |a, b| LEVELS.index(a['status'].to_sym) <=> LEVELS.index(b['status'].to_sym)}
36
36
  # build message: if multiple components: concatenate
37
37
  # message = data.map{|i|"#{i['component']}:#{i['message']}"}.join(', ').gsub("\n",' ')
38
38
  message = data
39
- .map{|i|i['component']}
39
+ .map{ |i| i['component']}
40
40
  .uniq
41
- .map{|comp|comp + ':' + data.select{|d|d['component'].eql?(comp)}.map{|d|d['message']}.join(',')}
41
+ .map{ |comp| comp + ':' + data.select{ |d| d['component'].eql?(comp)}.map{ |d| d['message']}.join(',')}
42
42
  .join(', ')
43
43
  .tr("\n", ' ')
44
44
  status = data.first['status'].upcase
@@ -80,7 +80,7 @@ module Aspera
80
80
  # translate for display
81
81
  def result
82
82
  raise 'missing result' if @data.empty?
83
- {type: :object_list, data: @data.map{|i|{'status' => LEVELS[i[:code]].to_s, 'component' => i[:comp], 'message' => i[:msg]}}}
83
+ {type: :object_list, data: @data.map{ |i| {'status' => LEVELS[i[:code]].to_s, 'component' => i[:comp], 'message' => i[:msg]}}}
84
84
  end
85
85
  end
86
86
  end
@@ -18,7 +18,7 @@ module Aspera
18
18
  end
19
19
 
20
20
  def all_sessions
21
- @agent.sessions.map { |session| session[:job_id] }.uniq.each.map{|job_id|job_to_transfer(job_id)}
21
+ @agent.sessions.map{ |session| session[:job_id]}.uniq.each.map{ |job_id| job_to_transfer(job_id)}
22
22
  end
23
23
 
24
24
  # status: ('waiting', 'partially_completed', 'unknown', 'waiting(read error)',] 'running', 'completed', 'failed'
@@ -185,9 +185,9 @@ module Aspera
185
185
  'size' => folder_stat.size,
186
186
  'mtime' => folder_stat.mtime.utc.iso8601,
187
187
  'permissions' => [
188
- { 'name' => 'view' },
189
- { 'name' => 'edit' },
190
- { 'name' => 'delete' }
188
+ {'name' => 'view'},
189
+ {'name' => 'edit'},
190
+ {'name' => 'delete'}
191
191
  ]
192
192
  },
193
193
  'items' => []
@@ -208,9 +208,9 @@ module Aspera
208
208
  'size' => item_stat.size,
209
209
  'mtime' => item_stat.mtime.utc.iso8601,
210
210
  'permissions' => [
211
- { 'name' => 'view' },
212
- { 'name' => 'edit' },
213
- { 'name' => 'delete' }
211
+ {'name' => 'view'},
212
+ {'name' => 'edit'},
213
+ {'name' => 'delete'}
214
214
  ]
215
215
  }
216
216
 
@@ -323,7 +323,7 @@ module Aspera
323
323
 
324
324
  def set_json_response(request, response, json, code: 200)
325
325
  response.status = code
326
- response['Content-Type'] = 'application/json'
326
+ response['Content-Type'] = Rest::MIME_JSON
327
327
  response.body = json.to_json
328
328
  Log.log.trace1{Log.dump("response for #{request.request_method} #{request.path}", json)}
329
329
  end
@@ -10,14 +10,15 @@ module Aspera
10
10
  # OAuth 2 client for the REST client
11
11
  # Generate bearer token
12
12
  # Bearer tokens are cached in memory and in a file cache for later re-use
13
- # https://tools.ietf.org/html/rfc6749
13
+ # OAuth 2.0 Authorization Framework: https://tools.ietf.org/html/rfc6749
14
+ # Bearer Token Usage: https://tools.ietf.org/html/rfc6750
14
15
  class Base
15
16
  # @param ** Parameters for REST
16
17
  # @param client_id [String, nil]
17
18
  # @param client_secret [String, nil]
18
19
  # @param scope [String, nil]
19
20
  # @param use_query [bool] Provide parameters in query instead of body
20
- # @param path_token [String] API end point to create a token
21
+ # @param path_token [String] API end point to create a token from base URL
21
22
  # @param token_field [String] Field in result that contains the token
22
23
  # @param cache_ids [Array, nil] List of unique identifiers for cache id generation
23
24
  def initialize(
@@ -56,7 +57,7 @@ module Aspera
56
57
  # helper method to create token as per RFC
57
58
  def create_token_call(creation_params)
58
59
  Log.log.debug{'Generating a new token'.bg_green}
59
- payload = { body_type: :www }
60
+ payload = {content_type: Rest::MIME_WWW}
60
61
  if @use_query
61
62
  payload[:query] = creation_params
62
63
  else
@@ -65,7 +66,7 @@ module Aspera
65
66
  return @api.call(
66
67
  operation: 'POST',
67
68
  subpath: @path_token,
68
- headers: {'Accept' => 'application/json'},
69
+ headers: {'Accept' => Rest::MIME_JSON},
69
70
  **payload
70
71
  )
71
72
  end
@@ -79,6 +80,11 @@ module Aspera
79
80
  return call_params
80
81
  end
81
82
 
83
+ # @return value suitable for Authorization header
84
+ def authorization(**kwargs)
85
+ return OAuth::Factory.bearer_build(token(**kwargs))
86
+ end
87
+
82
88
  # get an OAuth v2 token (generated, cached, refreshed)
83
89
  # call token() to get a token.
84
90
  # if a token is expired (api returns 4xx), call again token(refresh: true)
@@ -91,12 +97,13 @@ module Aspera
91
97
  unless token_info.nil?
92
98
  token_data = token_info[:data]
93
99
  # Optional optimization:
94
- # check if node token is expired based on decoded content then force refresh if close enough
100
+ # Check if token is expired based on decoded content then force refresh if close enough
95
101
  # might help in case the transfer agent cannot refresh himself
96
102
  # `direct` agent is equipped with refresh code
97
103
  # an API was already called, but failed, we need to regenerate or refresh
98
104
  if refresh || token_info[:expired]
99
- if token_data.key?('refresh_token') && token_data['refresh_token'].eql?('not_supported')
105
+ refresh_token = nil
106
+ if token_data.key?('refresh_token') && !token_data['refresh_token'].eql?('not_supported')
100
107
  # save possible refresh token, before deleting the cache
101
108
  refresh_token = token_data['refresh_token']
102
109
  end
@@ -129,7 +136,7 @@ module Aspera
129
136
  end
130
137
  Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"}
131
138
  # ok we shall have a token here
132
- return OAuth::Factory.bearer_build(token_data[@token_field])
139
+ return token_data[@token_field]
133
140
  end
134
141
  end
135
142
  end
@@ -36,9 +36,8 @@ module Aspera
36
36
  return IdGenerator.from_list([
37
37
  PERSIST_CATEGORY_TOKEN,
38
38
  url,
39
- Factory.class_to_id(creator_class),
40
- *params
41
- ].flatten)
39
+ Factory.class_to_id(creator_class)] +
40
+ params)
42
41
  end
43
42
 
44
43
  # @return snake version of class name
@@ -111,7 +110,7 @@ module Aspera
111
110
  token_data = JSON.parse(token_raw_string)
112
111
  Aspera.assert_type(token_data, Hash)
113
112
  decoded_token = decode_token(token_data[TOKEN_FIELD])
114
- info = { data: token_data }
113
+ info = {data: token_data}
115
114
  Log.log.debug{Log.dump('decoded_token', decoded_token)}
116
115
  if decoded_token.is_a?(Hash)
117
116
  info[:decoded] = decoded_token
@@ -159,11 +158,11 @@ module Aspera
159
158
  Aspera.assert_type(parameters, Hash)
160
159
  id = parameters[:grant_method]
161
160
  Aspera.assert(@token_type_classes.key?(id)){"token grant method unknown: '#{id}'"}
162
- create_parameters = parameters.reject { |k, _v| k.eql?(:grant_method) }
161
+ create_parameters = parameters.reject{ |k, _v| k.eql?(:grant_method)}
163
162
  @token_type_classes[id].new(**create_parameters)
164
163
  end
165
164
  end
166
165
  # JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
167
- Factory.instance.register_decoder(lambda { |token| parts = token.split('.'); Aspera.assert(parts.length.eql?(3)){'not JWS token'}; JSON.parse(Base64.decode64(parts[1]))}) # rubocop:disable Style/Semicolon, Layout/LineLength
166
+ Factory.instance.register_decoder(lambda{ |token| parts = token.split('.'); Aspera.assert(parts.length.eql?(3)){'not JWS token'}; JSON.parse(Base64.decode64(parts[1]))}) # rubocop:disable Style/Semicolon, Layout/LineLength
168
167
  end
169
168
  end
@@ -20,12 +20,12 @@ module Aspera
20
20
 
21
21
  def create_token
22
22
  @api.call(
23
- operation: 'POST',
24
- subpath: @path_token,
25
- headers: {'Accept' => 'application/json'},
26
- query: @query.merge(scope: @scope), # scope is here because it may change over time (node)
27
- body: @body,
28
- body_type: :json
23
+ operation: 'POST',
24
+ subpath: @path_token,
25
+ query: @query.merge(scope: @scope), # scope is here because it may change over time (node)
26
+ content_type: Rest::MIME_JSON,
27
+ body: @body,
28
+ headers: {'Accept' => Rest::MIME_JSON}
29
29
  )
30
30
  end
31
31
  end
@@ -8,9 +8,9 @@ module Aspera
8
8
  # Persist data on file system
9
9
  class PersistencyActionOnce
10
10
  DELETE_DEFAULT = lambda(&:empty?)
11
- PARSE_DEFAULT = lambda {|t| JSON.parse(t)}
12
- FORMAT_DEFAULT = lambda {|h| JSON.generate(h)}
13
- MERGE_DEFAULT = lambda {|current, file| current.concat(file).uniq rescue current}
11
+ PARSE_DEFAULT = lambda{ |t| JSON.parse(t)}
12
+ FORMAT_DEFAULT = lambda{ |h| JSON.generate(h)}
13
+ MERGE_DEFAULT = lambda{ |current, file| current.concat(file).uniq rescue current}
14
14
  MANAGER_METHODS = %i[get put delete]
15
15
  private_constant :DELETE_DEFAULT, :PARSE_DEFAULT, :FORMAT_DEFAULT, :MERGE_DEFAULT, :MANAGER_METHODS
16
16
 
@@ -22,7 +22,7 @@ module Aspera
22
22
  # @param :format Optional dump method (default to JSON)
23
23
  # @param :merge Optional merge data from file to current data
24
24
  def initialize(manager:, data:, id:, delete: DELETE_DEFAULT, parse: PARSE_DEFAULT, format: FORMAT_DEFAULT, merge: MERGE_DEFAULT)
25
- Aspera.assert(MANAGER_METHODS.all?{|i|manager.respond_to?(i)}){"Manager must answer to #{MANAGER_METHODS}"}
25
+ Aspera.assert(MANAGER_METHODS.all?{ |i| manager.respond_to?(i)}){"Manager must answer to #{MANAGER_METHODS}"}
26
26
  Aspera.assert(!data.nil?)
27
27
  Aspera.assert_type(id, String)
28
28
  Aspera.assert(!id.empty?)
@@ -39,6 +39,7 @@ module Aspera
39
39
  merge.call(@persisted_object, parse.call(value)) unless value.nil?
40
40
  end
41
41
 
42
+ # Save persisted object on storage
42
43
  def save
43
44
  if @delete_condition.call(@persisted_object)
44
45
  @manager.delete(@object_id)
@@ -47,6 +48,7 @@ module Aspera
47
48
  end
48
49
  end
49
50
 
51
+ # @return internal persisted object, in order to modify its content
50
52
  def data
51
53
  return @persisted_object
52
54
  end