aspera-cli 4.5.0 → 4.8.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 (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +1 -0
  3. data/README.md +1894 -1574
  4. data/bin/ascli +21 -1
  5. data/bin/asession +38 -34
  6. data/docs/test_env.conf +14 -3
  7. data/examples/aoc.rb +17 -15
  8. data/examples/dascli +26 -0
  9. data/examples/faspex4.rb +42 -35
  10. data/examples/proxy.pac +1 -1
  11. data/examples/transfer.rb +38 -37
  12. data/lib/aspera/aoc.rb +245 -205
  13. data/lib/aspera/ascmd.rb +111 -90
  14. data/lib/aspera/ats_api.rb +16 -14
  15. data/lib/aspera/cli/basic_auth_plugin.rb +19 -18
  16. data/lib/aspera/cli/extended_value.rb +50 -39
  17. data/lib/aspera/cli/formater.rb +161 -135
  18. data/lib/aspera/cli/info.rb +18 -0
  19. data/lib/aspera/cli/listener/line_dump.rb +4 -2
  20. data/lib/aspera/cli/listener/logger.rb +3 -1
  21. data/lib/aspera/cli/listener/progress.rb +20 -21
  22. data/lib/aspera/cli/listener/progress_multi.rb +29 -31
  23. data/lib/aspera/cli/main.rb +194 -183
  24. data/lib/aspera/cli/manager.rb +213 -206
  25. data/lib/aspera/cli/plugin.rb +71 -49
  26. data/lib/aspera/cli/plugins/alee.rb +8 -7
  27. data/lib/aspera/cli/plugins/aoc.rb +675 -558
  28. data/lib/aspera/cli/plugins/ats.rb +116 -109
  29. data/lib/aspera/cli/plugins/bss.rb +35 -34
  30. data/lib/aspera/cli/plugins/config.rb +722 -542
  31. data/lib/aspera/cli/plugins/console.rb +28 -22
  32. data/lib/aspera/cli/plugins/cos.rb +28 -37
  33. data/lib/aspera/cli/plugins/faspex.rb +281 -227
  34. data/lib/aspera/cli/plugins/faspex5.rb +129 -84
  35. data/lib/aspera/cli/plugins/node.rb +426 -232
  36. data/lib/aspera/cli/plugins/orchestrator.rb +106 -98
  37. data/lib/aspera/cli/plugins/preview.rb +196 -191
  38. data/lib/aspera/cli/plugins/server.rb +131 -126
  39. data/lib/aspera/cli/plugins/shares.rb +49 -36
  40. data/lib/aspera/cli/plugins/sync.rb +27 -28
  41. data/lib/aspera/cli/transfer_agent.rb +84 -79
  42. data/lib/aspera/cli/version.rb +3 -1
  43. data/lib/aspera/colors.rb +37 -28
  44. data/lib/aspera/command_line_builder.rb +84 -63
  45. data/lib/aspera/cos_node.rb +68 -34
  46. data/lib/aspera/data_repository.rb +4 -2
  47. data/lib/aspera/environment.rb +61 -46
  48. data/lib/aspera/fasp/agent_base.rb +36 -31
  49. data/lib/aspera/fasp/agent_connect.rb +44 -37
  50. data/lib/aspera/fasp/agent_direct.rb +101 -104
  51. data/lib/aspera/fasp/agent_httpgw.rb +91 -90
  52. data/lib/aspera/fasp/agent_node.rb +36 -33
  53. data/lib/aspera/fasp/agent_trsdk.rb +28 -31
  54. data/lib/aspera/fasp/error.rb +3 -1
  55. data/lib/aspera/fasp/error_info.rb +81 -54
  56. data/lib/aspera/fasp/installation.rb +171 -151
  57. data/lib/aspera/fasp/listener.rb +2 -0
  58. data/lib/aspera/fasp/parameters.rb +105 -111
  59. data/lib/aspera/fasp/parameters.yaml +305 -249
  60. data/lib/aspera/fasp/resume_policy.rb +20 -20
  61. data/lib/aspera/fasp/transfer_spec.rb +27 -0
  62. data/lib/aspera/fasp/uri.rb +31 -29
  63. data/lib/aspera/faspex_gw.rb +95 -118
  64. data/lib/aspera/hash_ext.rb +12 -13
  65. data/lib/aspera/id_generator.rb +11 -9
  66. data/lib/aspera/keychain/encrypted_hash.rb +73 -57
  67. data/lib/aspera/keychain/macos_security.rb +27 -29
  68. data/lib/aspera/log.rb +40 -39
  69. data/lib/aspera/nagios.rb +24 -22
  70. data/lib/aspera/node.rb +38 -30
  71. data/lib/aspera/oauth.rb +217 -248
  72. data/lib/aspera/open_application.rb +9 -7
  73. data/lib/aspera/persistency_action_once.rb +15 -14
  74. data/lib/aspera/persistency_folder.rb +15 -18
  75. data/lib/aspera/preview/file_types.rb +266 -270
  76. data/lib/aspera/preview/generator.rb +94 -92
  77. data/lib/aspera/preview/image_error.png +0 -0
  78. data/lib/aspera/preview/options.rb +20 -17
  79. data/lib/aspera/preview/utils.rb +99 -102
  80. data/lib/aspera/preview/video_error.png +0 -0
  81. data/lib/aspera/{proxy_auto_config.erb.js → proxy_auto_config.js} +23 -31
  82. data/lib/aspera/proxy_auto_config.rb +114 -21
  83. data/lib/aspera/rest.rb +144 -142
  84. data/lib/aspera/rest_call_error.rb +3 -2
  85. data/lib/aspera/rest_error_analyzer.rb +31 -31
  86. data/lib/aspera/rest_errors_aspera.rb +18 -16
  87. data/lib/aspera/secret_hider.rb +68 -0
  88. data/lib/aspera/ssh.rb +20 -16
  89. data/lib/aspera/sync.rb +57 -54
  90. data/lib/aspera/temp_file_manager.rb +20 -14
  91. data/lib/aspera/timer_limiter.rb +10 -8
  92. data/lib/aspera/uri_reader.rb +14 -15
  93. data/lib/aspera/web_auth.rb +85 -80
  94. data.tar.gz.sig +0 -0
  95. metadata +169 -40
  96. metadata.gz.sig +2 -0
  97. data/bin/dascli +0 -13
  98. data/docs/Makefile +0 -63
  99. data/docs/README.erb.md +0 -4221
  100. data/docs/README.md +0 -13
  101. data/docs/diagrams.txt +0 -49
  102. data/docs/doc_tools.rb +0 -58
  103. data/lib/aspera/cli/plugins/shares2.rb +0 -114
  104. data/lib/aspera/fasp/default.rb +0 -17
data/lib/aspera/oauth.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'aspera/open_application'
2
4
  require 'aspera/web_auth'
3
5
  require 'aspera/id_generator'
@@ -11,203 +13,278 @@ module Aspera
11
13
  # call get_authorization() to get a token.
12
14
  # bearer tokens are kept in memory and also in a file cache for later re-use
13
15
  # if a token is expired (api returns 4xx), call again get_authorization({refresh: true})
16
+ # https://tools.ietf.org/html/rfc6749
14
17
  class Oauth
15
- # used for code exchange
16
- DEFAULT_PATH_AUTHORIZE='authorize'
17
- # to generate token
18
- DEFAULT_PATH_TOKEN='token'
19
- # field with token in result
20
- DEFAULT_TOKEN_FIELD='access_token'
21
- private
18
+ DEFAULT_CREATE_PARAMS = {
19
+ path_token: 'token', # default endpoint for /token to generate token
20
+ token_field: 'access_token', # field with token in result of call to path_token
21
+ web: {path_authorize: 'authorize'} # default endpoint for /authorize, used for code exchange
22
+ }.freeze
23
+
24
+ # OAuth methods supported by default
25
+ STD_AUTH_TYPES = %i[web jwt].freeze
26
+
22
27
  # remove 5 minutes to account for time offset (TODO: configurable?)
23
- JWT_NOTBEFORE_OFFSET_SEC=300
28
+ JWT_NOTBEFORE_OFFSET_SEC = 300
24
29
  # one hour validity (TODO: configurable?)
25
- JWT_EXPIRY_OFFSET_SEC=3600
30
+ JWT_EXPIRY_OFFSET_SEC = 3600
26
31
  # tokens older than 30 minutes will be discarded from cache
27
- TOKEN_CACHE_EXPIRY_SEC=1800
28
- # a prefix for persistency of tokens (garbage collect)
29
- PERSIST_CATEGORY_TOKEN='token'
30
- ONE_HOUR_AS_DAY_FRACTION=Rational(1,24)
32
+ TOKEN_CACHE_EXPIRY_SEC = 1800
33
+ # tokens valid for less than this duration will be regenerated
34
+ TOKEN_EXPIRATION_GUARD_SEC = 120
35
+ # a prefix for persistency of tokens (simplify garbage collect)
36
+ PERSIST_CATEGORY_TOKEN = 'token'
31
37
 
32
- private_constant :JWT_NOTBEFORE_OFFSET_SEC,:JWT_EXPIRY_OFFSET_SEC,:PERSIST_CATEGORY_TOKEN,:TOKEN_CACHE_EXPIRY_SEC,:ONE_HOUR_AS_DAY_FRACTION
33
- class << self
34
- # OAuth methods supported
35
- def auth_types
36
- [ :body_userpass, :header_userpass, :web, :jwt, :url_token, :ibm_apikey ]
37
- end
38
+ private_constant :JWT_NOTBEFORE_OFFSET_SEC,:JWT_EXPIRY_OFFSET_SEC,:TOKEN_CACHE_EXPIRY_SEC,:PERSIST_CATEGORY_TOKEN,:TOKEN_EXPIRATION_GUARD_SEC
38
39
 
40
+ # persistency manager
41
+ @persist = nil
42
+ # token creation methods
43
+ @create_handlers = {}
44
+ # token unique identifiers from oauth parameters
45
+ @id_handlers = {}
46
+
47
+ class << self
39
48
  def persist_mgr=(manager)
40
- @persist=manager
49
+ @persist = manager
50
+ # cleanup expired tokens
51
+ @persist.garbage_collect(PERSIST_CATEGORY_TOKEN,TOKEN_CACHE_EXPIRY_SEC)
41
52
  end
42
53
 
43
54
  def persist_mgr
44
55
  if @persist.nil?
45
- Log.log.warn('Not using persistency (use Aspera::Oauth.persist_mgr=Aspera::PersistencyFolder.new)')
56
+ Log.log.debug('Not using persistency') # (use Aspera::Oauth.persist_mgr=Aspera::PersistencyFolder.new)
46
57
  # create NULL persistency class
47
- @persist=Class.new do
48
- def get(x);nil;end;def delete(x);nil;end;def put(x,y);nil;end;def garbage_collect(x,y);nil;end
58
+ @persist = Class.new do
59
+ def get(_x);nil;end;def delete(_x);nil;end;def put(_x,_y);nil;end;def garbage_collect(_x,_y);nil;end
49
60
  end.new
50
61
  end
51
62
  return @persist
52
63
  end
53
64
 
65
+ # delete all existing tokens
54
66
  def flush_tokens
55
67
  persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,nil)
56
68
  end
57
69
 
70
+ # register a bearer token decoder, mainly to inspect expiry date
58
71
  def register_decoder(method)
59
- @decoders||=[]
72
+ @decoders ||= []
60
73
  @decoders.push(method)
61
74
  end
62
75
 
76
+ # decode token using all registered decoders
63
77
  def decode_token(token)
64
- Log.log.debug(">>>> #{token} : #{@decoders.length}")
65
78
  @decoders.each do |decoder|
66
- result=decoder.call(token) rescue nil
79
+ result = decoder.call(token) rescue nil
67
80
  return result unless result.nil?
68
81
  end
69
82
  return nil
70
83
  end
71
- end
72
84
 
73
- # seems to be quite standard token encoding (RFC?)
74
- self.register_decoder lambda { |token| parts=token.split('.'); raise "not aoc token" unless parts.length.eql?(3); JSON.parse(Base64.decode64(parts[1]))}
85
+ # register a token creation method
86
+ # @param id creation type from field :crtype in constructor
87
+ # @param lambda_create called to create token
88
+ # @param id_create called to generate unique id for token, for cache
89
+ def register_token_creator(id, lambda_create, id_create)
90
+ raise 'ERROR: requites Symbol and 2 lambdas' unless id.is_a?(Symbol) && lambda_create.is_a?(Proc) && id_create.is_a?(Proc)
91
+ @create_handlers[id] = lambda_create
92
+ @id_handlers[id] = id_create
93
+ end
75
94
 
76
- # for supported parameters, look in the code for @params
77
- # parameters are provided all with oauth_ prefix :
78
- # :base_url
79
- # :client_id
80
- # :client_secret
81
- # :redirect_uri
82
- # :jwt_audience
83
- # :jwt_private_key_obj
84
- # :jwt_subject
85
- # :path_authorize (default: DEFAULT_PATH_AUTHORIZE)
86
- # :path_token (default: DEFAULT_PATH_TOKEN)
87
- # :scope (optional)
88
- # :grant (one of returned by self.auth_types)
89
- # :url_token
90
- # :user_name
91
- # :user_pass
92
- # :token_type
93
- def initialize(auth_params)
94
- Log.log.debug("auth=#{auth_params}")
95
- @params=auth_params.clone
96
- # default values
97
- # name of field to take as token from result of call to /token
98
- @params[:token_field]||=DEFAULT_TOKEN_FIELD
99
- # default endpoint for /token
100
- @params[:path_token]||=DEFAULT_PATH_TOKEN
101
- # default endpoint for /authorize
102
- @params[:path_authorize]||=DEFAULT_PATH_AUTHORIZE
103
- rest_params={base_url: @params[:base_url]}
104
- if @params.has_key?(:client_id)
105
- rest_params.merge!({auth: {
106
- type: :basic,
107
- username: @params[:client_id],
108
- password: @params[:client_secret]
109
- }})
95
+ # @return one of the registered creators for the given create type
96
+ def token_creator(id)
97
+ raise "token creator type unknown: #{id}/#{id.class}" unless @create_handlers.has_key?(id)
98
+ @create_handlers[id]
110
99
  end
111
- @token_auth_api=Rest.new(rest_params)
112
- if @params.has_key?(:redirect_uri)
113
- uri=URI.parse(@params[:redirect_uri])
114
- raise "redirect_uri scheme must be http" unless uri.scheme.start_with?('http')
115
- raise "redirect_uri must have a port" if uri.port.nil?
116
- # we could check that host is localhost or local address
100
+
101
+ # list of identifiers foundn in creation parameters that can be used to uniquely identify the token
102
+ def id_creator(id)
103
+ raise "id creator type unknown: #{id}/#{id.class}" unless @id_handlers.has_key?(id)
104
+ @id_handlers[id]
117
105
  end
118
- # cleanup expired tokens
119
- self.class.persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,TOKEN_CACHE_EXPIRY_SEC)
120
- end
106
+ end # self
121
107
 
122
- # open the login page, wait for code and check_code, then return code
123
- def goto_page_and_get_code(login_page_url,check_code)
108
+ # JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
109
+ register_decoder lambda { |token| parts = token.split('.'); raise 'not aoc token' unless parts.length.eql?(3); JSON.parse(Base64.decode64(parts[1]))}
110
+
111
+ # generic token creation, parameters are provided in :generic
112
+ register_token_creator :generic,lambda { |oauth|
113
+ return oauth.create_token(oauth.sparams)
114
+ },lambda { |oauth|
115
+ return [
116
+ oauth.sparams[:grant_type]&.split(':')&.last,
117
+ oauth.sparams[:apikey],
118
+ oauth.sparams[:response_type]
119
+ ]
120
+ }
121
+
122
+ # Authentication using Web browser
123
+ register_token_creator :web,lambda { |oauth|
124
+ verification_code = SecureRandom.uuid # used to check later
125
+ login_page_url = Rest.build_uri("#{oauth.api[:base_url]}/#{oauth.sparams[:path_authorize]}",
126
+ oauth.optional_scope_client_id.merge(response_type: 'code', redirect_uri: oauth.sparams[:redirect_uri], state: verification_code))
127
+ # here, we need a human to authorize on a web page
124
128
  Log.log.info("login_page_url=#{login_page_url}".bg_red.gray)
125
129
  # start a web server to receive request code
126
- webserver=WebAuth.new(@params[:redirect_uri])
130
+ webserver = WebAuth.new(oauth.sparams[:redirect_uri])
127
131
  # start browser on login page
128
132
  OpenApplication.instance.uri(login_page_url)
129
133
  # wait for code in request
130
- request_params=webserver.get_request
131
- Log.log.error("state does not match") if !check_code.eql?(request_params['state'])
132
- code=request_params['code']
133
- return code
134
- end
134
+ received_params = webserver.received_request
135
+ raise 'state does not match' unless verification_code.eql?(received_params['state'])
136
+ # exchange code for token
137
+ return oauth.create_token(oauth.optional_scope_client_id(add_secret: true).merge(
138
+ grant_type: 'authorization_code',
139
+ code: received_params['code'],
140
+ redirect_uri: oauth.sparams[:redirect_uri]))
141
+ },lambda { |_oauth|
142
+ return []
143
+ }
135
144
 
136
- def create_token(rest_params)
137
- return @token_auth_api.call({
138
- operation: 'POST',
139
- subpath: @params[:path_token],
140
- headers: {'Accept'=>'application/json'}}.merge(rest_params))
141
- end
145
+ # Authentication using private key
146
+ register_token_creator :jwt,lambda { |oauth|
147
+ # https://tools.ietf.org/html/rfc7523
148
+ # https://tools.ietf.org/html/rfc7519
149
+ require 'jwt'
150
+ seconds_since_epoch = Time.new.to_i
151
+ Log.log.info("seconds=#{seconds_since_epoch}")
152
+ raise 'missing JWT payload' unless oauth.sparams[:payload].is_a?(Hash)
153
+ jwt_payload = {
154
+ exp: seconds_since_epoch + JWT_EXPIRY_OFFSET_SEC, # expiration time
155
+ nbf: seconds_since_epoch - JWT_NOTBEFORE_OFFSET_SEC, # not before
156
+ iat: seconds_since_epoch, # issued at
157
+ jti: SecureRandom.uuid # JWT id
158
+ }.merge(oauth.sparams[:payload])
159
+ Log.log.debug("JWT jwt_payload=[#{jwt_payload}]")
160
+ rsa_private = oauth.sparams[:private_key_obj] # type: OpenSSL::PKey::RSA
161
+ Log.log.debug("private=[#{rsa_private}]")
162
+ assertion = JWT.encode(jwt_payload, rsa_private, 'RS256', oauth.sparams[:headers] || {})
163
+ Log.log.debug("assertion=[#{assertion}]")
164
+ return oauth.create_token(oauth.optional_scope_client_id.merge(grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: assertion))
165
+ },lambda { |oauth|
166
+ return [oauth.sparams.dig(:payload,:sub)]
167
+ }
168
+
169
+ attr_reader :gparams, :sparams, :api
170
+
171
+ private
142
172
 
143
- # @return unique identifier of token
144
- def token_cache_id(api_scope)
145
- oauth_uri=URI.parse(@params[:base_url])
146
- parts=[PERSIST_CATEGORY_TOKEN,api_scope,oauth_uri.host,oauth_uri.path]
147
- # add some of the parameters that uniquely define the token
148
- [:grant,:jwt_subject,:user_name,:url_token,:api_key].inject(parts){|p,i|p.push(@params[i])}
149
- return IdGenerator.from_list(parts)
173
+ # [M]=mandatory [D]=has default value [0]=accept nil
174
+ # :base_url [M] URL of authentication API
175
+ # :auth
176
+ # :crtype [M] :generic, :web, :jwt, custom
177
+ # :client_id [0]
178
+ # :client_secret [0]
179
+ # :scope [0]
180
+ # :path_token [D] API end point to create a token
181
+ # :token_field [D] field in result that contains the token
182
+ # :jwt:private_key_obj [M] for type :jwt
183
+ # :jwt:payload [M] for type :jwt
184
+ # :jwt:headers [0] for type :jwt
185
+ # :web:redirect_uri [M] for type :web
186
+ # :web:path_authorize [D] for type :web
187
+ # :generic [M] for type :generic
188
+ def initialize(a_params)
189
+ Log.log.debug("auth=#{a_params}")
190
+ # replace default values
191
+ @gparams = DEFAULT_CREATE_PARAMS.deep_merge(a_params)
192
+ # check that type is known
193
+ self.class.token_creator(@gparams[:crtype])
194
+ # specific parameters for the creation type
195
+ @sparams=@gparams[@gparams[:crtype]]
196
+ if @gparams[:crtype].eql?(:web) && @sparams.has_key?(:redirect_uri)
197
+ uri = URI.parse(@sparams[:redirect_uri])
198
+ raise 'redirect_uri scheme must be http or https' unless %w[http https].include?(uri.scheme)
199
+ raise 'redirect_uri must have a port' if uri.port.nil?
200
+ # TODO: we could check that host is localhost or local address
201
+ end
202
+ rest_params = {
203
+ base_url: @gparams[:base_url],
204
+ redirect_max: 2
205
+ }
206
+ rest_params[:auth] = a_params[:auth] if a_params.has_key?(:auth)
207
+ @api = Rest.new(rest_params)
208
+ # if needed use from api
209
+ @gparams.delete(:base_url)
210
+ @gparams.delete(:auth)
211
+ @gparams.delete(@gparams[:crtype])
212
+ Log.dump(:gparams,@gparams)
213
+ Log.dump(:sparams,@sparams)
150
214
  end
151
215
 
152
216
  public
153
217
 
154
- # used to change parameter, such as scope
155
- attr_reader :params
218
+ # helper method to create token as per RFC
219
+ def create_token(www_params)
220
+ return @api.call({
221
+ operation: 'POST',
222
+ subpath: @gparams[:path_token],
223
+ headers: {'Accept' => 'application/json'},
224
+ www_body_params: www_params})
225
+ end
156
226
 
157
- # @param options : :scope and :refresh
158
- def get_authorization(options={})
159
- # api scope can be overriden to get auth for other scope
160
- api_scope=options[:scope] || @params[:scope]
161
- # as it is optional in many place: create struct
162
- p_scope={}
163
- p_scope[:scope] = api_scope unless api_scope.nil?
164
- p_client_id_and_scope=p_scope.clone
165
- p_client_id_and_scope[:client_id] = @params[:client_id] if @params.has_key?(:client_id)
166
- use_refresh_token=options[:refresh]
227
+ # @return Hash with optional general parameters
228
+ def optional_scope_client_id(add_secret: false)
229
+ call_params={}
230
+ call_params[:scope] = @gparams[:scope] unless @gparams[:scope].nil?
231
+ call_params[:client_id] = @gparams[:client_id] unless @gparams[:client_id].nil?
232
+ call_params[:client_secret] = @gparams[:client_secret] if add_secret && !@gparams[:client_id].nil?
233
+ return call_params
234
+ end
167
235
 
168
- # generate token identifier to use with cache
169
- token_id=token_cache_id(api_scope)
236
+ # Oauth v2 token generation
237
+ # @param use_refresh_token set to true to force refresh or re-generation (if previous failed)
238
+ def get_authorization(use_refresh_token: false, use_cache: true)
239
+ # generate token unique identifier for persistency (memory/disk cache)
240
+ token_id = IdGenerator.from_list([
241
+ PERSIST_CATEGORY_TOKEN,
242
+ @api.params[:base_url],
243
+ @gparams[:crtype],
244
+ self.class.id_creator(@gparams[:crtype]).call(self), # array, so we flatten later
245
+ @gparams[:scope],
246
+ @api.params.dig(%i[auth username])
247
+ ].flatten)
170
248
 
171
249
  # get token_data from cache (or nil), token_data is what is returned by /token
172
- token_data=self.class.persist_mgr.get(token_id)
173
- token_data=JSON.parse(token_data) unless token_data.nil?
174
- # Optional optimization: check if node token is expired, then force refresh
175
- # in case the transfer agent cannot refresh himself
176
- # else, anyway, faspmanager is equipped with refresh code
177
- if !token_data.nil?
178
- # TODO: use @params[:token_field] ?
179
- decoded_node_token = self.class.decode_token(token_data['access_token'])
180
- Log.dump('decoded_node_token',decoded_node_token) unless decoded_node_token.nil?
181
- if decoded_node_token.is_a?(Hash) and decoded_node_token['expires_at'].is_a?(String)
182
- expires_at=DateTime.parse(decoded_node_token['expires_at'])
183
- # Time.at(decoded_node_token['exp'])
184
- # does it seem expired, with one hour of security
185
- use_refresh_token=true if DateTime.now > (expires_at-ONE_HOUR_AS_DAY_FRACTION)
250
+ token_data = self.class.persist_mgr.get(token_id) if use_cache
251
+ token_data = JSON.parse(token_data) unless token_data.nil?
252
+ # Optional optimization: check if node token is expired basd on decoded content then force refresh if close enough
253
+ # might help in case the transfer agent cannot refresh himself
254
+ # `direct` agent is equipped with refresh code
255
+ if !use_refresh_token && !token_data.nil?
256
+ decoded_token = self.class.decode_token(token_data[@gparams[:token_field]])
257
+ Log.dump('decoded_token',decoded_token) unless decoded_token.nil?
258
+ if decoded_token.is_a?(Hash)
259
+ expires_at_sec =
260
+ if decoded_token['expires_at'].is_a?(String) then DateTime.parse(decoded_token['expires_at']).to_time
261
+ elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
262
+ end
263
+ # force refresh if we see a token too close from expiration
264
+ use_refresh_token = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < TOKEN_EXPIRATION_GUARD_SEC
265
+ Log.log.debug("Expiration: #{expires_at_sec} / #{use_refresh_token}")
186
266
  end
187
267
  end
188
268
 
189
269
  # an API was already called, but failed, we need to regenerate or refresh
190
270
  if use_refresh_token
191
- if token_data.is_a?(Hash) and token_data.has_key?('refresh_token')
271
+ if token_data.is_a?(Hash) && token_data.has_key?('refresh_token')
192
272
  # save possible refresh token, before deleting the cache
193
- refresh_token=token_data['refresh_token']
273
+ refresh_token = token_data['refresh_token']
194
274
  end
195
- # delete caches
275
+ # delete cache
196
276
  self.class.persist_mgr.delete(token_id)
197
- token_data=nil
277
+ token_data = nil
198
278
  # lets try the existing refresh token
199
279
  if !refresh_token.nil?
200
280
  Log.log.info("refresh=[#{refresh_token}]".bg_green)
201
281
  # try to refresh
202
- # note: admin token has no refresh, and lives by default 1800secs
203
- # Note: scope is mandatory in Files, and we can either provide basic auth, or client_Secret in data
204
- resp=create_token(www_body_params: p_client_id_and_scope.merge({
205
- grant_type: 'refresh_token',
206
- refresh_token: refresh_token}))
207
- if resp[:http].code.start_with?('2') then
282
+ # note: AoC admin token has no refresh, and lives by default 1800secs
283
+ resp = create_token(optional_scope_client_id.merge(grant_type: 'refresh_token',refresh_token: refresh_token))
284
+ if resp[:http].code.start_with?('2')
208
285
  # save only if success
209
- json_data=resp[:http].body
210
- token_data=JSON.parse(json_data)
286
+ json_data = resp[:http].body
287
+ token_data = JSON.parse(json_data)
211
288
  self.class.persist_mgr.put(token_id,json_data)
212
289
  else
213
290
  Log.log.debug("refresh failed: #{resp[:http].body}".bg_red)
@@ -215,124 +292,16 @@ module Aspera
215
292
  end
216
293
  end
217
294
 
218
- # no cache
219
- if token_data.nil? then
220
- resp=nil
221
- case @params[:grant]
222
- when :web
223
- # AoC Web based Auth
224
- check_code=SecureRandom.uuid
225
- auth_params=p_client_id_and_scope.merge({
226
- response_type: 'code',
227
- redirect_uri: @params[:redirect_uri],
228
- state: check_code
229
- })
230
- auth_params[:client_secret]=@params[:client_secret] if @params.has_key?(:client_secret)
231
- login_page_url=Rest.build_uri("#{@params[:base_url]}/#{@params[:path_authorize]}",auth_params)
232
- # here, we need a human to authorize on a web page
233
- code=goto_page_and_get_code(login_page_url,check_code)
234
- # exchange code for token
235
- resp=create_token(www_body_params: p_client_id_and_scope.merge({
236
- grant_type: 'authorization_code',
237
- code: code,
238
- redirect_uri: @params[:redirect_uri]
239
- }))
240
- when :jwt
241
- # https://tools.ietf.org/html/rfc7519
242
- # https://tools.ietf.org/html/rfc7523
243
- require 'jwt'
244
- seconds_since_epoch=Time.new.to_i
245
- Log.log.info("seconds=#{seconds_since_epoch}")
246
-
247
- payload = {
248
- iss: @params[:client_id], # issuer
249
- sub: @params[:jwt_subject], # subject
250
- aud: @params[:jwt_audience], # audience
251
- nbf: seconds_since_epoch-JWT_NOTBEFORE_OFFSET_SEC, # not before
252
- exp: seconds_since_epoch+JWT_EXPIRY_OFFSET_SEC # expiration
253
- }
254
- # Hum.. compliant ? TODO: remove when Faspex5 API is clarified
255
- if @params.has_key?(:f5_username)
256
- payload[:jti] = SecureRandom.uuid # JWT id
257
- payload[:iat] = seconds_since_epoch # issued at
258
- payload.delete(:nbf) # not used in f5
259
- p_scope[:redirect_uri]="https://127.0.0.1:5000/token" # used ?
260
- p_scope[:state]=SecureRandom.uuid
261
- p_scope[:client_id]=@params[:client_id]
262
- @token_auth_api.params[:auth]={type: :basic,username: @params[:f5_username], password: @params[:f5_password]}
263
- end
264
-
265
- # non standard, only for global ids
266
- payload.merge!(@params[:jwt_add]) if @params.has_key?(:jwt_add)
267
- Log.log.debug("JWT payload=[#{payload}]")
268
-
269
- rsa_private=@params[:jwt_private_key_obj] # type: OpenSSL::PKey::RSA
270
- Log.log.debug("private=[#{rsa_private}]")
271
-
272
- assertion = JWT.encode(payload, rsa_private, 'RS256', @params[:jwt_headers]||{})
273
- Log.log.debug("assertion=[#{assertion}]")
274
-
275
- resp=create_token(www_body_params: p_scope.merge({
276
- grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
277
- assertion: assertion
278
- }))
279
- when :url_token
280
- # AoC Public Link
281
- params={url_token: @params[:url_token]}
282
- params[:password]=@params[:password] if @params.has_key?(:password)
283
- resp=create_token({
284
- json_params: params,
285
- url_params: p_scope.merge({grant_type: 'url_token'})
286
- })
287
- when :ibm_apikey
288
- # ATS
289
- resp=create_token(www_body_params: {
290
- grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
291
- response_type: 'cloud_iam',
292
- apikey: @params[:api_key]
293
- })
294
- when :delegated_refresh
295
- # COS
296
- resp=create_token(www_body_params: {
297
- grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
298
- response_type: 'delegated_refresh_token',
299
- apikey: @params[:api_key],
300
- receiver_client_ids: 'aspera_ats'
301
- })
302
- when :header_userpass
303
- # used in Faspex apiv4 and shares2
304
- resp=create_token(
305
- json_params: p_client_id_and_scope.merge({grant_type: 'password'}), #:www_body_params also works
306
- auth: {
307
- type: :basic,
308
- username: @params[:user_name],
309
- password: @params[:user_pass]}
310
- )
311
- when :body_userpass
312
- # legacy, not used
313
- resp=create_token(www_body_params: p_client_id_and_scope.merge({
314
- grant_type: 'password',
315
- username: @params[:user_name],
316
- password: @params[:user_pass]
317
- }))
318
- when :body_data
319
- # used in Faspex apiv5
320
- resp=create_token({
321
- auth: {type: :none},
322
- json_params: @params[:userpass_body],
323
- })
324
- else
325
- raise "auth grant type unknown: #{@params[:grant]}"
326
- end
327
- # TODO: test return code ?
328
- json_data=resp[:http].body
329
- token_data=JSON.parse(json_data)
295
+ # no cache, nor refresh: generate a token
296
+ if token_data.nil?
297
+ resp = self.class.token_creator(@gparams[:crtype]).call(self)
298
+ json_data = resp[:http].body
299
+ token_data = JSON.parse(json_data)
330
300
  self.class.persist_mgr.put(token_id,json_data)
331
301
  end # if ! in_cache
332
- raise "API error: No such field in answer: #{@params[:token_field]}" unless token_data.has_key?(@params[:token_field])
302
+ raise "API error: No such field in answer: #{@gparams[:token_field]}" unless token_data.has_key?(@gparams[:token_field])
333
303
  # ok we shall have a token here
334
- return 'Bearer '+token_data[@params[:token_field]]
304
+ return 'Bearer ' + token_data[@gparams[:token_field]]
335
305
  end
336
-
337
306
  end # OAuth
338
307
  end # Aspera
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'aspera/log'
2
4
  require 'aspera/environment'
3
5
  require 'rbconfig'
@@ -10,12 +12,12 @@ module Aspera
10
12
  class OpenApplication
11
13
  include Singleton
12
14
  # User Interfaces
13
- def self.user_interfaces; [ :text, :graphical ]; end
15
+ def self.user_interfaces; %i[text graphical]; end
14
16
 
15
17
  def self.default_gui_mode
16
18
  return :graphical if [Aspera::Environment::OS_WINDOWS,Aspera::Environment::OS_X].include?(Aspera::Environment.os)
17
19
  # unix family
18
- return :graphical if ENV.has_key?("DISPLAY") and !ENV["DISPLAY"].empty?
20
+ return :graphical if ENV.has_key?('DISPLAY') && !ENV['DISPLAY'].empty?
19
21
  return :text
20
22
  end
21
23
 
@@ -25,9 +27,9 @@ module Aspera
25
27
  when Aspera::Environment::OS_X
26
28
  return system('open',uri.to_s)
27
29
  when Aspera::Environment::OS_WINDOWS
28
- return system('start explorer "'+uri.to_s+'"')
30
+ return system('start explorer "' + uri.to_s + '"')
29
31
  when Aspera::Environment::OS_LINUX
30
- return system("xdg-open '#{uri.to_s}'")
32
+ return system("xdg-open '#{uri}'")
31
33
  else
32
34
  raise "no graphical open method for #{Aspera::Environment.os}"
33
35
  end
@@ -36,7 +38,7 @@ module Aspera
36
38
  attr_accessor :url_method
37
39
 
38
40
  def initialize
39
- @url_method=self.class.default_gui_mode
41
+ @url_method = self.class.default_gui_mode
40
42
  end
41
43
 
42
44
  # this is non blocking
@@ -47,9 +49,9 @@ module Aspera
47
49
  when :text
48
50
  case the_url.to_s
49
51
  when /^http/
50
- puts "USER ACTION: please enter this url in a browser:\n"+the_url.to_s.red()+"\n"
52
+ puts "USER ACTION: please enter this url in a browser:\n" + the_url.to_s.red + "\n"
51
53
  else
52
- puts "USER ACTION: open this:\n"+the_url.to_s.red()+"\n"
54
+ puts "USER ACTION: open this:\n" + the_url.to_s.red + "\n"
53
55
  end
54
56
  else
55
57
  raise StandardError,"unsupported url open method: #{@url_method}"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'aspera/log'
3
5
 
@@ -13,20 +15,20 @@ module Aspera
13
15
  # @param :merge Optional merge data from file to current data
14
16
  def initialize(options)
15
17
  Log.log.debug("persistency: #{options}")
16
- raise "options shall be Hash" unless options.is_a?(Hash)
17
- raise "mandatory :manager" if options[:manager].nil?
18
- raise "mandatory :data" if options[:data].nil?
19
- raise "mandatory :id (String)" unless options[:id].is_a?(String)
20
- raise "mandatory 1 element in :id" unless options[:id].length >= 1
21
- @manager=options[:manager]
22
- @persisted_object=options[:data]
23
- @object_id=options[:id]
18
+ raise 'options shall be Hash' unless options.is_a?(Hash)
19
+ raise 'mandatory :manager' if options[:manager].nil?
20
+ raise 'mandatory :data' if options[:data].nil?
21
+ raise 'mandatory :id (String)' unless options[:id].is_a?(String)
22
+ raise 'mandatory 1 element in :id' unless options[:id].length >= 1
23
+ @manager = options[:manager]
24
+ @persisted_object = options[:data]
25
+ @object_id = options[:id]
24
26
  # by default , at save time, file is deleted if data is nil
25
- @delete_condition=options[:delete] || lambda{|d|d.empty?}
26
- @persist_format=options[:format] || lambda {|h| JSON.generate(h)}
27
- persist_parse=options[:parse] || lambda {|t| JSON.parse(t)}
28
- persist_merge=options[:merge] || lambda {|current,file| current.concat(file).uniq rescue current}
29
- value=@manager.get(@object_id)
27
+ @delete_condition = options[:delete] || lambda{|d|d.empty?}
28
+ @persist_format = options[:format] || lambda {|h| JSON.generate(h)}
29
+ persist_parse = options[:parse] || lambda {|t| JSON.parse(t)}
30
+ persist_merge = options[:merge] || lambda {|current,file| current.concat(file).uniq rescue current}
31
+ value = @manager.get(@object_id)
30
32
  persist_merge.call(@persisted_object,persist_parse.call(value)) unless value.nil?
31
33
  end
32
34
 
@@ -41,6 +43,5 @@ module Aspera
41
43
  def data
42
44
  return @persisted_object
43
45
  end
44
-
45
46
  end
46
47
  end