aspera-cli 4.27.0 → 4.27.2

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 (142) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +46 -1
  4. data/CONTRIBUTING.md +1 -7
  5. data/TODO.md +50 -0
  6. data/docs/README.md +709 -489
  7. data/lib/aspera/agent/base.rb +10 -2
  8. data/lib/aspera/agent/connect.rb +7 -7
  9. data/lib/aspera/agent/desktop.rb +3 -3
  10. data/lib/aspera/agent/direct.rb +48 -19
  11. data/lib/aspera/agent/factory.rb +3 -3
  12. data/lib/aspera/agent/httpgw.rb +55 -14
  13. data/lib/aspera/agent/node.rb +2 -2
  14. data/lib/aspera/agent/transferd.rb +12 -12
  15. data/lib/aspera/api/aoc.rb +28 -28
  16. data/lib/aspera/api/cos_node.rb +10 -9
  17. data/lib/aspera/api/faspex.rb +2 -2
  18. data/lib/aspera/api/httpgw.rb +20 -20
  19. data/lib/aspera/api/node.rb +31 -53
  20. data/lib/aspera/ascmd.rb +13 -13
  21. data/lib/aspera/ascp/installation.rb +43 -36
  22. data/lib/aspera/ascp/management.rb +6 -6
  23. data/lib/aspera/assert.rb +12 -13
  24. data/lib/aspera/cli/ascp_actions.rb +7 -7
  25. data/lib/aspera/cli/async_transfer_store.rb +49 -5
  26. data/lib/aspera/cli/bootstrapper.rb +9 -9
  27. data/lib/aspera/cli/command_registry.rb +1 -1
  28. data/lib/aspera/cli/command_spec.rb +6 -3
  29. data/lib/aspera/cli/context.rb +3 -3
  30. data/lib/aspera/cli/extended_value.rb +26 -26
  31. data/lib/aspera/cli/formatter.rb +76 -54
  32. data/lib/aspera/cli/gem_checker.rb +1 -1
  33. data/lib/aspera/cli/hints.rb +2 -2
  34. data/lib/aspera/cli/http.rb +22 -29
  35. data/lib/aspera/cli/mailer.rb +7 -7
  36. data/lib/aspera/cli/mcp_tool.rb +31 -4
  37. data/lib/aspera/cli/option_declarator.rb +77 -0
  38. data/lib/aspera/cli/options.schema.yaml +273 -3
  39. data/lib/aspera/cli/parser.rb +539 -280
  40. data/lib/aspera/cli/plugins/alee.rb +1 -1
  41. data/lib/aspera/cli/plugins/aoc.rb +303 -314
  42. data/lib/aspera/cli/plugins/ats.rb +25 -21
  43. data/lib/aspera/cli/plugins/base.rb +357 -186
  44. data/lib/aspera/cli/plugins/basic_auth.rb +0 -12
  45. data/lib/aspera/cli/plugins/config.rb +76 -73
  46. data/lib/aspera/cli/plugins/console.rb +8 -8
  47. data/lib/aspera/cli/plugins/cos.rb +4 -3
  48. data/lib/aspera/cli/plugins/factory.rb +2 -2
  49. data/lib/aspera/cli/plugins/faspex5.rb +259 -248
  50. data/lib/aspera/cli/plugins/faspio.rb +7 -2
  51. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  52. data/lib/aspera/cli/plugins/mcp.rb +11 -11
  53. data/lib/aspera/cli/plugins/node.rb +227 -283
  54. data/lib/aspera/cli/plugins/orchestrator.rb +23 -23
  55. data/lib/aspera/cli/plugins/preview.rb +61 -64
  56. data/lib/aspera/cli/plugins/server.rb +36 -31
  57. data/lib/aspera/cli/plugins/shares.rb +174 -135
  58. data/lib/aspera/cli/preset_actions.rb +39 -19
  59. data/lib/aspera/cli/preset_manager.rb +74 -32
  60. data/lib/aspera/cli/result.rb +18 -16
  61. data/lib/aspera/cli/runner.rb +75 -39
  62. data/lib/aspera/cli/secret_finder.rb +1 -1
  63. data/lib/aspera/cli/sync_actions.rb +36 -35
  64. data/lib/aspera/cli/terminal_formatter.rb +2 -2
  65. data/lib/aspera/cli/transfer_actions.rb +17 -10
  66. data/lib/aspera/cli/transfer_agent.rb +44 -44
  67. data/lib/aspera/cli/transfer_progress.rb +7 -7
  68. data/lib/aspera/cli/vault_manager.rb +34 -12
  69. data/lib/aspera/cli/version.rb +1 -1
  70. data/lib/aspera/cli/wizard.rb +17 -20
  71. data/lib/aspera/colors.rb +3 -3
  72. data/lib/aspera/command_line_builder.rb +17 -16
  73. data/lib/aspera/command_line_converter.rb +1 -1
  74. data/lib/aspera/coverage.rb +1 -2
  75. data/lib/aspera/data_repository.rb +1 -1
  76. data/lib/aspera/dot_container.rb +8 -8
  77. data/lib/aspera/environment.rb +13 -13
  78. data/lib/aspera/faspex_gw.rb +4 -4
  79. data/lib/aspera/faspex_postproc.rb +14 -9
  80. data/lib/aspera/graphql.rb +4 -2
  81. data/lib/aspera/hash_ext.rb +2 -2
  82. data/lib/aspera/json_rpc/client.rb +1 -1
  83. data/lib/aspera/keychain/base.rb +10 -3
  84. data/lib/aspera/keychain/encrypted_hash.rb +92 -17
  85. data/lib/aspera/keychain/factory.rb +17 -5
  86. data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
  87. data/lib/aspera/keychain/macos_security.rb +23 -25
  88. data/lib/aspera/keychain/one_password_api.rb +86 -0
  89. data/lib/aspera/keychain/one_password_base.rb +34 -0
  90. data/lib/aspera/keychain/one_password_cli.rb +98 -0
  91. data/lib/aspera/link_header.rb +2 -2
  92. data/lib/aspera/log.rb +13 -13
  93. data/lib/aspera/markdown.rb +5 -5
  94. data/lib/aspera/nagios.rb +8 -8
  95. data/lib/aspera/node_simulator.rb +33 -27
  96. data/lib/aspera/oauth/base.rb +4 -5
  97. data/lib/aspera/oauth/boot.rb +3 -3
  98. data/lib/aspera/oauth/factory.rb +5 -5
  99. data/lib/aspera/oauth/jwt.rb +5 -7
  100. data/lib/aspera/oauth/web.rb +4 -4
  101. data/lib/aspera/persistency_action_once.rb +4 -4
  102. data/lib/aspera/persistency_folder.rb +8 -8
  103. data/lib/aspera/preview/file_types.rb +4 -4
  104. data/lib/aspera/preview/generator.rb +49 -46
  105. data/lib/aspera/preview/options.rb +3 -2
  106. data/lib/aspera/preview/terminal.rb +5 -5
  107. data/lib/aspera/preview/utils.rb +65 -18
  108. data/lib/aspera/products/connect.rb +2 -2
  109. data/lib/aspera/products/desktop.rb +1 -1
  110. data/lib/aspera/products/other.rb +2 -2
  111. data/lib/aspera/products/transferd.rb +3 -3
  112. data/lib/aspera/proxy_auto_config.rb +10 -10
  113. data/lib/aspera/rest.rb +149 -115
  114. data/lib/aspera/rest_error_analyzer.rb +3 -3
  115. data/lib/aspera/rest_list.rb +10 -9
  116. data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
  117. data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
  118. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +39 -0
  119. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
  120. data/lib/aspera/schema/documentation.rb +17 -11
  121. data/lib/aspera/schema/reader.rb +78 -4
  122. data/lib/aspera/schema/registry.rb +26 -7
  123. data/lib/aspera/secret_hider.rb +5 -5
  124. data/lib/aspera/ssh.rb +64 -31
  125. data/lib/aspera/ssl.rb +3 -3
  126. data/lib/aspera/sync/database.rb +2 -2
  127. data/lib/aspera/sync/operations.rb +13 -13
  128. data/lib/aspera/temp_file_manager.rb +24 -4
  129. data/lib/aspera/transfer/faux_file.rb +2 -2
  130. data/lib/aspera/transfer/parameters.rb +15 -15
  131. data/lib/aspera/transfer/resumer.rb +9 -9
  132. data/lib/aspera/transfer/spec.rb +1 -1
  133. data/lib/aspera/transfer/spec.schema.yaml +1 -0
  134. data/lib/aspera/uri_reader.rb +52 -15
  135. data/lib/aspera/web_auth.rb +6 -5
  136. data/lib/aspera/web_server_simple.rb +9 -9
  137. data/lib/aspera/yaml.rb +4 -4
  138. data.tar.gz.sig +0 -0
  139. metadata +10 -4
  140. metadata.gz.sig +0 -0
  141. data/lib/aspera/cli/plugins/faspex.rb +0 -576
  142. data/lib/aspera/transfer/uri.rb +0 -56
@@ -2,25 +2,22 @@
2
2
 
3
3
  require 'aspera/oauth/jwt'
4
4
  require 'aspera/assert'
5
+ require 'aspera/cli/option_declarator'
5
6
  require 'aspera/cli/plugins/factory'
6
7
 
7
8
  module Aspera
8
9
  module Cli
9
10
  # The wizard detects applications and generates a config
10
11
  class Wizard
12
+ extend OptionDeclarator
13
+
11
14
  WIZARD_RESULT_KEYS = %i[preset_value test_args].freeze
12
15
  DEFAULT_PRIV_KEY_FILENAME = 'my_private_key.pem' # pragma: allowlist secret
13
16
  private_constant :WIZARD_RESULT_KEYS, :DEFAULT_PRIV_KEY_FILENAME
14
17
 
15
- class << self
16
- # Declare all wizard CLI options (metadata only - no handler binding yet).
17
- # @param options [Aspera::Cli::Parser]
18
- def declare_options(options)
19
- options.declare(:override, description: 'Wizard: override existing value', allowed: Allowed::TYPES_BOOLEAN, default: false)
20
- options.declare(:default, description: 'Wizard: set as default configuration for specified plugin (also: update)', allowed: Allowed::TYPES_BOOLEAN, default: true)
21
- options.declare(:key_path, description: 'Wizard: path to private key for JWT')
22
- end
23
- end
18
+ option :override, description: 'Wizard: override existing value', allowed: Type::BOOLEAN, default: false
19
+ option :default, description: 'Wizard: set as default configuration for specified plugin (also: update)', allowed: Type::BOOLEAN, default: true
20
+ option :key_path, description: 'Wizard: path to private key for JWT'
24
21
 
25
22
  def initialize(parent, main_folder)
26
23
  @parent = parent
@@ -42,7 +39,7 @@ module Aspera
42
39
  end
43
40
 
44
41
  def check_email(email)
45
- Aspera.assert(email =~ /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i, type: ParameterError){"Username shall be an email: #{email}"}
42
+ Aspera.assert(email =~ /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i, type: ParameterError) { "Username shall be an email: #{email}" }
46
43
  end
47
44
 
48
45
  # Find a plugin, and issue the "require"
@@ -62,14 +59,14 @@ module Aspera
62
59
  next unless plugin_klass.respond_to?(:detect)
63
60
  detection_info = nil
64
61
  begin
65
- Log.log.debug{"detecting #{plugin_name_sym} at #{app_url}"}
62
+ Log.log.debug { "detecting #{plugin_name_sym} at #{app_url}" }
66
63
  RestParameters.instance.spinner_cb.call(plugin_name_sym.to_s)
67
64
  detection_info = plugin_klass.detect(app_url)
68
65
  rescue OpenSSL::SSL::SSLError => e
69
66
  Log.log.warn(e.message)
70
67
  Log.log.warn('Use option --insecure=yes to allow unchecked certificate') if e.message.include?('cert')
71
68
  rescue StandardError => e
72
- Log.log.debug{"detect error: [#{e.class}] #{e}"}
69
+ Log.log.debug { "detect error: [#{e.class}] #{e}" }
73
70
  next
74
71
  end
75
72
  next if detection_info.nil?
@@ -80,8 +77,8 @@ module Aspera
80
77
  found_apps.push({product: plugin_name_sym, name: app_name, url: app_url, version: 'unknown'}.merge(detection_info))
81
78
  end
82
79
  RestParameters.instance.spinner_cb.call(action: :success)
83
- Aspera.assert(!found_apps.empty?){"No known application found at #{app_url}"}
84
- Aspera.assert(found_apps.all?{ |a| a.keys.all?(Symbol)}, 'all app info keys must be symbols')
80
+ Aspera.assert(!found_apps.empty?) { "No known application found at #{app_url}" }
81
+ Aspera.assert(found_apps.all? { |a| a.keys.all?(Symbol) }, 'all app info keys must be symbols')
85
82
  return found_apps
86
83
  end
87
84
 
@@ -125,14 +122,14 @@ module Aspera
125
122
  # @param apps [Array] list of detected apps
126
123
  def find(apps, preset_name: '')
127
124
  identification = if apps.length.eql?(1)
128
- Log.log.debug{"Detected: #{identification}"}
125
+ Log.log.debug { "Detected: #{identification}" }
129
126
  apps.first
130
127
  else
131
128
  formatter.display_status('Multiple applications detected, please select from:')
132
129
  require 'aspera/cli/result'
133
130
  formatter.display_results(Result::ObjectList.new(apps, fields: %w[product url version]))
134
- answer = options.prompt_user_input_in_list('product', apps.map{ |a| a[:product]})
135
- apps.find{ |a| a[:product].eql?(answer)}
131
+ answer = options.prompt_user_input_in_list('product', apps.map { |a| a[:product] })
132
+ apps.find { |a| a[:product].eql?(answer) }
136
133
  end
137
134
  wiz_preset_name = preset_name
138
135
  Log.dump(:identification, identification)
@@ -142,11 +139,11 @@ module Aspera
142
139
  options.add_option_preset({url: wiz_url}, 'wizard')
143
140
  # Instantiate plugin: command line options will be known, e.g. private_key, and wizard can be called
144
141
  plugin_instance = Plugins::Factory.instance.plugin_class(identification[:product]).new(context: @parent.context)
145
- Aspera.assert(plugin_instance.respond_to?(:wizard), type: Cli::BadArgument){"Detected: #{identification[:product]}, but this application has no wizard"}
142
+ Aspera.assert(plugin_instance.respond_to?(:wizard), type: Cli::BadArgument) { "Detected: #{identification[:product]}, but this application has no wizard" }
146
143
  # Call the wizard
147
144
  wizard_result = plugin_instance.wizard(self, wiz_url)
148
- Log.log.debug{"wizard result: #{wizard_result}"}
149
- Aspera.assert(WIZARD_RESULT_KEYS.eql?(wizard_result.keys.sort)){"missing or extra keys in wizard result: #{wizard_result.keys}"}
145
+ Log.log.debug { "wizard result: #{wizard_result}" }
146
+ Aspera.assert(WIZARD_RESULT_KEYS.eql?(wizard_result.keys.sort)) { "missing or extra keys in wizard result: #{wizard_result.keys}" }
150
147
  # Get preset name from user or default
151
148
  if wiz_preset_name.empty?
152
149
  elements = [
data/lib/aspera/colors.rb CHANGED
@@ -52,9 +52,9 @@ class String
52
52
  0 # by default reset all
53
53
  end
54
54
  )
55
- define_method(name){"#{begin_seq}#{self}#{end_seq}"}
55
+ define_method(name) { "#{begin_seq}#{self}#{end_seq}" }
56
56
  else
57
- define_method(name){self}
57
+ define_method(name) { self }
58
58
  end
59
59
  end
60
60
 
@@ -62,7 +62,7 @@ class String
62
62
  # @param colors [Array<Symbol>] List of decorations.
63
63
  # @return [String] Enhanced String.
64
64
  def apply(*colors)
65
- colors.reduce(self){ |s, c| s.public_send(c)}
65
+ colors.reduce(self) { |s, c| s.public_send(c) }
66
66
  end
67
67
 
68
68
  # Transform capitalized to snake case
@@ -62,16 +62,17 @@ module Aspera
62
62
  # @param ascp [Boolean] `true` if `ascp`
63
63
  # @return [Schema::Reader] The JSON schema
64
64
  def validate_schema(schema, ascp: false)
65
- Aspera.assert_type(schema, Schema::Reader){'schema'}
66
- Aspera.assert(schema.current.key?('properties')){"Schema must have 'properties': #{schema}"}
65
+ Aspera.assert_type(schema, Schema::Reader) { 'schema' }
66
+ Aspera.assert(schema.current.key?('properties')) { "Schema must have 'properties': #{schema}" }
67
67
  schema.each_property do |property_schema, name, _full_name|
68
68
  node = property_schema.current
69
69
  unsupported_keys = node.keys - PROPERTY_KEYS
70
- Aspera.assert(unsupported_keys.empty?){"Unsupported definition keys: #{unsupported_keys}"}
71
- Aspera.assert(node.key?('type') || node.key?('enum')){"Missing type for #{name} in #{schema.current.dig('description').current}"}
72
- Aspera.assert(node['type'].eql?('boolean')){"switch must be bool: #{name}"} if node['x-cli-switch'] && !node['x-cli-special']
70
+ Aspera.assert(unsupported_keys.empty?) { "Unsupported definition keys: #{unsupported_keys}" }
71
+ Aspera.assert(node.key?('type') || node.key?('enum')) { "Missing type for #{name} in #{schema.current.dig('description').current}" }
72
+ Aspera.assert(node['type'].eql?('boolean')) { "switch must be bool: #{name}" } if node['x-cli-switch'] && !node['x-cli-special']
73
+ Aspera.assert(node['x-cli-switch']) { "boolean direct option must have x-cli-switch: #{name}" } if node['type'].eql?('boolean') && node.key?('x-cli-option') && supported_by_agent(:direct, node) && !node['x-cli-special']
73
74
  node['x-cli-option'] = "--#{name.to_s.tr('_', '-')}" if node['x-cli-option'].eql?(true) || (node['x-cli-switch'].eql?(true) && !node.key?('x-cli-option'))
74
- Aspera.assert(DIRECT_PROPERTIES.any?{ |i| node.key?(i)}, name, type: :warn) if ascp && supported_by_agent(:direct, node)
75
+ Aspera.assert(DIRECT_PROPERTIES.any? { |i| node.key?(i) }, name, type: :warn) if ascp && supported_by_agent(:direct, node)
75
76
  node.freeze
76
77
  end
77
78
  schema
@@ -104,7 +105,7 @@ module Aspera
104
105
  Log.dump(:exec_spec, @result)
105
106
  # warn about non translated arguments
106
107
  @object.each_pair do |name, value|
107
- Log.log.warn{"Unknown transfer spec parameter: #{name} = \"#{value}\""} unless @processed_parameters.include?(name)
108
+ Log.log.warn { "Unknown transfer spec parameter: #{name} = \"#{value}\"" } unless @processed_parameters.include?(name)
108
109
  end
109
110
  # set result
110
111
  exec_spec.env.merge!(@result.env)
@@ -116,7 +117,7 @@ module Aspera
116
117
  def add_command_line_options(*options)
117
118
  options = options.first if options.first.is_a?(Array) && options.length.eql?(1)
118
119
  Aspera.assert_type(options, Array)
119
- options.each{ |o| @result.args.push(o.to_s)}
120
+ options.each { |o| @result.args.push(o.to_s) }
120
121
  end
121
122
 
122
123
  def process_params
@@ -138,11 +139,11 @@ module Aspera
138
139
  properties = @schema['properties'][name]
139
140
  # should not happen
140
141
  if properties.nil?
141
- Log.log.warn{"Unknown parameter #{name}"}
142
+ Log.log.warn { "Unknown parameter #{name}" }
142
143
  return
143
144
  end
144
145
  # check mandatory parameter (nil is valid value), TODO: change exception ?
145
- Aspera.assert(!(@schema['required']&.include?(name) && !properties['x-cli-special'] && !@object.key?(name)), type: Transfer::Error){"Missing mandatory parameter: #{name}"}
146
+ Aspera.assert(!(@schema['required']&.include?(name) && !properties['x-cli-special'] && !@object.key?(name)), type: Transfer::Error) { "Missing mandatory parameter: #{name}" }
146
147
  parameter_value = @object[name]
147
148
  # no default setting
148
149
  # parameter_value=properties['default'] if parameter_value.nil? and properties.has_key?('default')
@@ -155,7 +156,7 @@ module Aspera
155
156
  when 'object' then [Hash]
156
157
  when 'integer' then [Integer]
157
158
  when 'boolean' then [TrueClass, FalseClass]
158
- else Aspera.error_unexpected_value(properties['type']){"Property #{name}"}
159
+ else Aspera.error_unexpected_value(properties['type']) { "Property #{name}" }
159
160
  end
160
161
  end.flatten
161
162
  # check that value is of expected type
@@ -168,7 +169,7 @@ module Aspera
168
169
  return if parameter_value.nil?
169
170
 
170
171
  # check that value is of an accepted type (string, integer, boolean)
171
- Aspera.assert(!(properties.key?('enum') && !properties['enum'].include?(parameter_value))){"Enum value #{parameter_value} is not allowed for #{name}"}
172
+ Aspera.assert(!(properties.key?('enum') && !properties['enum'].include?(parameter_value))) { "Enum value #{parameter_value} is not allowed for #{name}" }
172
173
 
173
174
  # convert some values if value on command line needs processing from value in structure
174
175
  if (convert = properties['x-cli-convert'])
@@ -176,9 +177,9 @@ module Aspera
176
177
  case convert
177
178
  when Hash then convert[parameter_value]
178
179
  when String then @convert.send(convert, parameter_value)
179
- else Aspera.error_unexpected_value(convert){"Conversion type for #{name} is Hash or String only."}
180
+ else Aspera.error_unexpected_value(convert) { "Conversion type for #{name} is Hash or String only." }
180
181
  end
181
- Aspera.assert(!converted_value.nil?){"No conversion for: #{name}=#{parameter_value}"}
182
+ Aspera.assert(!converted_value.nil?) { "No conversion for: #{name}=#{parameter_value}" }
182
183
  parameter_value = converted_value
183
184
  end
184
185
 
@@ -199,14 +200,14 @@ module Aspera
199
200
  add_command_line_options(properties['x-cli-option'])
200
201
  when false
201
202
  add_command_line_options(properties['x-cli-false']) if properties.key?('x-cli-false')
202
- else Aspera.error_unexpected_value(parameter_value){name}
203
+ else Aspera.error_unexpected_value(parameter_value) { name }
203
204
  end
204
205
  else
205
206
  # transform into command line option with value
206
207
  # parameter_value=parameter_value.to_s if parameter_value.is_a?(Integer)
207
208
  parameter_value = [parameter_value] unless parameter_value.is_a?(Array)
208
209
  # if transfer_spec value is an array, applies option many times
209
- parameter_value.each{ |v| add_command_line_options(properties['x-cli-option'], v)}
210
+ parameter_value.each { |v| add_command_line_options(properties['x-cli-option'], v) }
210
211
  end
211
212
  end
212
213
  end
@@ -19,7 +19,7 @@ module Aspera
19
19
  case value
20
20
  when 'yes' then return true
21
21
  when 'no' then return false
22
- else Aspera.error_unexpected_value(value){'only: yes or no: '}
22
+ else Aspera.error_unexpected_value(value) { 'only: yes or no: ' }
23
23
  end
24
24
  end
25
25
 
@@ -27,12 +27,11 @@ if ENV.key?('ENABLE_COVERAGE')
27
27
  # assert usually do not trigger
28
28
  add_filter do |source_file|
29
29
  source_file.lines.each do |line|
30
- line.skipped! if no_cov_functions.any?{ |i| line.src.include?(i)}
30
+ line.skipped! if no_cov_functions.any? { |i| line.src.include?(i) }
31
31
  end
32
32
  false
33
33
  end
34
34
  # no coverage in these files
35
- add_filter 'lib/aspera/cli/plugins/faspex.rb'
36
35
  add_filter 'lib/aspera/node_simulator.rb'
37
36
  add_filter 'lib/aspera/keychain/macos_security.rb'
38
37
  add_filter 'lib/aspera/assert.rb'
@@ -24,7 +24,7 @@ module Aspera
24
24
  # @return [String] decoded data
25
25
  def item(name)
26
26
  index = ELEMENTS.index(name)
27
- Aspera.assert(index, type: ParameterError){"Unknown data item #{name} (#{name.class})"}
27
+ Aspera.assert(index, type: ParameterError) { "Unknown data item #{name} (#{name.class})" }
28
28
  raw_data = data(START_INDEX + index)
29
29
  case name
30
30
  when :dsa, :rsa
@@ -14,7 +14,7 @@ module Aspera
14
14
  def dotted_to_container(path, value, result = nil)
15
15
  Aspera.assert_array_all(path, String)
16
16
  # Typed keys
17
- keys = path.map{ |k| int_or_string(k)}
17
+ keys = path.map { |k| int_or_string(k) }
18
18
  # Create, or re-use first level container
19
19
  current = (result ||= new_hash_or_array_from_key(keys.first))
20
20
  # walk the path, and create sub-containers if necessary
@@ -73,19 +73,19 @@ module Aspera
73
73
  add_elements(path, current)
74
74
  when Array
75
75
  # Array has no nested structures: list of Strings
76
- if current.none?{ |i| i.is_a?(Array) || i.is_a?(Hash)}
76
+ if current.none? { |i| i.is_a?(Array) || i.is_a?(Hash) }
77
77
  to_insert = current.map(&:to_s)
78
78
  # Array of Hashes with only 'name' keys: list of Strings
79
- elsif current.all?{ |i| i.is_a?(Hash) && i.keys == ['name']}
80
- to_insert = current.map{ |i| i['name']}
79
+ elsif current.all? { |i| i.is_a?(Hash) && i.keys == ['name'] }
80
+ to_insert = current.map { |i| i['name'] }
81
81
  # Array of Hashes with only 'name' and 'value' keys: Hash of key/values
82
- elsif current.all?{ |i| i.is_a?(Hash) && i.key?('name') && i.key?('value') && i.length <= 3}
82
+ elsif current.all? { |i| i.is_a?(Hash) && i.key?('name') && i.key?('value') && i.length <= 3 }
83
83
  # if there is an extra key, other than 'name' and 'value', insert that key as is
84
- add_elements(path, current.flat_map{ |h| h.except('name', 'value').to_a})
84
+ add_elements(path, current.flat_map { |h| h.except('name', 'value').to_a })
85
85
  # Insert name/value pairs as Hash
86
- add_elements(path, current.to_h{ |h| h.values_at('name', 'value')})
86
+ add_elements(path, current.to_h { |h| h.values_at('name', 'value') })
87
87
  else
88
- add_elements(path, current.each_with_index.map{ |v, i| [i, v]})
88
+ add_elements(path, current.each_with_index.map { |v, i| [i, v] })
89
89
  end
90
90
  else
91
91
  to_insert = current
@@ -118,11 +118,11 @@ module Aspera
118
118
  def secure_execute(*cmd, mode: :execute, **kwargs)
119
119
  cmd = cmd.map(&:to_s)
120
120
  Aspera.assert(cmd.size.positive?, 'executable must be present', type: ArgumentError)
121
- Aspera.assert_values(mode, PROCESS_MODES, type: ArgumentError){'mode'}
121
+ Aspera.assert_values(mode, PROCESS_MODES, type: ArgumentError) { 'mode' }
122
122
  Log.log.debug do
123
123
  parts = [mode.to_s, 'command:']
124
- kwargs[:env]&.each{ |k, v| parts << "#{k}=#{shell_escape_pretty(v.to_s)}"}
125
- cmd.each{ |a| parts << shell_escape_pretty(a)}
124
+ kwargs[:env]&.each { |k, v| parts << "#{k}=#{shell_escape_pretty(v.to_s)}" }
125
+ cmd.each { |a| parts << shell_escape_pretty(a) }
126
126
  parts.join(' ')
127
127
  end
128
128
  case mode
@@ -142,7 +142,7 @@ module Aspera
142
142
  # https://docs.ruby-lang.org/en/master/Open3.html#method-c-capture3
143
143
  # https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options
144
144
  argv = [kwargs.delete(:env)].compact + cmd
145
- exception = kwargs.delete(:exception){true}
145
+ exception = kwargs.delete(:exception) { true }
146
146
  result = Open3.capture3(*argv, **kwargs)
147
147
  Log.dump(:stdout, result[0], level: :trace1)
148
148
  Log.dump(:stderr, result[1], level: :trace1)
@@ -182,7 +182,7 @@ module Aspera
182
182
  elsif File.directory?(path)
183
183
  mode = 0o700
184
184
  else
185
- Log.log.debug{"No restriction can be set for #{path}"}
185
+ Log.log.debug { "No restriction can be set for #{path}" }
186
186
  end
187
187
  end
188
188
  File.chmod(mode, path) unless mode.nil?
@@ -198,7 +198,7 @@ module Aspera
198
198
 
199
199
  # force locale to C so that unicode characters are not used
200
200
  def force_terminal_c
201
- I18N_VARS.each{ |var| ENV[var] = 'C'}
201
+ I18N_VARS.each { |var| ENV[var] = 'C' }
202
202
  end
203
203
 
204
204
  # @return [Boolean] true if we can display Unicode characters
@@ -208,7 +208,7 @@ module Aspera
208
208
  # https://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html
209
209
  def terminal_supports_unicode?
210
210
  return false unless terminal?
211
- locale_charmap_utf8? || I18N_VARS.any?{ |var| ENV[var]&.include?('UTF-8')}
211
+ locale_charmap_utf8? || I18N_VARS.any? { |var| ENV[var]&.include?('UTF-8') }
212
212
  end
213
213
 
214
214
  private
@@ -237,11 +237,11 @@ module Aspera
237
237
  OS_WINDOWS
238
238
  when /darwin/, /mac os/
239
239
  OS_MACOS
240
- when /linux/
240
+ when /linux/, /cosmo/
241
241
  OS_LINUX
242
242
  when /aix/
243
243
  OS_AIX
244
- else Aspera.error_unexpected_value(RbConfig::CONFIG['host_os']){'host_os'}
244
+ else Aspera.error_unexpected_value(RbConfig::CONFIG['host_os']) { 'host_os' }
245
245
  end
246
246
  @cpu =
247
247
  case RbConfig::CONFIG['host_cpu']
@@ -253,7 +253,7 @@ module Aspera
253
253
  CPU_S390
254
254
  when /arm/, /aarch64/
255
255
  CPU_ARM64
256
- else Aspera.error_unexpected_value(RbConfig::CONFIG['host_cpu']){'host_cpu'}
256
+ else Aspera.error_unexpected_value(RbConfig::CONFIG['host_cpu']) { 'host_cpu' }
257
257
  end
258
258
  @executable_extension = @os.eql?(OS_WINDOWS) ? '.exe' : nil
259
259
  # :text or :graphical depending on the environment
@@ -289,7 +289,7 @@ module Aspera
289
289
  def fix_home
290
290
  return unless @os.eql?(OS_WINDOWS) && ENV.key?('USERPROFILE') && Dir.exist?(ENV.fetch('USERPROFILE', nil))
291
291
  ENV['HOME'] = ENV.fetch('USERPROFILE', nil)
292
- Log.log.debug{"Windows: set HOME to USERPROFILE: #{Dir.home}"}
292
+ Log.log.debug { "Windows: set HOME to USERPROFILE: #{Dir.home}" }
293
293
  nil
294
294
  end
295
295
 
@@ -305,7 +305,7 @@ module Aspera
305
305
  when Environment::OS_MACOS then self.class.secure_execute('open', uri.to_s)
306
306
  when Environment::OS_WINDOWS then self.class.secure_execute('start', 'explorer', %Q{"#{uri}"})
307
307
  when Environment::OS_LINUX then self.class.secure_execute('xdg-open', uri.to_s)
308
- else Aspera.error_unexpected_value(os){'no graphical open method'}
308
+ else Aspera.error_unexpected_value(os) { 'no graphical open method' }
309
309
  end
310
310
  nil
311
311
  end
@@ -336,7 +336,7 @@ module Aspera
336
336
  else
337
337
  puts "USER ACTION: open this:\n#{the_url.to_s.red}\n"
338
338
  end
339
- else Aspera.error_unexpected_value(@url_method){'URL open method'}
339
+ else Aspera.error_unexpected_value(@url_method) { 'URL open method' }
340
340
  end
341
341
  end
342
342
 
@@ -44,7 +44,7 @@ module Aspera
44
44
  package_data = {
45
45
  'title' => faspex_pkg_delivery['title'],
46
46
  'note' => faspex_pkg_delivery['note'],
47
- 'recipients' => faspex_pkg_delivery['recipients'].map{ |name| {'name'=>name}}
47
+ 'recipients' => faspex_pkg_delivery['recipients'].map { |name| {'name'=>name} }
48
48
  }
49
49
  package = @app_api.create('packages', package_data)
50
50
  # TODO: option to send from remote source or httpgw
@@ -70,7 +70,7 @@ module Aspera
70
70
  begin
71
71
  Aspera.assert(!request.body.nil?, 'payload missing')
72
72
  faspex_pkg_parameters = JSON.parse(request.body)
73
- Log.log.debug{"faspex pkg create parameters=#{faspex_pkg_parameters}"}
73
+ Log.dump(:faspex_pkg_parameters, faspex_pkg_parameters)
74
74
  # compare string, as class is not yet known here
75
75
  faspex_package_create_result =
76
76
  case @app_api.class.name
@@ -80,7 +80,7 @@ module Aspera
80
80
  faspex4_send_to_faspex5(faspex_pkg_parameters)
81
81
  else Aspera.error_unexpected_value(@app_api.class.name)
82
82
  end
83
- Log.log.debug{"faspex_package_create_result=#{faspex_package_create_result}"}
83
+ Log.dump(:faspex_package_create_result, faspex_package_create_result)
84
84
  response.status = 200
85
85
  response.content_type = Mime::JSON
86
86
  response.body = JSON.generate(faspex_package_create_result)
@@ -89,7 +89,7 @@ module Aspera
89
89
  response['Content-Type'] = Mime::JSON
90
90
  response.body = {error: e.message, stacktrace: e.backtrace}.to_json
91
91
  Log.log.error(e.message)
92
- Log.log.debug{e.backtrace.join("\n")}
92
+ Log.log.debug { e.backtrace.join("\n") }
93
93
  end
94
94
  else
95
95
  response.status = 400
@@ -16,18 +16,18 @@ module Aspera
16
16
  @parameters = parameters.symbolize_keys
17
17
  Log.dump(:post_proc_parameters, @parameters)
18
18
  not_allowed = @parameters.keys - ALLOWED_PARAMETERS
19
- Aspera.assert(not_allowed.empty?){"unsupported parameters: #{not_allowed.join(', ')}"}
19
+ Aspera.assert(not_allowed.empty?) { "unsupported parameters: #{not_allowed.join(', ')}" }
20
20
  @parameters[:script_folder] ||= '.'
21
21
  @parameters[:fail_on_error] ||= false
22
22
  @parameters[:timeout_seconds] ||= 60
23
23
  super(server)
24
- Log.log.debug{'Faspex4PostProcServlet initialized'}
24
+ Log.log.debug { 'Faspex4PostProcServlet initialized' }
25
25
  end
26
26
 
27
27
  # :reek:UncommunicativeMethodName
28
28
  def do_POST(request, response)
29
- Log.log.debug{"request=#{request.path}"}
30
- Log.log.debug{"query=#{request.query}"}
29
+ Log.log.debug { "request=#{request.path}" }
30
+ Log.dump(:query, request.query)
31
31
  begin
32
32
  # Only accept requests on the root
33
33
  if !request.path.start_with?(@parameters[:root])
@@ -44,9 +44,14 @@ module Aspera
44
44
  end
45
45
  # build script path by removing domain and adding script folder
46
46
  script_file = request.path[@parameters[:root].size..]
47
- Log.log.debug{"script file=#{script_file}"}
47
+ Log.log.debug { "script file=#{script_file}" }
48
48
  script_path = File.join(@parameters[:script_folder], script_file)
49
- Log.log.debug{"script=#{script_path}"}
49
+ # Resolve the real path and ensure it stays within script_folder (prevents path traversal)
50
+ resolved_script_folder = File.realpath(@parameters[:script_folder])
51
+ resolved_script_path = File.realpath(script_path)
52
+ Aspera.assert(resolved_script_path.start_with?("#{resolved_script_folder}/")) { 'Script path traversal attempt detected' }
53
+ script_path = resolved_script_path
54
+ Log.log.debug { "script=#{script_path}" }
50
55
  webhook_parameters = JSON.parse(request.body)
51
56
  Log.dump(:webhook_parameters, webhook_parameters)
52
57
  process_status = nil
@@ -55,7 +60,7 @@ module Aspera
55
60
  Environment.secure_eval(File.read(script_path), __FILE__, __LINE__).call(webhook_parameters)
56
61
  else
57
62
  # env expects only strings
58
- environment = webhook_parameters.each_with_object({}){ |(k, v), h| h[k] = v.to_s}
63
+ environment = webhook_parameters.each_with_object({}) { |(k, v), h| h[k] = v.to_s }
59
64
  post_proc_pid = Environment.secure_execute(script_path, mode: :background, env: environment)
60
65
  Timeout.timeout(@parameters[:timeout_seconds]) do
61
66
  # "wait" for process to avoid zombie
@@ -63,12 +68,12 @@ module Aspera
63
68
  post_proc_pid = nil
64
69
  end
65
70
  process_status = $CHILD_STATUS
66
- Aspera.assert(process_status.success? || !@parameters[:fail_on_error]){"script #{script_path} failed with code #{process_status.exitstatus}"}
71
+ Aspera.assert(process_status.success? || !@parameters[:fail_on_error]) { "script #{script_path} failed with code #{process_status.exitstatus}" }
67
72
  end
68
73
  response.status = 200
69
74
  response.content_type = Mime::JSON
70
75
  response.body = JSON.generate({status: 'success', script: script_path, exit_code: process_status&.exitstatus})
71
- Log.log.debug{'Script executed successfully'}
76
+ Log.log.debug { 'Script executed successfully' }
72
77
  rescue => e
73
78
  Log.log.error("Script failed: #{e.class}:#{e.message}")
74
79
  if !post_proc_pid.nil?
@@ -21,14 +21,16 @@ module Aspera
21
21
  query = load_query(name)
22
22
  response = rest.create(nil, {query: query, variables: variables})
23
23
  errors = response['errors']
24
- raise RestCallError, errors.map{ |e| e['message']}.join("\n") if errors.is_a?(Array) && !errors.empty?
24
+ raise RestCallError, errors.map { |e| e['message'] }.join("\n") if errors.is_a?(Array) && !errors.empty?
25
25
  response['data']
26
26
  end
27
27
 
28
28
  private
29
29
 
30
30
  def load_query(name)
31
- File.read(File.join(QUERIES_FOLDER, "#{name}.graphql"))
31
+ candidate = File.realpath(File.join(QUERIES_FOLDER, "#{name}.graphql"))
32
+ Aspera.assert(candidate.start_with?("#{QUERIES_FOLDER}/")) { "Invalid GraphQL query name: #{name}" }
33
+ File.read(candidate)
32
34
  end
33
35
  end
34
36
  end
@@ -5,14 +5,14 @@ class ::Hash
5
5
  # @param second [Hash] Hash to merge into self
6
6
  # @return [Hash] New merged hash
7
7
  def deep_merge(second)
8
- merge(second){ |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.deep_merge(v2) : v2}
8
+ merge(second) { |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.deep_merge(v2) : v2 }
9
9
  end
10
10
 
11
11
  # Recursively merge another hash in-place, with nested hashes merged deeply
12
12
  # @param second [Hash] Hash to merge into self
13
13
  # @return [Hash] self
14
14
  def deep_merge!(second)
15
- merge!(second){ |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.deep_merge!(v2) : v2}
15
+ merge!(second) { |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.deep_merge!(v2) : v2 }
16
16
  end
17
17
 
18
18
  # Recursively iterate through hash and execute block on leaf values
@@ -44,7 +44,7 @@ module Aspera
44
44
  params: args,
45
45
  id: @request_id += 1
46
46
  })
47
- Aspera.assert_type(data, Hash){'response'}
47
+ Aspera.assert_type(data, Hash) { 'response' }
48
48
  Aspera.assert(data['jsonrpc'] == VERSION, 'bad version in response')
49
49
  Aspera.assert(data.key?('id'), 'missing id in response')
50
50
  Aspera.assert(!(data.key?('error') && data.key?('result')), 'both error and response')
@@ -4,12 +4,19 @@ module Aspera
4
4
  module Keychain
5
5
  class Base
6
6
  CONTENT_KEYS = %i[label username password url description].freeze
7
+
8
+ # Returns a lightweight list: one Hash per secret with :label (and :id if the backend has one).
9
+ # Override in subclasses that can provide this cheaply without fetching full item details.
10
+ def ids
11
+ all.map { |s| s.slice(:id, :label).compact }
12
+ end
13
+
7
14
  def validate_set(options)
8
- Aspera.assert_type(options, Hash){'options'}
15
+ Aspera.assert_type(options, Hash) { 'options' }
9
16
  unsupported = options.keys - CONTENT_KEYS
10
- Aspera.assert(unsupported.empty?){"unsupported options: #{unsupported}, use #{CONTENT_KEYS.join(', ')}"}
17
+ Aspera.assert(unsupported.empty?) { "unsupported options: #{unsupported}, use #{CONTENT_KEYS.join(', ')}" }
11
18
  options.each_pair do |k, v|
12
- Aspera.assert_type(v, String){k.to_s}
19
+ Aspera.assert_type(v, String) { k.to_s }
13
20
  end
14
21
  Aspera.assert(options.key?(:label), 'label is required')
15
22
  end