aspera-cli 4.4.0 → 4.7.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2095 -1503
  3. data/bin/ascli +2 -1
  4. data/bin/asession +4 -5
  5. data/docs/test_env.conf +3 -0
  6. data/examples/aoc.rb +4 -3
  7. data/examples/faspex4.rb +25 -25
  8. data/examples/proxy.pac +1 -1
  9. data/examples/transfer.rb +17 -17
  10. data/lib/aspera/aoc.rb +238 -185
  11. data/lib/aspera/ascmd.rb +93 -83
  12. data/lib/aspera/ats_api.rb +11 -10
  13. data/lib/aspera/cli/basic_auth_plugin.rb +13 -14
  14. data/lib/aspera/cli/extended_value.rb +42 -33
  15. data/lib/aspera/cli/formater.rb +142 -108
  16. data/lib/aspera/cli/info.rb +17 -0
  17. data/lib/aspera/cli/listener/line_dump.rb +3 -2
  18. data/lib/aspera/cli/listener/logger.rb +2 -1
  19. data/lib/aspera/cli/listener/progress.rb +16 -18
  20. data/lib/aspera/cli/listener/progress_multi.rb +18 -21
  21. data/lib/aspera/cli/main.rb +173 -149
  22. data/lib/aspera/cli/manager.rb +163 -168
  23. data/lib/aspera/cli/plugin.rb +43 -31
  24. data/lib/aspera/cli/plugins/alee.rb +6 -6
  25. data/lib/aspera/cli/plugins/aoc.rb +405 -370
  26. data/lib/aspera/cli/plugins/ats.rb +86 -79
  27. data/lib/aspera/cli/plugins/bss.rb +14 -16
  28. data/lib/aspera/cli/plugins/config.rb +580 -362
  29. data/lib/aspera/cli/plugins/console.rb +23 -19
  30. data/lib/aspera/cli/plugins/cos.rb +18 -18
  31. data/lib/aspera/cli/plugins/faspex.rb +201 -158
  32. data/lib/aspera/cli/plugins/faspex5.rb +80 -57
  33. data/lib/aspera/cli/plugins/node.rb +183 -166
  34. data/lib/aspera/cli/plugins/orchestrator.rb +71 -67
  35. data/lib/aspera/cli/plugins/preview.rb +92 -96
  36. data/lib/aspera/cli/plugins/server.rb +79 -75
  37. data/lib/aspera/cli/plugins/shares.rb +35 -19
  38. data/lib/aspera/cli/plugins/sync.rb +20 -22
  39. data/lib/aspera/cli/transfer_agent.rb +76 -113
  40. data/lib/aspera/cli/version.rb +2 -1
  41. data/lib/aspera/colors.rb +35 -27
  42. data/lib/aspera/command_line_builder.rb +48 -34
  43. data/lib/aspera/cos_node.rb +29 -21
  44. data/lib/aspera/data_repository.rb +3 -2
  45. data/lib/aspera/environment.rb +50 -45
  46. data/lib/aspera/fasp/{manager.rb → agent_base.rb} +28 -25
  47. data/lib/aspera/fasp/{connect.rb → agent_connect.rb} +52 -43
  48. data/lib/aspera/fasp/{local.rb → agent_direct.rb} +58 -72
  49. data/lib/aspera/fasp/{http_gw.rb → agent_httpgw.rb} +37 -43
  50. data/lib/aspera/fasp/{node.rb → agent_node.rb} +35 -16
  51. data/lib/aspera/fasp/agent_trsdk.rb +104 -0
  52. data/lib/aspera/fasp/error.rb +2 -1
  53. data/lib/aspera/fasp/error_info.rb +68 -52
  54. data/lib/aspera/fasp/installation.rb +152 -124
  55. data/lib/aspera/fasp/listener.rb +1 -0
  56. data/lib/aspera/fasp/parameters.rb +87 -92
  57. data/lib/aspera/fasp/parameters.yaml +305 -249
  58. data/lib/aspera/fasp/resume_policy.rb +11 -14
  59. data/lib/aspera/fasp/transfer_spec.rb +26 -0
  60. data/lib/aspera/fasp/uri.rb +22 -21
  61. data/lib/aspera/faspex_gw.rb +55 -89
  62. data/lib/aspera/hash_ext.rb +4 -3
  63. data/lib/aspera/id_generator.rb +8 -7
  64. data/lib/aspera/keychain/encrypted_hash.rb +121 -0
  65. data/lib/aspera/keychain/macos_security.rb +90 -0
  66. data/lib/aspera/log.rb +55 -37
  67. data/lib/aspera/nagios.rb +13 -12
  68. data/lib/aspera/node.rb +30 -25
  69. data/lib/aspera/oauth.rb +175 -226
  70. data/lib/aspera/open_application.rb +4 -3
  71. data/lib/aspera/persistency_action_once.rb +6 -6
  72. data/lib/aspera/persistency_folder.rb +5 -9
  73. data/lib/aspera/preview/file_types.rb +6 -5
  74. data/lib/aspera/preview/generator.rb +25 -24
  75. data/lib/aspera/preview/options.rb +16 -14
  76. data/lib/aspera/preview/utils.rb +98 -98
  77. data/lib/aspera/{proxy_auto_config.erb.js → proxy_auto_config.js} +23 -31
  78. data/lib/aspera/proxy_auto_config.rb +111 -20
  79. data/lib/aspera/rest.rb +154 -135
  80. data/lib/aspera/rest_call_error.rb +2 -2
  81. data/lib/aspera/rest_error_analyzer.rb +23 -25
  82. data/lib/aspera/rest_errors_aspera.rb +15 -14
  83. data/lib/aspera/ssh.rb +12 -10
  84. data/lib/aspera/sync.rb +42 -41
  85. data/lib/aspera/temp_file_manager.rb +18 -14
  86. data/lib/aspera/timer_limiter.rb +2 -1
  87. data/lib/aspera/uri_reader.rb +7 -5
  88. data/lib/aspera/web_auth.rb +79 -76
  89. metadata +116 -29
  90. data/docs/Makefile +0 -66
  91. data/docs/README.erb.md +0 -3973
  92. data/docs/README.md +0 -13
  93. data/docs/diagrams.txt +0 -49
  94. data/docs/doc_tools.rb +0 -58
  95. data/lib/aspera/api_detector.rb +0 -60
  96. data/lib/aspera/cli/plugins/shares2.rb +0 -114
  97. data/lib/aspera/secrets.rb +0 -20
data/lib/aspera/oauth.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'aspera/open_application'
2
3
  require 'aspera/web_auth'
3
4
  require 'aspera/id_generator'
@@ -11,14 +12,17 @@ module Aspera
11
12
  # call get_authorization() to get a token.
12
13
  # bearer tokens are kept in memory and also in a file cache for later re-use
13
14
  # if a token is expired (api returns 4xx), call again get_authorization({refresh: true})
15
+ # https://tools.ietf.org/html/rfc6749
14
16
  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
17
+ DEFAULT_CREATE_PARAMS={
18
+ token_field: 'access_token', # field with token in result
19
+ path_token: 'token', # default endpoint for /token to generate token
20
+ web: {path_authorize: 'authorize'} # default endpoint for /authorize, used for code exchange
21
+ }.freeze
22
+
23
+ # OAuth methods supported by default
24
+ STD_AUTH_TYPES=[:web, :jwt].freeze
25
+
22
26
  # remove 5 minutes to account for time offset (TODO: configurable?)
23
27
  JWT_NOTBEFORE_OFFSET_SEC=300
24
28
  # one hour validity (TODO: configurable?)
@@ -27,184 +31,237 @@ module Aspera
27
31
  TOKEN_CACHE_EXPIRY_SEC=1800
28
32
  # a prefix for persistency of tokens (garbage collect)
29
33
  PERSIST_CATEGORY_TOKEN='token'
30
- ONE_HOUR_AS_DAY_FRACTION=Rational(1,24)
34
+ TOKEN_EXPIRATION_GUARD_SEC=120
31
35
 
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
36
+ private_constant :JWT_NOTBEFORE_OFFSET_SEC,:JWT_EXPIRY_OFFSET_SEC,:TOKEN_CACHE_EXPIRY_SEC,:PERSIST_CATEGORY_TOKEN,:TOKEN_EXPIRATION_GUARD_SEC
38
37
 
38
+ # persistency manager
39
+ @persist=nil
40
+ # token creation methods
41
+ @handlers={}
42
+ # token unique identifiers from oauth parameters
43
+ @id_elements=[
44
+ [:crtype],
45
+ [:generic,:grant_type],
46
+ [:jwt,:payload,:sub],
47
+ [:auth,:username],
48
+ [:aoc_pub_link,:json,:url_token],
49
+ [:generic,:apikey],
50
+ [:scope],
51
+ [:generic,:response_type]
52
+ ]
53
+
54
+ class << self
39
55
  def persist_mgr=(manager)
40
56
  @persist=manager
57
+ # cleanup expired tokens
58
+ @persist.garbage_collect(PERSIST_CATEGORY_TOKEN,TOKEN_CACHE_EXPIRY_SEC)
41
59
  end
42
60
 
43
61
  def persist_mgr
44
62
  if @persist.nil?
45
- Log.log.warn('Not using persistency (use Aspera::Oauth.persist_mgr=Aspera::PersistencyFolder.new)')
63
+ Log.log.debug('Not using persistency') # (use Aspera::Oauth.persist_mgr=Aspera::PersistencyFolder.new)
46
64
  # create NULL persistency class
47
65
  @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
66
+ def get(_x);nil;end;def delete(_x);nil;end;def put(_x,_y);nil;end;def garbage_collect(_x,_y);nil;end # rubocop:disable Layout/EmptyLineBetweenDefs
49
67
  end.new
50
68
  end
51
69
  return @persist
52
70
  end
53
71
 
72
+ # delete all existing tokens
54
73
  def flush_tokens
55
74
  persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,nil)
56
75
  end
57
76
 
77
+ # register a bearer token decoder, mainly to inspect expiry date
58
78
  def register_decoder(method)
59
79
  @decoders||=[]
60
80
  @decoders.push(method)
61
81
  end
62
82
 
83
+ # decode token using all registered decoders
63
84
  def decode_token(token)
64
- Log.log.debug(">>>> #{token} : #{@decoders.length}")
65
85
  @decoders.each do |decoder|
66
86
  result=decoder.call(token) rescue nil
67
87
  return result unless result.nil?
68
88
  end
69
89
  return nil
70
90
  end
71
- end
72
91
 
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]))}
92
+ # register a token creation method, specify using field :crtype in constructor
93
+ def register_token_creator(id, method)
94
+ raise 'error' unless id.is_a?(Symbol) && method.is_a?(Proc)
95
+ @handlers[id]=method
96
+ end
75
97
 
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
- }})
98
+ # @return one of the registered creators for the given create type
99
+ def token_creator(id)
100
+ raise "token create type unknown: #{id}/#{id.class}" unless @handlers.has_key?(id)
101
+ @handlers[id]
110
102
  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
103
+
104
+ # list of identifiers foundn in creation parameters that can be used to uniquely identify the token
105
+ def id_elements
106
+ return @id_elements
117
107
  end
118
- # cleanup expired tokens
119
- self.class.persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,TOKEN_CACHE_EXPIRY_SEC)
120
- end
108
+ end # self
109
+
110
+ # JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
111
+ register_decoder lambda { |token| parts=token.split('.'); raise 'not aoc token' unless parts.length.eql?(3); JSON.parse(Base64.decode64(parts[1]))}
121
112
 
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)
113
+ # generic token creation, parameters are provided in :generic
114
+ register_token_creator :generic,lambda { |oauth|
115
+ return oauth.create_token(oauth.params[:generic])
116
+ }
117
+
118
+ # Authentication using Web browser
119
+ register_token_creator :web,lambda { |oauth|
120
+ callback_verif=SecureRandom.uuid # used to check later
121
+ login_page_url=Rest.build_uri("#{oauth.params[:base_url]}/#{oauth.params[:web][:path_authorize]}",optional_scope_client_id({response_type: 'code', redirect_uri: oauth.params[:web][:redirect_uri], state: callback_verif}))
122
+ # here, we need a human to authorize on a web page
124
123
  Log.log.info("login_page_url=#{login_page_url}".bg_red.gray)
125
124
  # start a web server to receive request code
126
- webserver=WebAuth.new(@params[:redirect_uri])
125
+ webserver=WebAuth.new(oauth.params[:web][:redirect_uri])
127
126
  # start browser on login page
128
127
  OpenApplication.instance.uri(login_page_url)
129
128
  # 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
129
+ received_params=webserver.received_request
130
+ raise 'state does not match' if !callback_verif.eql?(received_params['state'])
131
+ # exchange code for token
132
+ return oauth.create_token(oauth.optional_scope_client_id({grant_type: 'authorization_code', code: received_params['code'], redirect_uri: oauth.params[:web][:redirect_uri]},add_secret: true))
133
+ }
135
134
 
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))
135
+ # Authentication using private key
136
+ register_token_creator :jwt,lambda { |oauth|
137
+ # https://tools.ietf.org/html/rfc7523
138
+ # https://tools.ietf.org/html/rfc7519
139
+ require 'jwt'
140
+ seconds_since_epoch=Time.new.to_i
141
+ Log.log.info("seconds=#{seconds_since_epoch}")
142
+ raise 'missing JWT payload' unless oauth.params[:jwt][:payload].is_a?(Hash)
143
+ jwt_payload = {
144
+ exp: seconds_since_epoch+JWT_EXPIRY_OFFSET_SEC, # expiration time
145
+ nbf: seconds_since_epoch-JWT_NOTBEFORE_OFFSET_SEC, # not before
146
+ iat: seconds_since_epoch, # issued at
147
+ jti: SecureRandom.uuid # JWT id
148
+ }.merge(oauth.params[:jwt][:payload])
149
+ Log.log.debug("JWT jwt_payload=[#{jwt_payload}]")
150
+ rsa_private=oauth.params[:jwt][:private_key_obj] # type: OpenSSL::PKey::RSA
151
+ Log.log.debug("private=[#{rsa_private}]")
152
+ assertion = JWT.encode(jwt_payload, rsa_private, 'RS256', oauth.params[:jwt][:headers]||{})
153
+ Log.log.debug("assertion=[#{assertion}]")
154
+ return oauth.create_token(oauth.optional_scope_client_id({grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: assertion}))
155
+ }
156
+
157
+ attr_reader :params, :token_auth_api
158
+
159
+ private
160
+
161
+ # [M]=mandatory [D]=has default value [0]=accept nil
162
+ # :base_url [M] URL of authentication API
163
+ # :auth
164
+ # :crtype [M] :generic, :web, :jwt, custom
165
+ # :client_id [0]
166
+ # :client_secret [0]
167
+ # :scope [0]
168
+ # :path_token [D] API end point to create a token
169
+ # :token_field [D] field in result that contains the token
170
+ # :jwt:private_key_obj [M] for type :jwt
171
+ # :jwt:payload [M] for type :jwt
172
+ # :jwt:headers [0] for type :jwt
173
+ # :web:redirect_uri [M] for type :web
174
+ # :web:path_authorize [D] for type :web
175
+ # :generic [M] for type :generic
176
+ def initialize(a_params)
177
+ Log.log.debug("auth=#{a_params}")
178
+ # replace default values
179
+ @params=DEFAULT_CREATE_PARAMS.clone.deep_merge(a_params)
180
+ if @params.has_key?(:redirect_uri)
181
+ uri=URI.parse(@params[:web][:redirect_uri])
182
+ raise 'redirect_uri scheme must be http or https' unless ['http','https'].include?(uri.scheme)
183
+ raise 'redirect_uri must have a port' if uri.port.nil?
184
+ # TODO: we could check that host is localhost or local address
185
+ end
186
+ rest_params={base_url: @params[:base_url]}
187
+ rest_params[:auth]=a_params[:auth] if a_params.has_key?(:auth)
188
+ @token_auth_api=Rest.new(rest_params)
141
189
  end
142
190
 
143
191
  # @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])}
192
+ # TODO: external handlers shall provide unique identifiers
193
+ def token_cache_id
194
+ parts=[PERSIST_CATEGORY_TOKEN,@params[:base_url]]
195
+ # add some of the parameters that uniquely identify the token
196
+ self.class.id_elements.each do |p|
197
+ identifier=@params.dig(*p)
198
+ identifier=identifier.split(':').last if identifier.is_a?(String) && p.last.eql?(:grant_type)
199
+ parts.push(identifier) unless identifier.nil?
200
+ end
149
201
  return IdGenerator.from_list(parts)
150
202
  end
151
203
 
152
204
  public
153
205
 
154
- # used to change parameter, such as scope
155
- attr_reader :params
206
+ # helper method to create token as per RFC
207
+ def create_token(www_params)
208
+ return @token_auth_api.call({
209
+ operation: 'POST',
210
+ subpath: @params[:path_token],
211
+ headers: {'Accept'=>'application/json'},
212
+ www_body_params: www_params})
213
+ end
156
214
 
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]
215
+ # helper method
216
+ def optional_scope_client_id(call_params, add_secret: false)
217
+ call_params[:scope] = @params[:scope] unless @params[:scope].nil?
218
+ call_params[:client_id] = @params[:client_id] unless @params[:client_id].nil?
219
+ call_params[:client_secret] = @params[:client_secret] if add_secret && !@params[:client_id].nil?
220
+ return call_params
221
+ end
167
222
 
168
- # generate token identifier to use with cache
169
- token_id=token_cache_id(api_scope)
223
+ # Oauth v2 token generation
224
+ # @param use_refresh_token set to true to force refresh or re-generation (if previous failed)
225
+ def get_authorization(use_refresh_token: false)
226
+ # generate token unique identifier for persistency (memory/disk cache)
227
+ token_id=token_cache_id
170
228
 
171
229
  # get token_data from cache (or nil), token_data is what is returned by /token
172
230
  token_data=self.class.persist_mgr.get(token_id)
173
231
  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)
232
+ # Optional optimization: check if node token is expired basd on decoded content then force refresh if close enough
233
+ # might help in case the transfer agent cannot refresh himself
234
+ # `direct` agent is equipped with refresh code
235
+ if !use_refresh_token && !token_data.nil?
236
+ decoded_token = self.class.decode_token(token_data[@params[:token_field]])
237
+ Log.dump('decoded_token',decoded_token) unless decoded_token.nil?
238
+ if decoded_token.is_a?(Hash)
239
+ expires_at_sec=
240
+ if decoded_token['expires_at'].is_a?(String) then DateTime.parse(decoded_token['expires_at']).to_time
241
+ elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
242
+ end
243
+ # force refresh if we see a token too close from expiration
244
+ use_refresh_token=true if expires_at_sec.is_a?(Time) && (expires_at_sec-Time.now) < TOKEN_EXPIRATION_GUARD_SEC
245
+ Log.log.debug("Expiration: #{expires_at_sec} / #{use_refresh_token}")
186
246
  end
187
247
  end
188
248
 
189
249
  # an API was already called, but failed, we need to regenerate or refresh
190
250
  if use_refresh_token
191
- if token_data.is_a?(Hash) and token_data.has_key?('refresh_token')
251
+ if token_data.is_a?(Hash) && token_data.has_key?('refresh_token')
192
252
  # save possible refresh token, before deleting the cache
193
253
  refresh_token=token_data['refresh_token']
194
254
  end
195
- # delete caches
255
+ # delete cache
196
256
  self.class.persist_mgr.delete(token_id)
197
257
  token_data=nil
198
258
  # lets try the existing refresh token
199
259
  if !refresh_token.nil?
200
260
  Log.log.info("refresh=[#{refresh_token}]".bg_green)
201
261
  # 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
262
+ # note: AoC admin token has no refresh, and lives by default 1800secs
263
+ resp=create_token(optional_scope_client_id({grant_type: 'refresh_token',refresh_token: refresh_token}))
264
+ if resp[:http].code.start_with?('2')
208
265
  # save only if success
209
266
  json_data=resp[:http].body
210
267
  token_data=JSON.parse(json_data)
@@ -215,116 +272,9 @@ module Aspera
215
272
  end
216
273
  end
217
274
 
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 ?
275
+ # no cache, nor refresh: generate a token
276
+ if token_data.nil?
277
+ resp=self.class.token_creator(@params[:crtype]).call(self)
328
278
  json_data=resp[:http].body
329
279
  token_data=JSON.parse(json_data)
330
280
  self.class.persist_mgr.put(token_id,json_data)
@@ -333,6 +283,5 @@ module Aspera
333
283
  # ok we shall have a token here
334
284
  return 'Bearer '+token_data[@params[:token_field]]
335
285
  end
336
-
337
286
  end # OAuth
338
287
  end # Aspera
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'aspera/log'
2
3
  require 'aspera/environment'
3
4
  require 'rbconfig'
@@ -10,12 +11,12 @@ module Aspera
10
11
  class OpenApplication
11
12
  include Singleton
12
13
  # User Interfaces
13
- def self.user_interfaces; [ :text, :graphical ]; end
14
+ def self.user_interfaces; [:text, :graphical]; end
14
15
 
15
16
  def self.default_gui_mode
16
17
  return :graphical if [Aspera::Environment::OS_WINDOWS,Aspera::Environment::OS_X].include?(Aspera::Environment.os)
17
18
  # unix family
18
- return :graphical if ENV.has_key?("DISPLAY") and !ENV["DISPLAY"].empty?
19
+ return :graphical if ENV.has_key?('DISPLAY') && !ENV['DISPLAY'].empty?
19
20
  return :text
20
21
  end
21
22
 
@@ -27,7 +28,7 @@ module Aspera
27
28
  when Aspera::Environment::OS_WINDOWS
28
29
  return system('start explorer "'+uri.to_s+'"')
29
30
  when Aspera::Environment::OS_LINUX
30
- return system("xdg-open '#{uri.to_s}'")
31
+ return system("xdg-open '#{uri}'")
31
32
  else
32
33
  raise "no graphical open method for #{Aspera::Environment.os}"
33
34
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'json'
2
3
  require 'aspera/log'
3
4
 
@@ -13,11 +14,11 @@ module Aspera
13
14
  # @param :merge Optional merge data from file to current data
14
15
  def initialize(options)
15
16
  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
17
+ raise 'options shall be Hash' unless options.is_a?(Hash)
18
+ raise 'mandatory :manager' if options[:manager].nil?
19
+ raise 'mandatory :data' if options[:data].nil?
20
+ raise 'mandatory :id (String)' unless options[:id].is_a?(String)
21
+ raise 'mandatory 1 element in :id' unless options[:id].length >= 1
21
22
  @manager=options[:manager]
22
23
  @persisted_object=options[:data]
23
24
  @object_id=options[:id]
@@ -41,6 +42,5 @@ module Aspera
41
42
  def data
42
43
  return @persisted_object
43
44
  end
44
-
45
45
  end
46
46
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'fileutils'
2
3
  require 'aspera/log'
3
4
 
@@ -10,10 +11,6 @@ module Aspera
10
11
  private_constant :FILE_SUFFIX
11
12
  def initialize(folder)
12
13
  @cache={}
13
- set_folder(folder)
14
- end
15
-
16
- def set_folder(folder)
17
14
  @folder=folder
18
15
  Log.log.debug("persistency folder: #{@folder}")
19
16
  end
@@ -22,12 +19,12 @@ module Aspera
22
19
  def get(object_id)
23
20
  Log.log.debug("persistency get: #{object_id}")
24
21
  if @cache.has_key?(object_id)
25
- Log.log.debug("got from memory cache")
22
+ Log.log.debug('got from memory cache')
26
23
  else
27
24
  persist_filepath=id_to_filepath(object_id)
28
25
  Log.log.debug("persistency = #{persist_filepath}")
29
26
  if File.exist?(persist_filepath)
30
- Log.log.debug("got from file cache")
27
+ Log.log.debug('got from file cache')
31
28
  @cache[object_id]=File.read(persist_filepath)
32
29
  end
33
30
  end
@@ -35,7 +32,7 @@ module Aspera
35
32
  end
36
33
 
37
34
  def put(object_id,value)
38
- raise "value: only String supported" unless value.is_a?(String)
35
+ raise 'value: only String supported' unless value.is_a?(String)
39
36
  persist_filepath=id_to_filepath(object_id)
40
37
  Log.log.debug("persistency saving: #{persist_filepath}")
41
38
  File.write(persist_filepath,value)
@@ -66,11 +63,10 @@ module Aspera
66
63
 
67
64
  # @param object_id String or Array
68
65
  def id_to_filepath(object_id)
69
- raise "object_id: only String supported" unless object_id.is_a?(String)
66
+ raise 'object_id: only String supported' unless object_id.is_a?(String)
70
67
  FileUtils.mkdir_p(@folder)
71
68
  return File.join(@folder,"#{object_id}#{FILE_SUFFIX}")
72
69
  #.gsub(/[^a-z]+/,FILE_FIELD_SEPARATOR)
73
70
  end
74
-
75
71
  end # PersistencyFolder
76
72
  end # Aspera
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'aspera/log'
2
3
  require 'singleton'
3
4
 
@@ -305,15 +306,15 @@ module Aspera
305
306
  def conversion_type(filepath,mimetype)
306
307
  Log.log.debug("conversion_type(#{filepath},m=#{mimetype},t=#{@use_mimemagic})")
307
308
  # 1- get type from provided mime type, using local mapping
308
- conv_type=SUPPORTED_MIME_TYPES[mimetype] if ! mimetype.nil?
309
+ conv_type=SUPPORTED_MIME_TYPES[mimetype] if !mimetype.nil?
309
310
  # 2- else, from computed mime type (if available)
310
- if conv_type.nil? and @use_mimemagic
311
+ if conv_type.nil? && @use_mimemagic
311
312
  detected_mime=mime_from_file(filepath)
312
- if ! detected_mime.nil?
313
+ if !detected_mime.nil?
313
314
  conv_type=SUPPORTED_MIME_TYPES[detected_mime]
314
- if ! mimetype.nil?
315
+ if !mimetype.nil?
315
316
  if mimetype.eql?(detected_mime)
316
- Log.log.debug("matching mime type per magic number")
317
+ Log.log.debug('matching mime type per magic number')
317
318
  else
318
319
  # note: detected can be nil
319
320
  Log.log.debug("non matching mime types: node=[#{mimetype}], magic=[#{detected_mime}]")