aspera-cli 4.18.1 → 4.20.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 (85) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +33 -0
  4. data/CONTRIBUTING.md +17 -12
  5. data/README.md +396 -185
  6. data/bin/asession +26 -19
  7. data/examples/build_exec +74 -0
  8. data/examples/{rubyc → build_exec_rubyc} +18 -2
  9. data/examples/get_proto_file.rb +7 -0
  10. data/lib/aspera/agent/alpha.rb +8 -8
  11. data/lib/aspera/agent/base.rb +4 -18
  12. data/lib/aspera/agent/connect.rb +14 -13
  13. data/lib/aspera/agent/direct.rb +123 -120
  14. data/lib/aspera/agent/httpgw.rb +2 -3
  15. data/lib/aspera/agent/node.rb +10 -10
  16. data/lib/aspera/agent/trsdk.rb +17 -20
  17. data/lib/aspera/api/alee.rb +15 -0
  18. data/lib/aspera/api/aoc.rb +128 -99
  19. data/lib/aspera/api/ats.rb +1 -1
  20. data/lib/aspera/api/cos_node.rb +1 -1
  21. data/lib/aspera/api/httpgw.rb +104 -64
  22. data/lib/aspera/api/node.rb +33 -12
  23. data/lib/aspera/ascmd.rb +56 -48
  24. data/lib/aspera/ascp/installation.rb +142 -70
  25. data/lib/aspera/ascp/management.rb +7 -3
  26. data/lib/aspera/ascp/products.rb +13 -7
  27. data/lib/aspera/assert.rb +10 -5
  28. data/lib/aspera/cli/formatter.rb +42 -26
  29. data/lib/aspera/cli/hints.rb +2 -1
  30. data/lib/aspera/cli/info.rb +12 -10
  31. data/lib/aspera/cli/main.rb +16 -13
  32. data/lib/aspera/cli/manager.rb +15 -10
  33. data/lib/aspera/cli/plugin.rb +17 -31
  34. data/lib/aspera/cli/plugin_factory.rb +10 -1
  35. data/lib/aspera/cli/plugins/alee.rb +3 -3
  36. data/lib/aspera/cli/plugins/aoc.rb +222 -194
  37. data/lib/aspera/cli/plugins/ats.rb +16 -14
  38. data/lib/aspera/cli/plugins/config.rb +66 -53
  39. data/lib/aspera/cli/plugins/console.rb +3 -3
  40. data/lib/aspera/cli/plugins/faspex.rb +11 -21
  41. data/lib/aspera/cli/plugins/faspex5.rb +44 -42
  42. data/lib/aspera/cli/plugins/faspio.rb +2 -2
  43. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  44. data/lib/aspera/cli/plugins/node.rb +155 -96
  45. data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
  46. data/lib/aspera/cli/plugins/preview.rb +8 -9
  47. data/lib/aspera/cli/plugins/server.rb +6 -10
  48. data/lib/aspera/cli/plugins/shares.rb +13 -9
  49. data/lib/aspera/cli/sync_actions.rb +72 -31
  50. data/lib/aspera/cli/transfer_agent.rb +13 -14
  51. data/lib/aspera/cli/transfer_progress.rb +36 -18
  52. data/lib/aspera/cli/version.rb +1 -1
  53. data/lib/aspera/command_line_builder.rb +3 -4
  54. data/lib/aspera/coverage.rb +13 -1
  55. data/lib/aspera/environment.rb +59 -10
  56. data/lib/aspera/faspex_gw.rb +3 -3
  57. data/lib/aspera/json_rpc.rb +1 -1
  58. data/lib/aspera/keychain/encrypted_hash.rb +2 -0
  59. data/lib/aspera/keychain/macos_security.rb +7 -12
  60. data/lib/aspera/log.rb +4 -4
  61. data/lib/aspera/node_simulator.rb +1 -1
  62. data/lib/aspera/oauth/base.rb +39 -45
  63. data/lib/aspera/oauth/factory.rb +11 -4
  64. data/lib/aspera/oauth/generic.rb +4 -8
  65. data/lib/aspera/oauth/jwt.rb +4 -4
  66. data/lib/aspera/oauth/url_json.rb +3 -2
  67. data/lib/aspera/oauth/web.rb +10 -6
  68. data/lib/aspera/persistency_action_once.rb +16 -8
  69. data/lib/aspera/preview/utils.rb +5 -16
  70. data/lib/aspera/rest.rb +100 -76
  71. data/lib/aspera/secret_hider.rb +3 -2
  72. data/lib/aspera/ssh.rb +1 -1
  73. data/lib/aspera/transfer/faux_file.rb +7 -5
  74. data/lib/aspera/transfer/parameters.rb +41 -35
  75. data/lib/aspera/transfer/spec.rb +16 -18
  76. data/lib/aspera/transfer/sync.rb +51 -50
  77. data/lib/aspera/transfer/uri.rb +1 -1
  78. data/lib/aspera/uri_reader.rb +1 -1
  79. data/lib/aspera/web_auth.rb +166 -18
  80. data/lib/aspera/web_server_simple.rb +27 -15
  81. data/lib/transfer_pb.rb +84 -0
  82. data/lib/transfer_services_pb.rb +82 -0
  83. data.tar.gz.sig +0 -0
  84. metadata +25 -6
  85. metadata.gz.sig +0 -0
@@ -5,6 +5,7 @@ require 'aspera/log'
5
5
  require 'aspera/assert'
6
6
  require 'rbconfig'
7
7
  require 'singleton'
8
+ require 'English'
8
9
 
9
10
  # cspell:words MEBI mswin bccwin
10
11
 
@@ -31,7 +32,6 @@ module Aspera
31
32
  BYTES_PER_MEBIBIT = MEBI / BITS_PER_BYTE
32
33
 
33
34
  class << self
34
- @terminal_supports_unicode = nil
35
35
  def ruby_version
36
36
  return RbConfig::CONFIG['RUBY_PROGRAM_VERSION']
37
37
  end
@@ -66,10 +66,13 @@ module Aspera
66
66
  raise "Unknown CPU: #{RbConfig::CONFIG['host_cpu']}"
67
67
  end
68
68
 
69
+ # normalized architecture name
70
+ # see constants: OS_* and CPU_*
69
71
  def architecture
70
72
  return "#{os}-#{cpu}"
71
73
  end
72
74
 
75
+ # executable file extension for current OS
73
76
  def exe_extension
74
77
  return '.exe' if os.eql?(OS_WINDOWS)
75
78
  return ''
@@ -83,6 +86,7 @@ module Aspera
83
86
  Log.log.debug{"Windows: set HOME to USERPROFILE: #{Dir.home}"}
84
87
  end
85
88
 
89
+ # empty variable binding for secure eval
86
90
  def empty_binding
87
91
  return Kernel.binding
88
92
  end
@@ -92,7 +96,46 @@ module Aspera
92
96
  Kernel.send('lave'.reverse, code, empty_binding, file, line)
93
97
  end
94
98
 
95
- # value is provided in block
99
+ def log_spawn(env:, exec:, args:)
100
+ [
101
+ 'execute:'.red,
102
+ env.map{|k, v| "#{k}=#{Shellwords.shellescape(v)}"},
103
+ Shellwords.shellescape(exec),
104
+ args.map{|a|Shellwords.shellescape(a)}
105
+ ].flatten.join(' ')
106
+ end
107
+
108
+ # start process in background, or raise exception
109
+ # caller can call Process.wait on returned value
110
+ def secure_spawn(exec:, args: [], env: [])
111
+ Log.log.debug {log_spawn(env: env, exec: exec, args: args)}
112
+ # start ascp in separate process
113
+ ascp_pid = Process.spawn(env, [exec, exec], *args, close_others: true)
114
+ Log.log.debug{"pid: #{ascp_pid}"}
115
+ return ascp_pid
116
+ end
117
+
118
+ # @param exec [String] path to executable
119
+ # @param args [Array] arguments to executable
120
+ # @param opts [Hash] options to capture3
121
+ # @return stdout of executable or raise expcetion
122
+ def secure_capture(exec:, args: [], **opts)
123
+ Aspera.assert_type(exec, String)
124
+ Aspera.assert_type(args, Array)
125
+ Aspera.assert_type(opts, Hash)
126
+ Log.log.debug {log_spawn(env: {}, exec: exec, args: args)}
127
+ stdout, stderr, status = Open3.capture3(exec, *args, **opts)
128
+ Log.log.debug{"status=#{status}, stderr=#{stderr}"}
129
+ Log.log.trace1{"stdout=#{stdout}"}
130
+ raise "process failed: #{status.exitstatus} : #{stderr}" unless status.success?
131
+ return stdout
132
+ end
133
+
134
+ # Write content to a file, with restricted access
135
+ # @param path [String] the file path
136
+ # @param force [Boolean] if true, overwrite the file
137
+ # @param mode [Integer] the file mode (permissions)
138
+ # @block [Proc] return the content to write to the file
96
139
  def write_file_restricted(path, force: false, mode: nil)
97
140
  Aspera.assert(block_given?, exception_class: Aspera::InternalError)
98
141
  if force || !File.exist?(path)
@@ -105,6 +148,7 @@ module Aspera
105
148
  return path
106
149
  end
107
150
 
151
+ # restrict access to a file or folder to user only
108
152
  def restrict_file_access(path, mode: nil)
109
153
  if mode.nil?
110
154
  # or FileUtils ?
@@ -121,18 +165,12 @@ module Aspera
121
165
  Log.log.warn(e.message)
122
166
  end
123
167
 
168
+ # @return true if we are in a terminal
124
169
  def terminal?
125
170
  $stdout.tty?
126
171
  end
127
172
 
128
- # @return true if we can display Unicode characters
129
- # https://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html
130
- # https://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html
131
- def terminal_supports_unicode?
132
- @terminal_supports_unicode = terminal? && %w(LC_ALL LC_CTYPE LANG).any?{|var|ENV[var]&.include?('UTF-8')} if @terminal_supports_unicode.nil?
133
- return @terminal_supports_unicode
134
- end
135
-
173
+ # @return :text or :graphical depending on the environment
136
174
  def default_gui_mode
137
175
  # assume not remotely connected on macos and windows
138
176
  return :graphical if [Environment::OS_WINDOWS, Environment::OS_MACOS].include?(Environment.os)
@@ -141,6 +179,7 @@ module Aspera
141
179
  return :text
142
180
  end
143
181
 
182
+ # open a URI in a graphical browser
144
183
  # command must be non blocking
145
184
  def open_uri_graphical(uri)
146
185
  case Environment.os
@@ -152,6 +191,7 @@ module Aspera
152
191
  end
153
192
  end
154
193
 
194
+ # open a file in an editor
155
195
  def open_editor(file_path)
156
196
  if ENV.key?('EDITOR')
157
197
  system(ENV['EDITOR'], file_path.to_s)
@@ -166,6 +206,15 @@ module Aspera
166
206
 
167
207
  def initialize
168
208
  @url_method = self.class.default_gui_mode
209
+ @terminal_supports_unicode = nil
210
+ end
211
+
212
+ # @return true if we can display Unicode characters
213
+ # https://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html
214
+ # https://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html
215
+ def terminal_supports_unicode?
216
+ @terminal_supports_unicode = self.class.terminal? && %w(LC_ALL LC_CTYPE LANG).any?{|var|ENV[var]&.include?('UTF-8')} if @terminal_supports_unicode.nil?
217
+ return @terminal_supports_unicode
169
218
  end
170
219
 
171
220
  # Allows a user to open a Url
@@ -29,7 +29,7 @@ module Aspera
29
29
  'recipients' => faspex_pkg_delivery['recipients'],
30
30
  'workspace_id' => @app_context
31
31
  }
32
- created_package = @app_api.create_package_simple(package_data, true, @new_user_option)
32
+ created_package = @app_api.create_package_simple(package_data, true, nil)
33
33
  # but we place it in a Faspex package creation response
34
34
  return {
35
35
  'links' => { 'status' => 'unused' },
@@ -44,7 +44,7 @@ module Aspera
44
44
  'note' => faspex_pkg_delivery['note'],
45
45
  'recipients' => faspex_pkg_delivery['recipients'].map{|name|{'name'=>name}}
46
46
  }
47
- package = @app_api.create('packages', package_data)[:data]
47
+ package = @app_api.create('packages', package_data)
48
48
  # TODO: option to send from remote source or httpgw
49
49
  transfer_spec = @app_api.call(
50
50
  operation: 'POST',
@@ -85,7 +85,7 @@ module Aspera
85
85
  rescue => e
86
86
  response.status = 500
87
87
  response['Content-Type'] = 'application/json'
88
- response.body = {error: e.message}.to_json
88
+ response.body = {error: e.message, stacktrace: e.backtrace}.to_json
89
89
  Log.log.error(e.message)
90
90
  Log.log.debug{e.backtrace.join("\n")}
91
91
  end
@@ -34,7 +34,7 @@ module Aspera
34
34
  method: "#{@namespace}#{method}",
35
35
  params: args,
36
36
  id: @request_id += 1
37
- })[:data]
37
+ })
38
38
  Aspera.assert_type(data, Hash){'response'}
39
39
  Aspera.assert(data['jsonrpc'] == JSON_RPC_VERSION){'bad version in response'}
40
40
  Aspera.assert(data.key?('id')){'missing id in response'}
@@ -16,10 +16,12 @@ module Aspera
16
16
  FILE_TYPE = 'encrypted_hash_vault'
17
17
  CONTENT_KEYS = %i[label username password url description].freeze
18
18
  FILE_KEYS = %w[version type cipher data].sort.freeze
19
+ private_constant :LEGACY_CIPHER_NAME, :DEFAULT_CIPHER_NAME, :FILE_TYPE, :CONTENT_KEYS, :FILE_KEYS
19
20
  def initialize(path, current_password)
20
21
  Aspera.assert_type(path, String){'path to vault file'}
21
22
  @path = path
22
23
  @all_secrets = {}
24
+ @cipher_name = DEFAULT_CIPHER_NAME
23
25
  vault_encrypted_data = nil
24
26
  if File.exist?(@path)
25
27
  vault_file = File.read(@path)
@@ -4,7 +4,7 @@
4
4
  require 'aspera/cli/info'
5
5
  require 'aspera/log'
6
6
  require 'aspera/assert'
7
- require 'open3'
7
+ require 'aspera/environment'
8
8
 
9
9
  # enhance the gem to support other key chains
10
10
  module Aspera
@@ -48,20 +48,15 @@ module Aspera
48
48
  options[:path] = uri.path unless ['', '/'].include?(uri.path)
49
49
  options[:port] = uri.port unless uri.port.eql?(443) && !url.include?(':443/')
50
50
  end
51
- command_line = [SECURITY_UTILITY, command]
51
+ command_args = [command]
52
52
  options&.each do |k, v|
53
53
  Aspera.assert(supported.key?(k)){"unknown option: #{k}"}
54
54
  next if v.nil?
55
- command_line.push("-#{supported[k]}")
56
- command_line.push(v.shellescape) unless v.empty?
55
+ command_args.push("-#{supported[k]}")
56
+ command_args.push(v.shellescape) unless v.empty?
57
57
  end
58
- command_line.push(last_opt) unless last_opt.nil?
59
- Log.log.debug{"executing>>#{command_line.join(' ')}"}
60
- stdout, stderr, status = Open3.capture3(*command_line)
61
- Log.log.debug{"status=#{status}, stderr=#{stderr}"}
62
- Log.log.trace1{"stdout=#{stdout}"}
63
- raise "#{SECURITY_UTILITY} failed: #{status.exitstatus} : #{stderr}" unless status.success?
64
- return stdout
58
+ command_args.push(last_opt) unless last_opt.nil?
59
+ return Environment.secure_capture(exec: SECURITY_UTILITY, args: command_args)
65
60
  end
66
61
 
67
62
  def key_chains(output)
@@ -78,7 +73,7 @@ module Aspera
78
73
 
79
74
  def list(options={})
80
75
  Aspera.assert_values(options[:domain], DOMAINS, exception_class: ArgumentError){'domain'} unless options[:domain].nil?
81
- key_chains(execute('list-key_chains', options, LIST_OPTIONS))
76
+ key_chains(execute('list-keychains', options, LIST_OPTIONS))
82
77
  end
83
78
 
84
79
  def by_name(name)
data/lib/aspera/log.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/assert'
3
4
  require 'aspera/colors'
4
5
  require 'aspera/secret_hider'
5
6
  require 'logger'
6
7
  require 'pp'
7
8
  require 'json'
8
9
  require 'singleton'
10
+ require 'stringio'
9
11
 
10
12
  old_verbose = $VERBOSE
11
13
  $VERBOSE = nil
@@ -72,8 +74,7 @@ module Aspera
72
74
  JSON.pretty_generate(object) rescue PP.pp(object, +'')
73
75
  when :ruby
74
76
  PP.pp(object, +'')
75
- else
76
- raise 'wrong parameter, expect ruby or json'
77
+ else error_unexpected_value(@@format){'dump format'}
77
78
  end
78
79
  "#{name.to_s.green} (#{@@format})=\n#{result}"
79
80
  end
@@ -126,8 +127,7 @@ module Aspera
126
127
  Syslog::Logger.make_methods(severity.downcase)
127
128
  end
128
129
  @logger = Syslog::Logger.new(@program_name, Syslog::LOG_LOCAL2)
129
- else
130
- raise "unknown log type: #{new_log_type}, use one of: #{LOG_TYPES.join(', ')}"
130
+ else error_unexpected_value(new_log_type){"log type (#{LOG_TYPES.join(', ')})"}
131
131
  end
132
132
  @logger.level = current_severity_integer
133
133
  @logger_type = new_log_type
@@ -39,7 +39,7 @@ module Aspera
39
39
  set_json_response(response, {
40
40
  application: 'node',
41
41
  current_time: Time.now.utc.iso8601(0),
42
- version: info['ascp_version'].gsub(/ .*$/, ''),
42
+ version: info['sdk_ascp_version'].gsub(/ .*$/, ''),
43
43
  license_expiration_date: info['expiration_date'],
44
44
  license_max_rate: info['maximum_bandwidth'],
45
45
  os: %x(uname -srv).chomp,
@@ -3,64 +3,66 @@
3
3
  require 'aspera/oauth/factory'
4
4
  require 'aspera/log'
5
5
  require 'aspera/assert'
6
- require 'aspera/id_generator'
7
6
  require 'date'
8
7
 
9
8
  module Aspera
10
9
  module OAuth
11
- # Implement OAuth 2 for the REST client and generate a bearer token
12
- # bearer tokens are cached in memory and in a file cache for later re-use
10
+ # OAuth 2 client for the REST client
11
+ # Generate bearer token
12
+ # Bearer tokens are cached in memory and in a file cache for later re-use
13
13
  # https://tools.ietf.org/html/rfc6749
14
14
  class Base
15
- # scope can be modified after creation
16
- attr_writer :scope
17
-
18
- # [M]=mandatory [D]=has default value [O]=Optional/nil
19
- # @param base_url [M] URL of authentication API
20
- # @param auth [O] basic auth parameters
21
- # @param client_id [O]
22
- # @param client_secret [O]
23
- # @param scope [O]
24
- # @param path_token [D] API end point to create a token
25
- # @param token_field [D] field in result that contains the token
15
+ # @param ** Parameters for REST
16
+ # @param client_id [String, nil]
17
+ # @param client_secret [String, nil]
18
+ # @param scope [String, nil]
19
+ # @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 token_field [String] Field in result that contains the token
22
+ # @param cache_ids [Array, nil] List of unique identifiers for cache id generation
26
23
  def initialize(
27
- base_url:,
28
- auth: nil,
29
24
  client_id: nil,
30
25
  client_secret: nil,
31
26
  scope: nil,
32
27
  use_query: false,
33
- path_token: 'token', # default endpoint for /token to generate token
34
- token_field: 'access_token' # field with token in result of call to path_token
28
+ path_token: 'token',
29
+ token_field: 'access_token',
30
+ cache_ids: nil,
31
+ **rest_params
35
32
  )
36
- Aspera.assert_type(base_url, String)
37
33
  Aspera.assert(respond_to?(:create_token), 'create_token method must be defined', exception_class: InternalError)
38
- @base_url = base_url
34
+ # this is the OAuth API
35
+ @api = Rest.new(**rest_params)
39
36
  @path_token = path_token
40
37
  @token_field = token_field
41
38
  @client_id = client_id
42
39
  @client_secret = client_secret
43
- @scope = scope
44
40
  @use_query = use_query
45
- @identifiers = []
46
- @identifiers.push(auth[:username]) if auth.is_a?(Hash) && auth.key?(:username)
47
- # this is the OAuth API
48
- @api = Rest.new(
49
- base_url: @base_url,
50
- redirect_max: 2,
51
- auth: auth)
41
+ @base_cache_ids = cache_ids.clone
42
+ @base_cache_ids = [] if @base_cache_ids.nil?
43
+ Aspera.assert_type(@base_cache_ids, Array)
44
+ if @api.auth_params.key?(:username)
45
+ cache_ids.push(@api.auth_params[:username])
46
+ end
47
+ @base_cache_ids.freeze
48
+ self.scope = scope
49
+ end
50
+
51
+ # Scope can be modified after creation, then update identifier for cache
52
+ def scope=(scope)
53
+ @scope = scope
54
+ # generate token unique identifier for persistency (memory/disk cache)
55
+ @token_cache_id = Factory.cache_id(@api.base_url, self.class, @base_cache_ids, @scope)
52
56
  end
53
57
 
54
58
  # helper method to create token as per RFC
55
59
  def create_token_call(creation_params)
56
60
  Log.log.debug{'Generating a new token'.bg_green}
57
- payload = {
58
- body: creation_params,
59
- body_type: :www
60
- }
61
+ payload = { body_type: :www }
61
62
  if @use_query
62
63
  payload[:query] = creation_params
63
- payload[:body] = {}
64
+ else
65
+ payload[:body] = creation_params
64
66
  end
65
67
  return @api.call(
66
68
  operation: 'POST',
@@ -85,16 +87,8 @@ module Aspera
85
87
  # @param cache set to false to disable cache
86
88
  # @param refresh set to true to force refresh or re-generation (if previous failed)
87
89
  def token(cache: true, refresh: false)
88
- # generate token unique identifier for persistency (memory/disk cache)
89
- token_id = IdGenerator.from_list(Factory.id(
90
- @base_url,
91
- Factory.class_to_id(self.class),
92
- @identifiers,
93
- @scope
94
- ))
95
-
96
90
  # get token_data from cache (or nil), token_data is what is returned by /token
97
- token_data = Factory.instance.persist_mgr.get(token_id) if cache
91
+ token_data = Factory.instance.persist_mgr.get(@token_cache_id) if cache
98
92
  token_data = JSON.parse(token_data) unless token_data.nil?
99
93
  # Optional optimization: check if node token is expired based on decoded content then force refresh if close enough
100
94
  # might help in case the transfer agent cannot refresh himself
@@ -120,7 +114,7 @@ module Aspera
120
114
  refresh_token = token_data['refresh_token']
121
115
  end
122
116
  # delete cache
123
- Factory.instance.persist_mgr.delete(token_id)
117
+ Factory.instance.persist_mgr.delete(@token_cache_id)
124
118
  token_data = nil
125
119
  # lets try the existing refresh token
126
120
  if !refresh_token.nil?
@@ -132,7 +126,7 @@ module Aspera
132
126
  # save only if success
133
127
  json_data = resp[:http].body
134
128
  token_data = JSON.parse(json_data)
135
- Factory.instance.persist_mgr.put(token_id, json_data)
129
+ Factory.instance.persist_mgr.put(@token_cache_id, json_data)
136
130
  else
137
131
  Log.log.debug{"refresh failed: #{resp[:http].body}".bg_red}
138
132
  end
@@ -144,7 +138,7 @@ module Aspera
144
138
  resp = create_token
145
139
  json_data = resp[:http].body
146
140
  token_data = JSON.parse(json_data)
147
- Factory.instance.persist_mgr.put(token_id, json_data)
141
+ Factory.instance.persist_mgr.put(@token_cache_id, json_data)
148
142
  end
149
143
  Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"}
150
144
  # ok we shall have a token here
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'singleton'
3
+ require 'aspera/id_generator'
4
4
  require 'aspera/assert'
5
+ require 'singleton'
5
6
  require 'base64'
6
7
  module Aspera
7
8
  module OAuth
@@ -29,11 +30,17 @@ module Aspera
29
30
  return token[BEARER_PREFIX.length..-1]
30
31
  end
31
32
 
32
- def id(*params)
33
- return [PERSIST_CATEGORY_TOKEN, *params].flatten
33
+ # @return a cache identifier
34
+ def cache_id(url, creator_class, *params)
35
+ return IdGenerator.from_list([
36
+ PERSIST_CATEGORY_TOKEN,
37
+ url,
38
+ Factory.class_to_id(creator_class),
39
+ *params
40
+ ].flatten)
34
41
  end
35
42
 
36
- # snake version of class name is the identifier
43
+ # @return snake version of class name
37
44
  def class_to_id(creator_class)
38
45
  return creator_class.name.split('::').last.capital_to_snake.to_sym
39
46
  end
@@ -13,17 +13,13 @@ module Aspera
13
13
  receiver_client_ids: nil,
14
14
  **base_params
15
15
  )
16
- super(**base_params)
16
+ super(**base_params, cache_ids: [grant_type&.split(':')&.last, apikey, response_type])
17
17
  @create_params = {
18
18
  grant_type: grant_type
19
19
  }
20
- @create_params[:response_type] = response_type if response_type
21
- @create_params[:apikey] = apikey if apikey
22
- @create_params[:receiver_client_ids] = receiver_client_ids if receiver_client_ids
23
- @identifiers.push(
24
- @create_params[:grant_type]&.split(':')&.last,
25
- @create_params[:apikey],
26
- @create_params[:response_type])
20
+ @create_params[:response_type] = response_type unless response_type.nil?
21
+ @create_params[:apikey] = apikey unless apikey.nil?
22
+ @create_params[:receiver_client_ids] = receiver_client_ids unless receiver_client_ids.nil?
27
23
  end
28
24
 
29
25
  def create_token
@@ -13,6 +13,7 @@ module Aspera
13
13
  # https://tools.ietf.org/html/rfc7523
14
14
  # https://tools.ietf.org/html/rfc7519
15
15
  class Jwt < Base
16
+ GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
16
17
  # @param private_key_obj private key object
17
18
  # @param payload payload to be included in the JWT
18
19
  # @param headers headers to be included in the JWT
@@ -25,17 +26,16 @@ module Aspera
25
26
  Aspera.assert_type(private_key_obj, OpenSSL::PKey::RSA){'private_key_obj'}
26
27
  Aspera.assert_type(payload, Hash){'payload'}
27
28
  Aspera.assert_type(headers, Hash){'headers'}
28
- super(**base_params)
29
+ super(**base_params, cache_ids: [payload[:sub]])
29
30
  @private_key_obj = private_key_obj
30
31
  @additional_payload = payload
31
32
  @headers = headers
32
- @identifiers.push(@additional_payload[:sub])
33
33
  end
34
34
 
35
35
  def create_token
36
36
  require 'jwt'
37
37
  seconds_since_epoch = Time.new.to_i
38
- Log.log.info{"seconds=#{seconds_since_epoch}"}
38
+ Log.log.debug{"seconds_since_epoch=#{seconds_since_epoch}"}
39
39
  jwt_payload = {
40
40
  exp: seconds_since_epoch + OAuth::Factory.instance.parameters[:jwt_expiry_offset_sec], # expiration time
41
41
  nbf: seconds_since_epoch - OAuth::Factory.instance.parameters[:jwt_accepted_offset_sec], # not before
@@ -46,7 +46,7 @@ module Aspera
46
46
  Log.log.debug{"private=[#{@private_key_obj}]"}
47
47
  assertion = JWT.encode(jwt_payload, @private_key_obj, 'RS256', @headers)
48
48
  Log.log.debug{"assertion=[#{assertion}]"}
49
- return create_token_call(optional_scope_client_id.merge(grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: assertion))
49
+ return create_token_call(optional_scope_client_id.merge(grant_type: GRANT_TYPE, assertion: assertion))
50
50
  end
51
51
  end
52
52
  Factory.instance.register_token_creator(Jwt)
@@ -6,15 +6,16 @@ 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
11
  def initialize(
10
12
  url:,
11
13
  json:,
12
14
  **generic_params
13
15
  )
14
- super(**generic_params)
16
+ super(**generic_params, cache_ids: [json[:url_token]])
15
17
  @body = json
16
18
  @query = url
17
- @identifiers.push(@body[:url_token])
18
19
  end
19
20
 
20
21
  def create_token
@@ -8,8 +8,11 @@ module Aspera
8
8
  module OAuth
9
9
  # Authentication using Web browser
10
10
  class Web < Base
11
- # @param g_o:redirect_uri [M] for type :web
12
- # @param g_o:path_authorize [D] for type :web
11
+ class << self
12
+ attr_accessor :additionnal_info
13
+ end
14
+ # @param redirect_uri url to receive the code after auth (to be exchanged for token)
15
+ # @param path_authorize path to login page on web app
13
16
  def initialize(
14
17
  redirect_uri:,
15
18
  path_authorize: 'authorize',
@@ -21,18 +24,19 @@ module Aspera
21
24
  uri = URI.parse(@redirect_uri)
22
25
  Aspera.assert(%w[http https].include?(uri.scheme)){'redirect_uri scheme must be http or https'}
23
26
  Aspera.assert(!uri.port.nil?){'redirect_uri must have a port'}
24
- # TODO: we could check that host is localhost or local address
27
+ # TODO: we could check that host is localhost or local address, as we are going to listen locally
25
28
  end
26
29
 
27
30
  def create_token
28
- random_state = SecureRandom.uuid # used to check later
31
+ # generate secure state to check later
32
+ random_state = SecureRandom.uuid
29
33
  login_page_url = Rest.build_uri(
30
- "#{@base_url}/#{@path_authorize}",
34
+ "#{@api.base_url}/#{@path_authorize}",
31
35
  optional_scope_client_id.merge(response_type: 'code', redirect_uri: @redirect_uri, state: random_state))
32
36
  # here, we need a human to authorize on a web page
33
37
  Log.log.info{"login_page_url=#{login_page_url}".bg_red.gray}
34
38
  # start a web server to receive request code
35
- web_server = WebAuth.new(@redirect_uri)
39
+ web_server = WebAuth.new(@redirect_uri, self.class.additionnal_info)
36
40
  # start browser on login page
37
41
  Environment.instance.open_uri(login_page_url)
38
42
  # wait for code in request
@@ -7,6 +7,13 @@ require 'aspera/assert'
7
7
  module Aspera
8
8
  # Persist data on file system
9
9
  class PersistencyActionOnce
10
+ DELETE_DEFAULT = lambda{|d|d.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}
14
+ MANAGER_METHODS = %i[get put delete]
15
+ private_constant :DELETE_DEFAULT, :PARSE_DEFAULT, :FORMAT_DEFAULT, :MERGE_DEFAULT, :MANAGER_METHODS
16
+
10
17
  # @param :manager Mandatory Database
11
18
  # @param :data Mandatory object to persist, must be same object from begin to end (assume array by default)
12
19
  # @param :id Mandatory identifiers
@@ -14,21 +21,22 @@ module Aspera
14
21
  # @param :parse Optional parse method (default to JSON)
15
22
  # @param :format Optional dump method (default to JSON)
16
23
  # @param :merge Optional merge data from file to current data
17
- def initialize(manager:, data:, id:, delete: nil, parse: nil, format: nil, merge: nil)
18
- Aspera.assert(!manager.nil?)
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}"}
19
26
  Aspera.assert(!data.nil?)
20
27
  Aspera.assert_type(id, String)
21
28
  Aspera.assert(!id.empty?)
29
+ Aspera.assert_type(delete, Proc)
30
+ Aspera.assert_type(parse, Proc)
31
+ Aspera.assert_type(format, Proc)
32
+ Aspera.assert_type(merge, Proc)
22
33
  @manager = manager
23
34
  @persisted_object = data
24
35
  @object_id = id
25
- # by default , at save time, file is deleted if data is nil
26
- @delete_condition = delete || lambda{|d|d.empty?}
27
- @persist_format = format || lambda {|h| JSON.generate(h)}
28
- persist_parse = parse || lambda {|t| JSON.parse(t)}
29
- persist_merge = merge || lambda {|current, file| current.concat(file).uniq rescue current}
36
+ @delete_condition = delete
37
+ @persist_format = format
30
38
  value = @manager.get(@object_id)
31
- persist_merge.call(@persisted_object, persist_parse.call(value)) unless value.nil?
39
+ merge.call(@persisted_object, parse.call(value)) unless value.nil?
32
40
  end
33
41
 
34
42
  def save