aspera-cli 4.15.0 → 4.17.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 (108) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/BUGS.md +29 -3
  4. data/CHANGELOG.md +375 -280
  5. data/CONTRIBUTING.md +71 -18
  6. data/README.md +1978 -1656
  7. data/bin/ascli +13 -31
  8. data/bin/asession +32 -22
  9. data/examples/dascli +2 -2
  10. data/lib/aspera/agent/alpha.rb +117 -0
  11. data/lib/aspera/agent/base.rb +61 -0
  12. data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
  13. data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +116 -116
  14. data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +21 -19
  15. data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +21 -33
  16. data/lib/aspera/agent/trsdk.rb +188 -0
  17. data/lib/aspera/api/aoc.rb +586 -0
  18. data/lib/aspera/api/ats.rb +46 -0
  19. data/lib/aspera/api/cos_node.rb +95 -0
  20. data/lib/aspera/api/node.rb +344 -0
  21. data/lib/aspera/ascmd.rb +47 -14
  22. data/lib/aspera/{fasp → ascp}/installation.rb +54 -15
  23. data/lib/aspera/{fasp → ascp}/management.rb +14 -14
  24. data/lib/aspera/{fasp → ascp}/products.rb +1 -1
  25. data/lib/aspera/assert.rb +45 -0
  26. data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
  27. data/lib/aspera/cli/extended_value.rb +5 -5
  28. data/lib/aspera/cli/formatter.rb +27 -14
  29. data/lib/aspera/cli/hints.rb +7 -6
  30. data/lib/aspera/cli/main.rb +49 -29
  31. data/lib/aspera/cli/manager.rb +46 -36
  32. data/lib/aspera/cli/plugin.rb +34 -20
  33. data/lib/aspera/cli/plugin_factory.rb +61 -0
  34. data/lib/aspera/cli/plugins/alee.rb +7 -7
  35. data/lib/aspera/cli/plugins/aoc.rb +168 -132
  36. data/lib/aspera/cli/plugins/ats.rb +33 -33
  37. data/lib/aspera/cli/plugins/bss.rb +3 -4
  38. data/lib/aspera/cli/plugins/config.rb +250 -272
  39. data/lib/aspera/cli/plugins/console.rb +8 -6
  40. data/lib/aspera/cli/plugins/cos.rb +20 -19
  41. data/lib/aspera/cli/plugins/faspex.rb +71 -60
  42. data/lib/aspera/cli/plugins/faspex5.rb +212 -133
  43. data/lib/aspera/cli/plugins/node.rb +83 -75
  44. data/lib/aspera/cli/plugins/orchestrator.rb +36 -44
  45. data/lib/aspera/cli/plugins/preview.rb +33 -31
  46. data/lib/aspera/cli/plugins/server.rb +33 -32
  47. data/lib/aspera/cli/plugins/shares.rb +39 -33
  48. data/lib/aspera/cli/sync_actions.rb +9 -9
  49. data/lib/aspera/cli/transfer_agent.rb +45 -25
  50. data/lib/aspera/cli/transfer_progress.rb +2 -3
  51. data/lib/aspera/cli/version.rb +1 -1
  52. data/lib/aspera/colors.rb +5 -0
  53. data/lib/aspera/command_line_builder.rb +16 -14
  54. data/lib/aspera/coverage.rb +21 -0
  55. data/lib/aspera/data_repository.rb +33 -2
  56. data/lib/aspera/environment.rb +5 -4
  57. data/lib/aspera/faspex_gw.rb +13 -11
  58. data/lib/aspera/faspex_postproc.rb +6 -5
  59. data/lib/aspera/id_generator.rb +4 -2
  60. data/lib/aspera/json_rpc.rb +10 -8
  61. data/lib/aspera/keychain/encrypted_hash.rb +46 -11
  62. data/lib/aspera/keychain/macos_security.rb +29 -22
  63. data/lib/aspera/log.rb +5 -4
  64. data/lib/aspera/nagios.rb +7 -2
  65. data/lib/aspera/node_simulator.rb +213 -0
  66. data/lib/aspera/oauth/base.rb +143 -0
  67. data/lib/aspera/oauth/factory.rb +124 -0
  68. data/lib/aspera/oauth/generic.rb +34 -0
  69. data/lib/aspera/oauth/jwt.rb +51 -0
  70. data/lib/aspera/oauth/url_json.rb +31 -0
  71. data/lib/aspera/oauth/web.rb +50 -0
  72. data/lib/aspera/oauth.rb +5 -328
  73. data/lib/aspera/open_application.rb +7 -7
  74. data/lib/aspera/persistency_action_once.rb +13 -14
  75. data/lib/aspera/persistency_folder.rb +3 -2
  76. data/lib/aspera/preview/file_types.rb +53 -267
  77. data/lib/aspera/preview/generator.rb +7 -5
  78. data/lib/aspera/preview/terminal.rb +17 -7
  79. data/lib/aspera/preview/utils.rb +8 -7
  80. data/lib/aspera/proxy_auto_config.rb +6 -3
  81. data/lib/aspera/rest.rb +187 -140
  82. data/lib/aspera/rest_error_analyzer.rb +1 -0
  83. data/lib/aspera/rest_errors_aspera.rb +5 -3
  84. data/lib/aspera/resumer.rb +77 -0
  85. data/lib/aspera/secret_hider.rb +5 -2
  86. data/lib/aspera/ssh.rb +15 -8
  87. data/lib/aspera/temp_file_manager.rb +1 -1
  88. data/lib/aspera/{fasp → transfer}/error.rb +3 -3
  89. data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
  90. data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
  91. data/lib/aspera/{fasp → transfer}/parameters.rb +95 -120
  92. data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +23 -19
  93. data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
  94. data/lib/aspera/transfer/sync.rb +273 -0
  95. data/lib/aspera/{fasp → transfer}/uri.rb +10 -9
  96. data/lib/aspera/web_server_simple.rb +12 -3
  97. data.tar.gz.sig +0 -0
  98. metadata +92 -68
  99. metadata.gz.sig +0 -0
  100. data/lib/aspera/aoc.rb +0 -606
  101. data/lib/aspera/ats_api.rb +0 -47
  102. data/lib/aspera/cos_node.rb +0 -93
  103. data/lib/aspera/fasp/agent_aspera.rb +0 -126
  104. data/lib/aspera/fasp/agent_base.rb +0 -48
  105. data/lib/aspera/fasp/agent_trsdk.rb +0 -146
  106. data/lib/aspera/fasp/resume_policy.rb +0 -77
  107. data/lib/aspera/node.rb +0 -338
  108. data/lib/aspera/sync.rb +0 -219
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/oauth/base'
4
+
5
+ module Aspera
6
+ module OAuth
7
+ class Generic < Base
8
+ def initialize(
9
+ grant_type:,
10
+ response_type: nil,
11
+ apikey: nil,
12
+ receiver_client_ids: nil,
13
+ **base_params
14
+ )
15
+ super(**base_params)
16
+ @create_params = {
17
+ grant_type: grant_type
18
+ }
19
+ @create_params[:response_type] = response_type if response_type
20
+ @create_params[:apikey] = apikey if apikey
21
+ @create_params[:receiver_client_ids] = receiver_client_ids if receiver_client_ids
22
+ @identifiers.push(
23
+ @create_params[:grant_type]&.split(':')&.last,
24
+ @create_params[:apikey],
25
+ @create_params[:response_type])
26
+ end
27
+
28
+ def create_token
29
+ return create_token_call(optional_scope_client_id.merge(@create_params))
30
+ end
31
+ end
32
+ Factory.instance.register_token_creator(Generic)
33
+ end
34
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/oauth/base'
4
+ require 'aspera/assert'
5
+ require 'securerandom'
6
+ module Aspera
7
+ module OAuth
8
+ # Authentication using private key
9
+ 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
13
+ def initialize(
14
+ payload:,
15
+ private_key_obj:,
16
+ headers: {},
17
+ **base_params
18
+ )
19
+ Aspera.assert_type(payload, Hash){'payload'}
20
+ Aspera.assert_type(private_key_obj, OpenSSL::PKey::RSA){'private_key_obj'}
21
+ Aspera.assert_type(headers, Hash){'headers'}
22
+ super(**base_params)
23
+ @private_key_obj = private_key_obj
24
+ @payload = payload
25
+ @headers = headers
26
+ @identifiers.push(@payload[:sub])
27
+ end
28
+
29
+ def create_token
30
+ # https://tools.ietf.org/html/rfc7523
31
+ # https://tools.ietf.org/html/rfc7519
32
+ require 'jwt'
33
+ seconds_since_epoch = Time.new.to_i
34
+ Log.log.info{"seconds=#{seconds_since_epoch}"}
35
+ Aspera.assert(@payload.is_a?(Hash)){'missing JWT payload'}
36
+ 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
+ jti: SecureRandom.uuid # JWT id
41
+ }.merge(@payload)
42
+ Log.log.debug{"JWT jwt_payload=[#{jwt_payload}]"}
43
+ Log.log.debug{"private=[#{@private_key_obj}]"}
44
+ assertion = JWT.encode(jwt_payload, @private_key_obj, 'RS256', @headers)
45
+ Log.log.debug{"assertion=[#{assertion}]"}
46
+ return create_token_call(optional_scope_client_id.merge(grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: assertion))
47
+ end
48
+ end
49
+ Factory.instance.register_token_creator(Jwt)
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/oauth/base'
4
+
5
+ module Aspera
6
+ module OAuth
7
+ class UrlJson < Base
8
+ def initialize(
9
+ json:,
10
+ url:,
11
+ **generic_params
12
+ )
13
+ super(**generic_params)
14
+ @json_params = json
15
+ @url_params = url
16
+ @identifiers.push(@json_params[:url_token])
17
+ end
18
+
19
+ def create_token
20
+ @api.call(
21
+ operation: 'POST',
22
+ subpath: @path_token,
23
+ 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)
26
+ )
27
+ end
28
+ end
29
+ Factory.instance.register_token_creator(UrlJson)
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/oauth/base'
4
+ require 'aspera/open_application'
5
+ require 'aspera/web_auth'
6
+ require 'aspera/assert'
7
+ module Aspera
8
+ module OAuth
9
+ # Authentication using Web browser
10
+ class Web < Base
11
+ # @param g_o:redirect_uri [M] for type :web
12
+ # @param g_o:path_authorize [D] for type :web
13
+ def initialize(
14
+ redirect_uri:,
15
+ path_authorize: 'authorize',
16
+ **base_params
17
+ )
18
+ super(**base_params)
19
+ @redirect_uri = redirect_uri
20
+ @path_authorize = path_authorize
21
+ uri = URI.parse(@redirect_uri)
22
+ Aspera.assert(%w[http https].include?(uri.scheme)){'redirect_uri scheme must be http or https'}
23
+ Aspera.assert(!uri.port.nil?){'redirect_uri must have a port'}
24
+ # TODO: we could check that host is localhost or local address
25
+ end
26
+
27
+ def create_token
28
+ random_state = SecureRandom.uuid # used to check later
29
+ login_page_url = Rest.build_uri(
30
+ "#{@base_url}/#{@path_authorize}",
31
+ optional_scope_client_id.merge(response_type: 'code', redirect_uri: @redirect_uri, state: random_state))
32
+ # here, we need a human to authorize on a web page
33
+ Log.log.info{"login_page_url=#{login_page_url}".bg_red.gray}
34
+ # start a web server to receive request code
35
+ web_server = WebAuth.new(@redirect_uri)
36
+ # start browser on login page
37
+ OpenApplication.instance.uri(login_page_url)
38
+ # wait for code in request
39
+ received_params = web_server.received_request
40
+ Aspera.assert(random_state.eql?(received_params['state'])){'wrong received state'}
41
+ # exchange code for token
42
+ return create_token_call(optional_scope_client_id(add_secret: true).merge(
43
+ grant_type: 'authorization_code',
44
+ code: received_params['code'],
45
+ redirect_uri: @redirect_uri))
46
+ end
47
+ end
48
+ Factory.instance.register_token_creator(Web)
49
+ end
50
+ end
data/lib/aspera/oauth.rb CHANGED
@@ -1,330 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'aspera/open_application'
4
- require 'aspera/web_auth'
5
- require 'aspera/id_generator'
6
- require 'base64'
7
- require 'date'
8
- require 'socket'
9
- require 'securerandom'
10
-
11
- module Aspera
12
- # Implement OAuth 2 for the REST client and generate a bearer token
13
- # call get_authorization() to get a token.
14
- # bearer tokens are kept in memory and also in a file cache for later re-use
15
- # if a token is expired (api returns 4xx), call again get_authorization({refresh: true})
16
- # https://tools.ietf.org/html/rfc6749
17
- class Oauth
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
-
27
- @@globals = { # rubocop:disable Style/ClassVars
28
- # remove 5 minutes to account for time offset between client and server (TODO: configurable?)
29
- jwt_accepted_offset_sec: 300,
30
- # one hour validity (TODO: configurable?)
31
- jwt_expiry_offset_sec: 3600,
32
- # tokens older than 30 minutes will be discarded from cache
33
- token_cache_expiry_sec: 1800,
34
- # tokens valid for less than this duration will be regenerated
35
- token_expiration_guard_sec: 120
36
- }
37
-
38
- # a prefix for persistency of tokens (simplify garbage collect)
39
- PERSIST_CATEGORY_TOKEN = 'token'
40
- # prefix for bearer token when in header
41
- BEARER_PREFIX = 'Bearer '
42
-
43
- private_constant :PERSIST_CATEGORY_TOKEN, :BEARER_PREFIX
44
-
45
- # persistency manager
46
- @persist = nil
47
- # token creation methods
48
- @create_handlers = {}
49
- # token unique identifiers from oauth parameters
50
- @id_handlers = {}
51
-
52
- class << self
53
- def bearer_build(token)
54
- return BEARER_PREFIX + token
55
- end
56
-
57
- def bearer_extract(token)
58
- raise 'not a bearer token, wrong prefix' unless bearer?(token)
59
- return token[BEARER_PREFIX.length..-1]
60
- end
61
-
62
- def bearer?(token)
63
- return token.start_with?(BEARER_PREFIX)
64
- end
65
-
66
- def persist_mgr=(manager)
67
- @persist = manager
68
- # cleanup expired tokens
69
- @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @@globals[:token_cache_expiry_sec])
70
- end
71
-
72
- def persist_mgr
73
- if @persist.nil?
74
- Log.log.debug('Not using persistency') # (use Aspera::Oauth.persist_mgr=Aspera::PersistencyFolder.new)
75
- # create NULL persistency class
76
- @persist = Class.new do
77
- 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, Style/Semicolon, Layout/LineLength
78
- end.new
79
- end
80
- return @persist
81
- end
82
-
83
- # delete all existing tokens
84
- def flush_tokens
85
- persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN, nil)
86
- end
87
-
88
- # register a bearer token decoder, mainly to inspect expiry date
89
- def register_decoder(method)
90
- @decoders ||= []
91
- @decoders.push(method)
92
- end
93
-
94
- # decode token using all registered decoders
95
- def decode_token(token)
96
- @decoders.each do |decoder|
97
- result = decoder.call(token) rescue nil
98
- return result unless result.nil?
99
- end
100
- return nil
101
- end
102
-
103
- # register a token creation method
104
- # @param id creation type from field :grant_method in constructor
105
- # @param lambda_create called to create token
106
- # @param id_create called to generate unique id for token, for cache
107
- def register_token_creator(id, lambda_create, id_create)
108
- Log.log.debug{"registering token creator #{id}"}
109
- raise 'ERROR: requites Symbol and 2 lambdas' unless id.is_a?(Symbol) && lambda_create.is_a?(Proc) && id_create.is_a?(Proc)
110
- @create_handlers[id] = lambda_create
111
- @id_handlers[id] = id_create
112
- end
113
-
114
- # @return one of the registered creators for the given create type
115
- def token_creator(id)
116
- raise "token grant method unknown: '#{id}' (#{id.class})" unless @create_handlers.key?(id)
117
- @create_handlers[id]
118
- end
119
-
120
- # list of identifiers found in creation parameters that can be used to uniquely identify the token
121
- def id_creator(id)
122
- raise "id creator type unknown: #{id}/#{id.class}" unless @id_handlers.key?(id)
123
- @id_handlers[id]
124
- end
125
- end # self
126
-
127
- # JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
128
- register_decoder lambda { |token| parts = token.split('.'); raise 'not aoc token' unless parts.length.eql?(3); JSON.parse(Base64.decode64(parts[1]))} # rubocop:disable Style/Semicolon, Layout/LineLength
129
-
130
- # generic token creation, parameters are provided in :generic
131
- register_token_creator :generic, lambda { |oauth|
132
- return oauth.create_token(oauth.specific_parameters)
133
- }, lambda { |oauth|
134
- return [
135
- oauth.specific_parameters[:grant_type]&.split(':')&.last,
136
- oauth.specific_parameters[:apikey],
137
- oauth.specific_parameters[:response_type]
138
- ]
139
- }
140
-
141
- # Authentication using Web browser
142
- register_token_creator :web, lambda { |oauth|
143
- random_state = SecureRandom.uuid # used to check later
144
- login_page_url = Rest.build_uri(
145
- "#{oauth.api.params[:base_url]}/#{oauth.specific_parameters[:path_authorize]}",
146
- oauth.optional_scope_client_id.merge(response_type: 'code', redirect_uri: oauth.specific_parameters[:redirect_uri], state: random_state))
147
- # here, we need a human to authorize on a web page
148
- Log.log.info{"login_page_url=#{login_page_url}".bg_red.gray}
149
- # start a web server to receive request code
150
- web_server = WebAuth.new(oauth.specific_parameters[:redirect_uri])
151
- # start browser on login page
152
- OpenApplication.instance.uri(login_page_url)
153
- # wait for code in request
154
- received_params = web_server.received_request
155
- raise 'wrong received state' unless random_state.eql?(received_params['state'])
156
- # exchange code for token
157
- return oauth.create_token(oauth.optional_scope_client_id(add_secret: true).merge(
158
- grant_type: 'authorization_code',
159
- code: received_params['code'],
160
- redirect_uri: oauth.specific_parameters[:redirect_uri]))
161
- }, lambda { |_oauth|
162
- return []
163
- }
164
-
165
- # Authentication using private key
166
- register_token_creator :jwt, lambda { |oauth|
167
- # https://tools.ietf.org/html/rfc7523
168
- # https://tools.ietf.org/html/rfc7519
169
- require 'jwt'
170
- seconds_since_epoch = Time.new.to_i
171
- Log.log.info{"seconds=#{seconds_since_epoch}"}
172
- raise 'missing JWT payload' unless oauth.specific_parameters[:payload].is_a?(Hash)
173
- jwt_payload = {
174
- exp: seconds_since_epoch + @@globals[:jwt_expiry_offset_sec], # expiration time
175
- nbf: seconds_since_epoch - @@globals[:jwt_accepted_offset_sec], # not before
176
- iat: seconds_since_epoch - @@globals[:jwt_accepted_offset_sec] + 1, # issued at (we tell a little in the past so that server always accepts)
177
- jti: SecureRandom.uuid # JWT id
178
- }.merge(oauth.specific_parameters[:payload])
179
- Log.log.debug{"JWT jwt_payload=[#{jwt_payload}]"}
180
- rsa_private = oauth.specific_parameters[:private_key_obj] # type: OpenSSL::PKey::RSA
181
- Log.log.debug{"private=[#{rsa_private}]"}
182
- assertion = JWT.encode(jwt_payload, rsa_private, 'RS256', oauth.specific_parameters[:headers] || {})
183
- Log.log.debug{"assertion=[#{assertion}]"}
184
- return oauth.create_token(oauth.optional_scope_client_id.merge(grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: assertion))
185
- }, lambda { |oauth|
186
- return [oauth.specific_parameters.dig(:payload, :sub)]
187
- }
188
-
189
- attr_reader :generic_parameters, :specific_parameters, :api
190
-
191
- private
192
-
193
- # [M]=mandatory [D]=has default value [0]=accept nil
194
- # :base_url [M] URL of authentication API
195
- # :auth
196
- # :grant_method [M] :generic, :web, :jwt, custom
197
- # :client_id [0]
198
- # :client_secret [0]
199
- # :scope [0]
200
- # :path_token [D] API end point to create a token
201
- # :token_field [D] field in result that contains the token
202
- # :jwt:private_key_obj [M] for type :jwt
203
- # :jwt:payload [M] for type :jwt
204
- # :jwt:headers [0] for type :jwt
205
- # :web:redirect_uri [M] for type :web
206
- # :web:path_authorize [D] for type :web
207
- # :generic [M] for type :generic
208
- def initialize(a_params)
209
- Log.log.debug{"auth=#{a_params}"}
210
- # replace default values
211
- @generic_parameters = DEFAULT_CREATE_PARAMS.deep_merge(a_params)
212
- # legacy
213
- @generic_parameters[:grant_method] ||= @generic_parameters.delete(:crtype) if @generic_parameters.key?(:crtype) # cspell: disable-line
214
- # check that type is known
215
- self.class.token_creator(@generic_parameters[:grant_method])
216
- # specific parameters for the creation type
217
- @specific_parameters = @generic_parameters[@generic_parameters[:grant_method]]
218
- if @generic_parameters[:grant_method].eql?(:web) && @specific_parameters.key?(:redirect_uri)
219
- uri = URI.parse(@specific_parameters[:redirect_uri])
220
- raise 'redirect_uri scheme must be http or https' unless %w[http https].include?(uri.scheme)
221
- raise 'redirect_uri must have a port' if uri.port.nil?
222
- # TODO: we could check that host is localhost or local address
223
- end
224
- rest_params = {
225
- base_url: @generic_parameters[:base_url],
226
- redirect_max: 2
227
- }
228
- rest_params[:auth] = a_params[:auth] if a_params.key?(:auth)
229
- @api = Rest.new(rest_params)
230
- # if needed use from api
231
- @generic_parameters.delete(:base_url)
232
- @generic_parameters.delete(:auth)
233
- @generic_parameters.delete(@generic_parameters[:grant_method])
234
- Log.log.debug{Log.dump(:generic_parameters, @generic_parameters)}
235
- Log.log.debug{Log.dump(:specific_parameters, @specific_parameters)}
236
- end
237
-
238
- public
239
-
240
- # helper method to create token as per RFC
241
- def create_token(www_params)
242
- Log.log.debug{'Generating a new token'.bg_green}
243
- return @api.call({
244
- operation: 'POST',
245
- subpath: @generic_parameters[:path_token],
246
- headers: {'Accept' => 'application/json'},
247
- www_body_params: www_params})
248
- end
249
-
250
- # @return Hash with optional general parameters
251
- def optional_scope_client_id(add_secret: false)
252
- call_params = {}
253
- call_params[:scope] = @generic_parameters[:scope] unless @generic_parameters[:scope].nil?
254
- call_params[:client_id] = @generic_parameters[:client_id] unless @generic_parameters[:client_id].nil?
255
- call_params[:client_secret] = @generic_parameters[:client_secret] if add_secret && !@generic_parameters[:client_id].nil?
256
- return call_params
257
- end
258
-
259
- # Oauth v2 token generation
260
- # @param use_refresh_token set to true to force refresh or re-generation (if previous failed)
261
- def get_authorization(use_refresh_token: false, use_cache: true)
262
- # generate token unique identifier for persistency (memory/disk cache)
263
- token_id = IdGenerator.from_list([
264
- PERSIST_CATEGORY_TOKEN,
265
- @api.params[:base_url],
266
- @generic_parameters[:grant_method],
267
- self.class.id_creator(@generic_parameters[:grant_method]).call(self), # array, so we flatten later
268
- @generic_parameters[:scope],
269
- @api.params.dig(%i[auth username])
270
- ].flatten)
271
-
272
- # get token_data from cache (or nil), token_data is what is returned by /token
273
- token_data = self.class.persist_mgr.get(token_id) if use_cache
274
- token_data = JSON.parse(token_data) unless token_data.nil?
275
- # Optional optimization: check if node token is expired based on decoded content then force refresh if close enough
276
- # might help in case the transfer agent cannot refresh himself
277
- # `direct` agent is equipped with refresh code
278
- if !use_refresh_token && !token_data.nil?
279
- decoded_token = self.class.decode_token(token_data[@generic_parameters[:token_field]])
280
- Log.log.debug{Log.dump('decoded_token', decoded_token)} unless decoded_token.nil?
281
- if decoded_token.is_a?(Hash)
282
- expires_at_sec =
283
- if decoded_token['expires_at'].is_a?(String) then DateTime.parse(decoded_token['expires_at']).to_time
284
- elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
285
- end
286
- # force refresh if we see a token too close from expiration
287
- use_refresh_token = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < @@globals[:token_expiration_guard_sec]
288
- Log.log.debug{"Expiration: #{expires_at_sec} / #{use_refresh_token}"}
289
- end
290
- end
291
-
292
- # an API was already called, but failed, we need to regenerate or refresh
293
- if use_refresh_token
294
- if token_data.is_a?(Hash) && token_data.key?('refresh_token') && !token_data['refresh_token'].eql?('not_supported')
295
- # save possible refresh token, before deleting the cache
296
- refresh_token = token_data['refresh_token']
297
- end
298
- # delete cache
299
- self.class.persist_mgr.delete(token_id)
300
- token_data = nil
301
- # lets try the existing refresh token
302
- if !refresh_token.nil?
303
- Log.log.info{"refresh=[#{refresh_token}]".bg_green}
304
- # try to refresh
305
- # note: AoC admin token has no refresh, and lives by default 1800secs
306
- resp = create_token(optional_scope_client_id.merge(grant_type: 'refresh_token', refresh_token: refresh_token))
307
- if resp[:http].code.start_with?('2')
308
- # save only if success
309
- json_data = resp[:http].body
310
- token_data = JSON.parse(json_data)
311
- self.class.persist_mgr.put(token_id, json_data)
312
- else
313
- Log.log.debug{"refresh failed: #{resp[:http].body}".bg_red}
314
- end
315
- end
316
- end
317
-
318
- # no cache, nor refresh: generate a token
319
- if token_data.nil?
320
- resp = self.class.token_creator(@generic_parameters[:grant_method]).call(self)
321
- json_data = resp[:http].body
322
- token_data = JSON.parse(json_data)
323
- self.class.persist_mgr.put(token_id, json_data)
324
- end # if ! in_cache
325
- raise "API error: No such field in answer: #{@generic_parameters[:token_field]}" unless token_data.key?(@generic_parameters[:token_field])
326
- # ok we shall have a token here
327
- return self.class.bearer_build(token_data[@generic_parameters[:token_field]])
328
- end
329
- end # OAuth
330
- end # Aspera
3
+ require 'aspera/oauth/factory'
4
+ require 'aspera/oauth/generic'
5
+ require 'aspera/oauth/jwt'
6
+ require 'aspera/oauth/web'
7
+ require 'aspera/oauth/url_json'
@@ -17,7 +17,7 @@ module Aspera
17
17
  def user_interfaces; USER_INTERFACES; end
18
18
 
19
19
  def default_gui_mode
20
- return :graphical if [Aspera::Environment::OS_WINDOWS, Aspera::Environment::OS_X].include?(Aspera::Environment.os)
20
+ return :graphical if [Environment::OS_WINDOWS, Environment::OS_X].include?(Environment.os)
21
21
  # unix family
22
22
  return :graphical if ENV.key?('DISPLAY') && !ENV['DISPLAY'].empty?
23
23
  return :text
@@ -25,19 +25,19 @@ module Aspera
25
25
 
26
26
  # command must be non blocking
27
27
  def uri_graphical(uri)
28
- case Aspera::Environment.os
29
- when Aspera::Environment::OS_X then return system('open', uri.to_s)
30
- when Aspera::Environment::OS_WINDOWS then return system('start', 'explorer', %Q{"#{uri}"})
31
- when Aspera::Environment::OS_LINUX then return system('xdg-open', uri.to_s)
28
+ case Environment.os
29
+ when Environment::OS_X then return system('open', uri.to_s)
30
+ when Environment::OS_WINDOWS then return system('start', 'explorer', %Q{"#{uri}"})
31
+ when Environment::OS_LINUX then return system('xdg-open', uri.to_s)
32
32
  else
33
- raise "no graphical open method for #{Aspera::Environment.os}"
33
+ raise "no graphical open method for #{Environment.os}"
34
34
  end
35
35
  end
36
36
 
37
37
  def editor(file_path)
38
38
  if ENV.key?('EDITOR')
39
39
  system(ENV['EDITOR'], file_path.to_s)
40
- elsif Aspera::Environment.os.eql?(Aspera::Environment::OS_WINDOWS)
40
+ elsif Environment.os.eql?(Environment::OS_WINDOWS)
41
41
  system('notepad.exe', %Q{"#{file_path}"})
42
42
  else
43
43
  uri_graphical(file_path.to_s)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'json'
4
4
  require 'aspera/log'
5
+ require 'aspera/assert'
5
6
 
6
7
  module Aspera
7
8
  # Persist data on file system
@@ -13,21 +14,19 @@ module Aspera
13
14
  # @param :parse Optional parse method (default to JSON)
14
15
  # @param :format Optional dump method (default to JSON)
15
16
  # @param :merge Optional merge data from file to current data
16
- def initialize(options)
17
- Log.log.debug{"persistency: #{options}"}
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]
17
+ def initialize(manager:, data:, id:, delete: nil, parse: nil, format: nil, merge: nil)
18
+ Aspera.assert(!manager.nil?)
19
+ Aspera.assert(!data.nil?)
20
+ Aspera.assert_type(id, String)
21
+ Aspera.assert(!id.empty?)
22
+ @manager = manager
23
+ @persisted_object = data
24
+ @object_id = id
26
25
  # by default , at save time, file is deleted if data is nil
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}
26
+ @delete_condition = delete || lambda{|d|d.empty?}
27
+ @persist_format = format || lambda {|h| JSON.generate(h)}
28
+ persist_parse = parse || lambda {|t| JSON.parse(t)}
29
+ persist_merge = merge || lambda {|current, file| current.concat(file).uniq rescue current}
31
30
  value = @manager.get(@object_id)
32
31
  persist_merge.call(@persisted_object, persist_parse.call(value)) unless value.nil?
33
32
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'fileutils'
4
4
  require 'aspera/log'
5
+ require 'aspera/assert'
5
6
  require 'aspera/environment'
6
7
 
7
8
  # search: persistency_folder PersistencyFolder
@@ -34,7 +35,7 @@ module Aspera
34
35
  end
35
36
 
36
37
  def put(object_id, value)
37
- raise 'value: only String supported' unless value.is_a?(String)
38
+ Aspera.assert_type(value, String)
38
39
  persist_filepath = id_to_filepath(object_id)
39
40
  Log.log.debug{"persistency saving: #{persist_filepath}"}
40
41
  FileUtils.rm_f(persist_filepath)
@@ -67,7 +68,7 @@ module Aspera
67
68
 
68
69
  # @param object_id String or Array
69
70
  def id_to_filepath(object_id)
70
- raise 'object_id: only String supported' unless object_id.is_a?(String)
71
+ Aspera.assert_type(object_id, String)
71
72
  FileUtils.mkdir_p(@folder)
72
73
  Environment.restrict_file_access(@folder)
73
74
  return File.join(@folder, "#{object_id}#{FILE_SUFFIX}")