aspera-cli 4.17.0 → 4.18.1

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 +3 -4
  3. data/CHANGELOG.md +33 -0
  4. data/CONTRIBUTING.md +15 -1
  5. data/README.md +711 -432
  6. data/bin/ascli +5 -0
  7. data/bin/asession +2 -2
  8. data/examples/build_package.sh +28 -0
  9. data/lib/aspera/agent/alpha.rb +10 -8
  10. data/lib/aspera/agent/base.rb +9 -6
  11. data/lib/aspera/agent/connect.rb +7 -8
  12. data/lib/aspera/agent/direct.rb +56 -37
  13. data/lib/aspera/agent/httpgw.rb +23 -324
  14. data/lib/aspera/agent/node.rb +19 -20
  15. data/lib/aspera/agent/trsdk.rb +19 -20
  16. data/lib/aspera/api/aoc.rb +17 -14
  17. data/lib/aspera/api/cos_node.rb +4 -4
  18. data/lib/aspera/api/httpgw.rb +342 -0
  19. data/lib/aspera/api/node.rb +135 -89
  20. data/lib/aspera/ascmd.rb +4 -3
  21. data/lib/aspera/ascp/installation.rb +15 -7
  22. data/lib/aspera/ascp/management.rb +2 -2
  23. data/lib/aspera/ascp/products.rb +1 -1
  24. data/lib/aspera/cli/basic_auth_plugin.rb +5 -9
  25. data/lib/aspera/cli/extended_value.rb +35 -16
  26. data/lib/aspera/cli/formatter.rb +161 -70
  27. data/lib/aspera/cli/hints.rb +18 -0
  28. data/lib/aspera/cli/main.rb +32 -39
  29. data/lib/aspera/cli/manager.rb +151 -119
  30. data/lib/aspera/cli/plugin.rb +27 -21
  31. data/lib/aspera/cli/plugin_factory.rb +31 -20
  32. data/lib/aspera/cli/plugins/alee.rb +14 -2
  33. data/lib/aspera/cli/plugins/aoc.rb +152 -141
  34. data/lib/aspera/cli/plugins/ats.rb +1 -1
  35. data/lib/aspera/cli/plugins/config.rb +72 -65
  36. data/lib/aspera/cli/plugins/console.rb +8 -5
  37. data/lib/aspera/cli/plugins/faspex.rb +32 -23
  38. data/lib/aspera/cli/plugins/faspex5.rb +232 -156
  39. data/lib/aspera/cli/plugins/faspio.rb +85 -0
  40. data/lib/aspera/cli/plugins/httpgw.rb +55 -0
  41. data/lib/aspera/cli/plugins/node.rb +129 -64
  42. data/lib/aspera/cli/plugins/orchestrator.rb +33 -30
  43. data/lib/aspera/cli/plugins/preview.rb +7 -3
  44. data/lib/aspera/cli/plugins/server.rb +6 -6
  45. data/lib/aspera/cli/plugins/shares.rb +16 -14
  46. data/lib/aspera/cli/special_values.rb +13 -0
  47. data/lib/aspera/cli/sync_actions.rb +10 -10
  48. data/lib/aspera/cli/transfer_agent.rb +7 -6
  49. data/lib/aspera/cli/version.rb +1 -1
  50. data/lib/aspera/environment.rb +70 -9
  51. data/lib/aspera/faspex_gw.rb +5 -4
  52. data/lib/aspera/faspex_postproc.rb +2 -2
  53. data/lib/aspera/log.rb +6 -3
  54. data/lib/aspera/node_simulator.rb +2 -2
  55. data/lib/aspera/oauth/base.rb +31 -19
  56. data/lib/aspera/oauth/factory.rb +12 -13
  57. data/lib/aspera/oauth/generic.rb +1 -0
  58. data/lib/aspera/oauth/jwt.rb +18 -15
  59. data/lib/aspera/oauth/url_json.rb +8 -6
  60. data/lib/aspera/oauth/web.rb +2 -2
  61. data/lib/aspera/persistency_folder.rb +2 -2
  62. data/lib/aspera/preview/generator.rb +3 -3
  63. data/lib/aspera/preview/options.rb +3 -3
  64. data/lib/aspera/preview/terminal.rb +4 -4
  65. data/lib/aspera/preview/utils.rb +3 -3
  66. data/lib/aspera/proxy_auto_config.rb +5 -1
  67. data/lib/aspera/rest.rb +105 -88
  68. data/lib/aspera/rest_call_error.rb +1 -1
  69. data/lib/aspera/rest_error_analyzer.rb +2 -2
  70. data/lib/aspera/rest_errors_aspera.rb +1 -1
  71. data/lib/aspera/resumer.rb +1 -1
  72. data/lib/aspera/secret_hider.rb +2 -4
  73. data/lib/aspera/ssh.rb +1 -1
  74. data/lib/aspera/transfer/parameters.rb +39 -36
  75. data/lib/aspera/transfer/spec.rb +2 -0
  76. data/lib/aspera/transfer/sync.rb +2 -1
  77. data/lib/aspera/transfer/uri.rb +1 -1
  78. data/lib/aspera/uri_reader.rb +5 -4
  79. data/lib/aspera/web_auth.rb +1 -1
  80. data/lib/aspera/web_server_simple.rb +4 -3
  81. data.tar.gz.sig +0 -0
  82. metadata +7 -4
  83. metadata.gz.sig +0 -0
  84. data/lib/aspera/cli/plugins/bss.rb +0 -71
  85. data/lib/aspera/open_application.rb +0 -71
@@ -7,7 +7,7 @@ module Aspera
7
7
  module Plugins
8
8
  # Plugin for Aspera Shares v1
9
9
  class Shares < Cli::BasicAuthPlugin
10
- API_BASE = 'node_api'
10
+ NODE_API_PREFIX = 'node_api'
11
11
  class << self
12
12
  def detect(address_or_url)
13
13
  address_or_url = "https://#{address_or_url}" unless address_or_url.match?(%r{^[a-z]{1,6}://})
@@ -16,7 +16,7 @@ module Aspera
16
16
  begin
17
17
  # shall fail: shares requires auth, but we check error message
18
18
  # TODO: use ping instead ?
19
- api.read("#{API_BASE}/app")
19
+ api.read("#{NODE_API_PREFIX}/app")
20
20
  rescue RestCallError => e
21
21
  if e.response.code.to_s.eql?('401') && e.response.body.eql?('{"error":{"user_message":"API user authentication failed"}}')
22
22
  found = true
@@ -64,22 +64,24 @@ module Aspera
64
64
  when :health
65
65
  nagios = Nagios.new
66
66
  begin
67
- Rest
68
- .new(base_url: "#{options.get_option(:url, mandatory: true)}/#{API_BASE}")
67
+ res = Rest
68
+ .new(base_url: "#{options.get_option(:url, mandatory: true)}/#{NODE_API_PREFIX}")
69
69
  .call(
70
70
  operation: 'GET',
71
71
  subpath: 'ping',
72
- headers: {'content-type': 'application/json'},
73
- return_error: true)
72
+ headers: {'content-type': 'application/json'})
73
+ raise 'Shares not detected' unless res[:http].body.eql?(' ')
74
74
  nagios.add_ok('shares api', 'accessible')
75
75
  rescue StandardError => e
76
- nagios.add_critical('node api', e.to_s)
76
+ nagios.add_critical('API', e.to_s)
77
77
  end
78
78
  return nagios.result
79
79
  when :repository, :files
80
- api_shares_node = basic_auth_api(API_BASE)
80
+ api_shares_node = basic_auth_api(NODE_API_PREFIX)
81
81
  repo_command = options.get_next_command(Node::COMMANDS_SHARES)
82
- return Node.new(**init_params, api: api_shares_node).execute_action(repo_command)
82
+ return Node
83
+ .new(**init_params, api: api_shares_node)
84
+ .execute_action(repo_command)
83
85
  when :admin
84
86
  api_shares_admin = basic_auth_api('api/v1')
85
87
  admin_command = options.get_next_command(%i[node share transfer_settings user group].freeze)
@@ -150,8 +152,8 @@ module Aspera
150
152
  end
151
153
  end
152
154
  end
153
- end # execute action
154
- end # Shares
155
- end # Plugins
156
- end # Cli
157
- end # Aspera
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aspera
4
+ module Cli
5
+ # base class for plugins modules
6
+ module SpecialValues
7
+ # special values
8
+ INIT = 'INIT'
9
+ ALL = 'ALL'
10
+ DEF = 'DEF'
11
+ end
12
+ end
13
+ end
@@ -29,9 +29,9 @@ module Aspera
29
29
  SIMPLE_ARGUMENTS_SYNC.each do |arg, check|
30
30
  value = options.get_next_argument(
31
31
  arg,
32
- type: check.is_a?(Class) ? check : nil,
33
- expected: check.is_a?(Class) ? :single : check,
34
- mandatory: false)
32
+ mandatory: false,
33
+ validation: check.is_a?(Class) ? check : nil,
34
+ accept_list: check.is_a?(Class) ? nil : check)
35
35
  break if value.nil?
36
36
  simple_session_args[arg] = value.to_s
37
37
  end
@@ -57,12 +57,12 @@ module Aspera
57
57
  command2 = options.get_next_command([:status])
58
58
  case command2
59
59
  when :status
60
- sync_session_name = options.get_next_argument('name of sync session', mandatory: false, type: String)
60
+ sync_session_name = options.get_next_argument('name of sync session', mandatory: false, validation: String)
61
61
  async_params = options.get_option(:sync_info, mandatory: true)
62
62
  return {type: :single_object, data: Transfer::Sync.admin_status(async_params, sync_session_name)}
63
- end # command2
64
- end # command
65
- end # execute_action
66
- end # SyncActions
67
- end # Cli
68
- end # Aspera
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -26,6 +26,7 @@ module Aspera
26
26
 
27
27
  <%=ts.to_yaml%>
28
28
  END_OF_TEMPLATE
29
+ CP4I_REMOTE_HOST_LB = 'N/A'
29
30
  # % (formatting bug in eclipse)
30
31
  private_constant :FILE_LIST_FROM_ARGS,
31
32
  :FILE_LIST_FROM_TRANSFER_SPEC,
@@ -101,8 +102,6 @@ module Aspera
101
102
  def agent_instance
102
103
  return @agent unless @agent.nil?
103
104
  agent_type = @opt_mgr.get_option(:transfer, mandatory: true)
104
- # agent plugin is loaded on demand to avoid loading unnecessary dependencies
105
- require "aspera/agent/#{agent_type}"
106
105
  # set keys as symbols
107
106
  agent_options = @opt_mgr.get_option(:transfer_info).symbolize_keys
108
107
  # special cases
@@ -126,8 +125,7 @@ module Aspera
126
125
  end
127
126
  agent_options[:progress] = @config.progress_bar
128
127
  # get agent instance
129
- new_agent = Kernel.const_get("Aspera::Agent::#{agent_type.capitalize}").new(agent_options)
130
- self.agent_instance = new_agent
128
+ self.agent_instance = Agent::Base.factory_create(agent_type, agent_options)
131
129
  Log.log.debug{"transfer agent is a #{@agent.class}"}
132
130
  return @agent
133
131
  end
@@ -177,7 +175,7 @@ module Aspera
177
175
  when nil, FILE_LIST_FROM_ARGS
178
176
  Log.log.debug('getting file list as parameters')
179
177
  # get remaining arguments
180
- file_list = @opt_mgr.get_next_argument('source file list', expected: :multiple)
178
+ file_list = @opt_mgr.get_next_argument('source file list', multiple: true)
181
179
  raise Cli::BadArgument, 'specify at least one file on command line or use ' \
182
180
  "--sources=#{FILE_LIST_FROM_TRANSFER_SPEC} to use transfer spec" if !file_list.is_a?(Array) || file_list.empty?
183
181
  when FILE_LIST_FROM_TRANSFER_SPEC
@@ -218,6 +216,9 @@ module Aspera
218
216
  def start(transfer_spec, rest_token: nil)
219
217
  # check parameters
220
218
  Aspera.assert_type(transfer_spec, Hash){'transfer_spec'}
219
+ if transfer_spec['remote_host'].eql?(CP4I_REMOTE_HOST_LB)
220
+ raise "Wrong remote host: #{CP4I_REMOTE_HOST_LB}"
221
+ end
221
222
  # process :src option
222
223
  case transfer_spec['direction']
223
224
  when Transfer::Spec::DIRECTION_RECEIVE
@@ -243,7 +244,7 @@ module Aspera
243
244
  updated_ts(transfer_spec)
244
245
  # if TS from app has content_protection (e.g. F5), that means content is protected: ask password if not provided
245
246
  if transfer_spec['content_protection'].eql?('decrypt') && !transfer_spec.key?('content_protection_password')
246
- transfer_spec['content_protection_password'] = @opt_mgr.prompt_user_input('content protection password', true)
247
+ transfer_spec['content_protection_password'] = @opt_mgr.prompt_user_input('content protection password', sensitive: true)
247
248
  end
248
249
  # create transfer agent
249
250
  agent_instance.start_transfer(transfer_spec, token_regenerator: rest_token)
@@ -4,6 +4,6 @@ module Aspera
4
4
  module Cli
5
5
  # for beta add extension : .beta1
6
6
  # for dev version add extension : .pre
7
- VERSION = '4.17.0'
7
+ VERSION = '4.18.1'
8
8
  end
9
9
  end
@@ -4,17 +4,21 @@
4
4
  require 'aspera/log'
5
5
  require 'aspera/assert'
6
6
  require 'rbconfig'
7
+ require 'singleton'
7
8
 
8
9
  # cspell:words MEBI mswin bccwin
9
10
 
10
11
  module Aspera
11
12
  # detect OS, architecture, and specific stuff
12
13
  class Environment
14
+ include Singleton
15
+ USER_INTERFACES = %i[text graphical].freeze
16
+
13
17
  OS_WINDOWS = :windows
14
- OS_X = :osx
18
+ OS_MACOS = :osx
15
19
  OS_LINUX = :linux
16
20
  OS_AIX = :aix
17
- OS_LIST = [OS_WINDOWS, OS_X, OS_LINUX, OS_AIX].freeze
21
+ OS_LIST = [OS_WINDOWS, OS_MACOS, OS_LINUX, OS_AIX].freeze
18
22
  CPU_X86_64 = :x86_64
19
23
  CPU_ARM64 = :arm64
20
24
  CPU_PPC64 = :ppc64
@@ -27,6 +31,7 @@ module Aspera
27
31
  BYTES_PER_MEBIBIT = MEBI / BITS_PER_BYTE
28
32
 
29
33
  class << self
34
+ @terminal_supports_unicode = nil
30
35
  def ruby_version
31
36
  return RbConfig::CONFIG['RUBY_PROGRAM_VERSION']
32
37
  end
@@ -36,7 +41,7 @@ module Aspera
36
41
  when /mswin/, /msys/, /mingw/, /cygwin/, /bccwin/, /wince/, /emc/
37
42
  return OS_WINDOWS
38
43
  when /darwin/, /mac os/
39
- return OS_X
44
+ return OS_MACOS
40
45
  when /linux/
41
46
  return OS_LINUX
42
47
  when /aix/
@@ -121,10 +126,66 @@ module Aspera
121
126
  end
122
127
 
123
128
  # @return true if we can display Unicode characters
124
- def use_unicode?
125
- @use_unicode = terminal? && ENV.values_at('LC_ALL', 'LC_CTYPE', 'LANG').compact.first.include?('UTF-8') if @use_unicode.nil?
126
- return @use_unicode
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
+
136
+ def default_gui_mode
137
+ # assume not remotely connected on macos and windows
138
+ return :graphical if [Environment::OS_WINDOWS, Environment::OS_MACOS].include?(Environment.os)
139
+ # unix family
140
+ return :graphical if ENV.key?('DISPLAY') && !ENV['DISPLAY'].empty?
141
+ return :text
142
+ end
143
+
144
+ # command must be non blocking
145
+ def open_uri_graphical(uri)
146
+ case Environment.os
147
+ when Environment::OS_MACOS then return system('open', uri.to_s)
148
+ when Environment::OS_WINDOWS then return system('start', 'explorer', %Q{"#{uri}"})
149
+ when Environment::OS_LINUX then return system('xdg-open', uri.to_s)
150
+ else
151
+ raise "no graphical open method for #{Environment.os}"
152
+ end
153
+ end
154
+
155
+ def open_editor(file_path)
156
+ if ENV.key?('EDITOR')
157
+ system(ENV['EDITOR'], file_path.to_s)
158
+ elsif Environment.os.eql?(Environment::OS_WINDOWS)
159
+ system('notepad.exe', %Q{"#{file_path}"})
160
+ else
161
+ open_uri_graphical(file_path.to_s)
162
+ end
163
+ end
164
+ end
165
+ attr_accessor :url_method
166
+
167
+ def initialize
168
+ @url_method = self.class.default_gui_mode
169
+ end
170
+
171
+ # Allows a user to open a Url
172
+ # if method is "text", then URL is displayed on terminal
173
+ # if method is "graphical", then the URL will be opened with the default browser.
174
+ # this is non blocking
175
+ def open_uri(the_url)
176
+ case @url_method
177
+ when :graphical
178
+ self.class.open_uri_graphical(the_url)
179
+ when :text
180
+ case the_url.to_s
181
+ when /^http/
182
+ puts "USER ACTION: please enter this url in a browser:\n#{the_url.to_s.red}\n"
183
+ else
184
+ puts "USER ACTION: open this:\n#{the_url.to_s.red}\n"
185
+ end
186
+ else
187
+ raise StandardError, "unsupported url open method: #{@url_method}"
127
188
  end
128
- end # self
129
- end # Environment
130
- end # Aspera
189
+ end
190
+ end
191
+ end
@@ -50,8 +50,9 @@ module Aspera
50
50
  operation: 'POST',
51
51
  subpath: "packages/#{package['id']}/transfer_spec/upload",
52
52
  headers: {'Accept' => 'application/json'},
53
- url_params: {transfer_type: Cli::Plugins::Faspex5::TRANSFER_CONNECT},
54
- json_params: {paths: [{'destination'=>'/'}]}
53
+ query: {transfer_type: Cli::Plugins::Faspex5::TRANSFER_CONNECT},
54
+ body: {paths: [{'destination'=>'/'}]},
55
+ body_type: :json
55
56
  )[:data]
56
57
  transfer_spec.delete('authentication')
57
58
  # but we place it in a Faspex package creation response
@@ -94,5 +95,5 @@ module Aspera
94
95
  response.body = {error: 'Unsupported endpoint'}.to_json
95
96
  end
96
97
  end
97
- end # Faspex4GWServlet
98
- end # Aspera
98
+ end
99
+ end
@@ -74,5 +74,5 @@ module Aspera
74
74
  response.body = {status: 'error', script: script_path, message: e.message}.to_json
75
75
  end
76
76
  end
77
- end # Faspex4PostProcServlet
78
- end # Aspera
77
+ end
78
+ end
data/lib/aspera/log.rb CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  require 'aspera/colors'
4
4
  require 'aspera/secret_hider'
5
- require 'aspera/environment'
6
- require 'aspera/assert'
7
5
  require 'logger'
8
6
  require 'pp'
9
7
  require 'json'
10
8
  require 'singleton'
11
9
 
10
+ old_verbose = $VERBOSE
11
+ $VERBOSE = nil
12
+
12
13
  # extend Ruby logger with trace levels
13
14
  class Logger
14
15
  TRACE_MAX = 2
@@ -44,6 +45,8 @@ class Logger
44
45
  Logger::Severity.constants.each { |severity| make_methods(severity) }
45
46
  end
46
47
 
48
+ $VERBOSE = old_verbose
49
+
47
50
  module Aspera
48
51
  # Singleton object for logging
49
52
  class Log
@@ -84,7 +87,7 @@ module Aspera
84
87
  ensure
85
88
  $stderr = real_stderr
86
89
  end
87
- end # class
90
+ end
88
91
 
89
92
  attr_reader :logger_type, :logger
90
93
  attr_writer :program_name
@@ -209,5 +209,5 @@ module Aspera
209
209
  session_id: 'bafc72b8-366c-4501-8095-47208183d6b8'}]
210
210
  }
211
211
  end
212
- end # NodeSimulatorServlet
213
- end # Aspera
212
+ end
213
+ end
@@ -9,9 +9,7 @@ require 'date'
9
9
  module Aspera
10
10
  module OAuth
11
11
  # Implement OAuth 2 for the REST client and generate a bearer token
12
- # call get_authorization() to get a token.
13
- # bearer tokens are kept in memory and also in a file cache for later re-use
14
- # if a token is expired (api returns 4xx), call again get_authorization(refresh: true)
12
+ # bearer tokens are cached in memory and in a file cache for later re-use
15
13
  # https://tools.ietf.org/html/rfc6749
16
14
  class Base
17
15
  # scope can be modified after creation
@@ -31,6 +29,7 @@ module Aspera
31
29
  client_id: nil,
32
30
  client_secret: nil,
33
31
  scope: nil,
32
+ use_query: false,
34
33
  path_token: 'token', # default endpoint for /token to generate token
35
34
  token_field: 'access_token' # field with token in result of call to path_token
36
35
  )
@@ -42,6 +41,7 @@ module Aspera
42
41
  @client_id = client_id
43
42
  @client_secret = client_secret
44
43
  @scope = scope
44
+ @use_query = use_query
45
45
  @identifiers = []
46
46
  @identifiers.push(auth[:username]) if auth.is_a?(Hash) && auth.key?(:username)
47
47
  # this is the OAuth API
@@ -52,13 +52,22 @@ module Aspera
52
52
  end
53
53
 
54
54
  # helper method to create token as per RFC
55
- def create_token_call(www_params)
55
+ def create_token_call(creation_params)
56
56
  Log.log.debug{'Generating a new token'.bg_green}
57
+ payload = {
58
+ body: creation_params,
59
+ body_type: :www
60
+ }
61
+ if @use_query
62
+ payload[:query] = creation_params
63
+ payload[:body] = {}
64
+ end
57
65
  return @api.call(
58
- operation: 'POST',
59
- subpath: @path_token,
60
- headers: {'Accept' => 'application/json'},
61
- www_body_params: www_params)
66
+ operation: 'POST',
67
+ subpath: @path_token,
68
+ headers: {'Accept' => 'application/json'},
69
+ **payload
70
+ )
62
71
  end
63
72
 
64
73
  # @return Hash with optional general parameters
@@ -70,24 +79,27 @@ module Aspera
70
79
  return call_params
71
80
  end
72
81
 
73
- # OAuth v2 token generation
74
- # @param use_refresh_token set to true to force refresh or re-generation (if previous failed)
75
- def get_authorization(use_refresh_token: false, use_cache: true)
82
+ # get an OAuth v2 token (generated, cached, refreshed)
83
+ # call token() to get a token.
84
+ # if a token is expired (api returns 4xx), call again token(refresh: true)
85
+ # @param cache set to false to disable cache
86
+ # @param refresh set to true to force refresh or re-generation (if previous failed)
87
+ def token(cache: true, refresh: false)
76
88
  # generate token unique identifier for persistency (memory/disk cache)
77
89
  token_id = IdGenerator.from_list(Factory.id(
78
90
  @base_url,
79
- @grant_method,
91
+ Factory.class_to_id(self.class),
80
92
  @identifiers,
81
93
  @scope
82
94
  ))
83
95
 
84
96
  # get token_data from cache (or nil), token_data is what is returned by /token
85
- token_data = Factory.instance.persist_mgr.get(token_id) if use_cache
97
+ token_data = Factory.instance.persist_mgr.get(token_id) if cache
86
98
  token_data = JSON.parse(token_data) unless token_data.nil?
87
99
  # Optional optimization: check if node token is expired based on decoded content then force refresh if close enough
88
100
  # might help in case the transfer agent cannot refresh himself
89
101
  # `direct` agent is equipped with refresh code
90
- if !use_refresh_token && !token_data.nil?
102
+ if !refresh && !token_data.nil?
91
103
  decoded_token = OAuth::Factory.instance.decode_token(token_data[@token_field])
92
104
  Log.log.debug{Log.dump('decoded_token', decoded_token)} unless decoded_token.nil?
93
105
  if decoded_token.is_a?(Hash)
@@ -96,13 +108,13 @@ module Aspera
96
108
  elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
97
109
  end
98
110
  # force refresh if we see a token too close from expiration
99
- use_refresh_token = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < OAuth::Factory.instance.globals[:token_expiration_guard_sec]
100
- Log.log.debug{"Expiration: #{expires_at_sec} / #{use_refresh_token}"}
111
+ refresh = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < OAuth::Factory.instance.parameters[:token_expiration_guard_sec]
112
+ Log.log.debug{"Expiration: #{expires_at_sec} / #{refresh}"}
101
113
  end
102
114
  end
103
115
 
104
116
  # an API was already called, but failed, we need to regenerate or refresh
105
- if use_refresh_token
117
+ if refresh
106
118
  if token_data.is_a?(Hash) && token_data.key?('refresh_token') && !token_data['refresh_token'].eql?('not_supported')
107
119
  # save possible refresh token, before deleting the cache
108
120
  refresh_token = token_data['refresh_token']
@@ -133,11 +145,11 @@ module Aspera
133
145
  json_data = resp[:http].body
134
146
  token_data = JSON.parse(json_data)
135
147
  Factory.instance.persist_mgr.put(token_id, json_data)
136
- end # if ! in_cache
148
+ end
137
149
  Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"}
138
150
  # ok we shall have a token here
139
151
  return OAuth::Factory.bearer_build(token_data[@token_field])
140
152
  end
141
- end # OAuth
153
+ end
142
154
  end
143
155
  end
@@ -5,6 +5,7 @@ require 'aspera/assert'
5
5
  require 'base64'
6
6
  module Aspera
7
7
  module OAuth
8
+ # Factory to create tokens and manage their cache
8
9
  class Factory
9
10
  include Singleton
10
11
  # a prefix for persistency of tokens (simplify garbage collect)
@@ -16,7 +17,11 @@ module Aspera
16
17
 
17
18
  class << self
18
19
  def bearer_build(token)
19
- return BEARER_PREFIX + token
20
+ return "#{BEARER_PREFIX}#{token}"
21
+ end
22
+
23
+ def bearer?(token)
24
+ return token.start_with?(BEARER_PREFIX)
20
25
  end
21
26
 
22
27
  def bearer_extract(token)
@@ -24,14 +29,11 @@ module Aspera
24
29
  return token[BEARER_PREFIX.length..-1]
25
30
  end
26
31
 
27
- def bearer?(token)
28
- return token.start_with?(BEARER_PREFIX)
29
- end
30
-
31
32
  def id(*params)
32
33
  return [PERSIST_CATEGORY_TOKEN, *params].flatten
33
34
  end
34
35
 
36
+ # snake version of class name is the identifier
35
37
  def class_to_id(creator_class)
36
38
  return creator_class.name.split('::').last.capital_to_snake.to_sym
37
39
  end
@@ -45,11 +47,8 @@ module Aspera
45
47
  # token creation methods
46
48
  @token_type_classes = {}
47
49
  @decoders = []
48
- @globals = {
49
- # remove 5 minutes to account for time offset between client and server (TODO: configurable?)
50
- jwt_accepted_offset_sec: 300,
51
- # one hour validity (TODO: configurable?)
52
- jwt_expiry_offset_sec: 3600,
50
+ # default parameters, others can be added by handlers
51
+ @parameters = {
53
52
  # tokens older than 30 minutes will be discarded from cache
54
53
  token_cache_expiry_sec: 1800,
55
54
  # tokens valid for less than this duration will be regenerated
@@ -59,12 +58,12 @@ module Aspera
59
58
 
60
59
  public
61
60
 
62
- attr_reader :globals
61
+ attr_reader :parameters
63
62
 
64
63
  def persist_mgr=(manager)
65
64
  @persist = manager
66
65
  # cleanup expired tokens
67
- @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @globals[:token_cache_expiry_sec])
66
+ @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @parameters[:token_cache_expiry_sec])
68
67
  end
69
68
 
70
69
  def persist_mgr
@@ -104,7 +103,7 @@ module Aspera
104
103
  # @param id_create called to generate unique id for token, for cache
105
104
  def register_token_creator(creator_class)
106
105
  Aspera.assert_type(creator_class, Class)
107
- id = self.class.class_to_id(creator_class)
106
+ id = Factory.class_to_id(creator_class)
108
107
  Log.log.debug{"registering token creator #{id}"}
109
108
  @token_type_classes[id] = creator_class
110
109
  end
@@ -4,6 +4,7 @@ require 'aspera/oauth/base'
4
4
 
5
5
  module Aspera
6
6
  module OAuth
7
+ # Generic token creator
7
8
  class Generic < Base
8
9
  def initialize(
9
10
  grant_type:,
@@ -5,41 +5,44 @@ require 'aspera/assert'
5
5
  require 'securerandom'
6
6
  module Aspera
7
7
  module OAuth
8
+ # remove 5 minutes to account for time offset between client and server (TODO: configurable?)
9
+ Factory.instance.parameters[:jwt_accepted_offset_sec] = 300
10
+ # one hour validity (TODO: configurable?)
11
+ Factory.instance.parameters[:jwt_expiry_offset_sec] = 3600
8
12
  # Authentication using private key
13
+ # https://tools.ietf.org/html/rfc7523
14
+ # https://tools.ietf.org/html/rfc7519
9
15
  class Jwt < Base
10
- # @param g_o:private_key_obj [M] for type :jwt
11
- # @param g_o:payload [M] for type :jwt
12
- # @param g_o:headers [0] for type :jwt
16
+ # @param private_key_obj private key object
17
+ # @param payload payload to be included in the JWT
18
+ # @param headers headers to be included in the JWT
13
19
  def initialize(
14
- payload:,
15
20
  private_key_obj:,
21
+ payload:,
16
22
  headers: {},
17
23
  **base_params
18
24
  )
19
- Aspera.assert_type(payload, Hash){'payload'}
20
25
  Aspera.assert_type(private_key_obj, OpenSSL::PKey::RSA){'private_key_obj'}
26
+ Aspera.assert_type(payload, Hash){'payload'}
21
27
  Aspera.assert_type(headers, Hash){'headers'}
22
28
  super(**base_params)
23
29
  @private_key_obj = private_key_obj
24
- @payload = payload
30
+ @additional_payload = payload
25
31
  @headers = headers
26
- @identifiers.push(@payload[:sub])
32
+ @identifiers.push(@additional_payload[:sub])
27
33
  end
28
34
 
29
35
  def create_token
30
- # https://tools.ietf.org/html/rfc7523
31
- # https://tools.ietf.org/html/rfc7519
32
36
  require 'jwt'
33
37
  seconds_since_epoch = Time.new.to_i
34
38
  Log.log.info{"seconds=#{seconds_since_epoch}"}
35
- Aspera.assert(@payload.is_a?(Hash)){'missing JWT payload'}
36
39
  jwt_payload = {
37
- exp: seconds_since_epoch + OAuth::Factory.instance.globals[:jwt_expiry_offset_sec], # expiration time
38
- nbf: seconds_since_epoch - OAuth::Factory.instance.globals[:jwt_accepted_offset_sec], # not before
39
- iat: seconds_since_epoch - OAuth::Factory.instance.globals[:jwt_accepted_offset_sec] + 1, # issued at
40
+ exp: seconds_since_epoch + OAuth::Factory.instance.parameters[:jwt_expiry_offset_sec], # expiration time
41
+ nbf: seconds_since_epoch - OAuth::Factory.instance.parameters[:jwt_accepted_offset_sec], # not before
42
+ iat: seconds_since_epoch - OAuth::Factory.instance.parameters[:jwt_accepted_offset_sec] + 1, # issued at
40
43
  jti: SecureRandom.uuid # JWT id
41
- }.merge(@payload)
42
- Log.log.debug{"JWT jwt_payload=[#{jwt_payload}]"}
44
+ }.merge(@additional_payload)
45
+ Log.log.debug{Log.dump(:jwt_payload, jwt_payload)}
43
46
  Log.log.debug{"private=[#{@private_key_obj}]"}
44
47
  assertion = JWT.encode(jwt_payload, @private_key_obj, 'RS256', @headers)
45
48
  Log.log.debug{"assertion=[#{assertion}]"}
@@ -4,16 +4,17 @@ require 'aspera/oauth/base'
4
4
 
5
5
  module Aspera
6
6
  module OAuth
7
+ # This class is used to create a token using a JSON body and a URL
7
8
  class UrlJson < Base
8
9
  def initialize(
9
- json:,
10
10
  url:,
11
+ json:,
11
12
  **generic_params
12
13
  )
13
14
  super(**generic_params)
14
- @json_params = json
15
- @url_params = url
16
- @identifiers.push(@json_params[:url_token])
15
+ @body = json
16
+ @query = url
17
+ @identifiers.push(@body[:url_token])
17
18
  end
18
19
 
19
20
  def create_token
@@ -21,8 +22,9 @@ module Aspera
21
22
  operation: 'POST',
22
23
  subpath: @path_token,
23
24
  headers: {'Accept' => 'application/json'},
24
- json_params: @json_params,
25
- url_params: @url_params.merge(scope: @scope) # scope is here because it may change over time (node)
25
+ query: @query.merge(scope: @scope), # scope is here because it may change over time (node)
26
+ body: @body,
27
+ body_type: :json
26
28
  )
27
29
  end
28
30
  end