aspera-cli 4.24.1 → 4.25.0.pre
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +1064 -745
- data/CONTRIBUTING.md +43 -100
- data/README.md +1281 -720
- data/bin/ascli +20 -1
- data/bin/asession +23 -27
- data/lib/aspera/agent/base.rb +10 -21
- data/lib/aspera/agent/connect.rb +2 -3
- data/lib/aspera/agent/desktop.rb +2 -2
- data/lib/aspera/agent/direct.rb +49 -32
- data/lib/aspera/agent/factory.rb +31 -0
- data/lib/aspera/api/aoc.rb +134 -76
- data/lib/aspera/api/cos_node.rb +3 -2
- data/lib/aspera/api/faspex.rb +213 -0
- data/lib/aspera/api/node.rb +107 -94
- data/lib/aspera/ascmd.rb +1 -2
- data/lib/aspera/ascp/installation.rb +73 -58
- data/lib/aspera/ascp/management.rb +119 -23
- data/lib/aspera/assert.rb +39 -11
- data/lib/aspera/cli/error.rb +4 -2
- data/lib/aspera/cli/extended_value.rb +91 -67
- data/lib/aspera/cli/formatter.rb +62 -27
- data/lib/aspera/cli/hints.rb +8 -0
- data/lib/aspera/cli/info.rb +4 -4
- data/lib/aspera/cli/main.rb +76 -84
- data/lib/aspera/cli/manager.rb +352 -248
- data/lib/aspera/cli/plugins/alee.rb +5 -4
- data/lib/aspera/cli/plugins/aoc.rb +175 -195
- data/lib/aspera/cli/plugins/ats.rb +4 -4
- data/lib/aspera/cli/plugins/base.rb +343 -0
- data/lib/aspera/cli/plugins/basic_auth.rb +45 -0
- data/lib/aspera/cli/plugins/config.rb +283 -269
- data/lib/aspera/cli/plugins/console.rb +27 -22
- data/lib/aspera/cli/plugins/cos.rb +3 -3
- data/lib/aspera/cli/plugins/factory.rb +78 -0
- data/lib/aspera/cli/plugins/faspex.rb +49 -46
- data/lib/aspera/cli/plugins/faspex5.rb +113 -225
- data/lib/aspera/cli/plugins/faspio.rb +19 -18
- data/lib/aspera/cli/plugins/httpgw.rb +14 -13
- data/lib/aspera/cli/plugins/node.rb +162 -149
- data/lib/aspera/cli/plugins/oauth.rb +48 -0
- data/lib/aspera/cli/plugins/orchestrator.rb +129 -45
- data/lib/aspera/cli/plugins/preview.rb +30 -50
- data/lib/aspera/cli/plugins/server.rb +21 -21
- data/lib/aspera/cli/plugins/shares.rb +45 -47
- data/lib/aspera/cli/sync_actions.rb +50 -39
- data/lib/aspera/cli/transfer_agent.rb +35 -49
- data/lib/aspera/cli/transfer_progress.rb +6 -6
- data/lib/aspera/cli/version.rb +3 -3
- data/lib/aspera/cli/wizard.rb +70 -55
- data/lib/aspera/colors.rb +6 -0
- data/lib/aspera/command_line_builder.rb +59 -61
- data/lib/aspera/command_line_converter.rb +2 -1
- data/lib/aspera/coverage.rb +2 -2
- data/lib/aspera/data_repository.rb +1 -1
- data/lib/aspera/environment.rb +51 -41
- data/lib/aspera/faspex_gw.rb +7 -5
- data/lib/aspera/faspex_postproc.rb +1 -1
- data/lib/aspera/keychain/factory.rb +1 -2
- data/lib/aspera/keychain/macos_security.rb +1 -1
- data/lib/aspera/log.rb +37 -9
- data/lib/aspera/markdown.rb +31 -0
- data/lib/aspera/nagios.rb +7 -6
- data/lib/aspera/oauth/base.rb +25 -28
- data/lib/aspera/oauth/factory.rb +9 -9
- data/lib/aspera/oauth/url_json.rb +2 -1
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/preview/file_types.rb +23 -37
- data/lib/aspera/products/connect.rb +7 -6
- data/lib/aspera/products/desktop.rb +1 -4
- data/lib/aspera/products/other.rb +9 -1
- data/lib/aspera/products/transferd.rb +0 -1
- data/lib/aspera/rest.rb +168 -113
- data/lib/aspera/rest_error_analyzer.rb +4 -4
- data/lib/aspera/ssh.rb +7 -4
- data/lib/aspera/ssl.rb +41 -0
- data/lib/aspera/sync/args.schema.yaml +46 -3
- data/lib/aspera/sync/conf.schema.yaml +307 -123
- data/lib/aspera/sync/database.rb +2 -1
- data/lib/aspera/sync/operations.rb +135 -79
- data/lib/aspera/temp_file_manager.rb +17 -5
- data/lib/aspera/transfer/error.rb +16 -7
- data/lib/aspera/transfer/parameters.rb +35 -22
- data/lib/aspera/transfer/resumer.rb +74 -0
- data/lib/aspera/transfer/spec.rb +5 -5
- data/lib/aspera/transfer/spec.schema.yaml +170 -59
- data/lib/aspera/transfer/spec_doc.rb +49 -43
- data/lib/aspera/uri_reader.rb +2 -2
- data/lib/aspera/web_auth.rb +6 -6
- data/lib/transferd_pb.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +26 -11
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/basic_auth_plugin.rb +0 -43
- data/lib/aspera/cli/plugin.rb +0 -333
- data/lib/aspera/cli/plugin_factory.rb +0 -81
- data/lib/aspera/resumer.rb +0 -77
- data/lib/aspera/transfer/error_info.rb +0 -91
data/lib/aspera/api/aoc.rb
CHANGED
|
@@ -8,11 +8,10 @@ require 'aspera/data_repository'
|
|
|
8
8
|
require 'aspera/transfer/spec'
|
|
9
9
|
require 'aspera/api/node'
|
|
10
10
|
require 'base64'
|
|
11
|
-
require 'cgi'
|
|
12
11
|
|
|
13
12
|
module Aspera
|
|
14
13
|
module Api
|
|
15
|
-
class AoC <
|
|
14
|
+
class AoC < Rest
|
|
16
15
|
PRODUCT_NAME = 'Aspera on Cloud'
|
|
17
16
|
# use default workspace if it is set, else none
|
|
18
17
|
DEFAULT_WORKSPACE = ''
|
|
@@ -74,7 +73,7 @@ module Aspera
|
|
|
74
73
|
# split host of URL into organization and domain
|
|
75
74
|
def split_org_domain(uri)
|
|
76
75
|
Aspera.assert_type(uri, URI)
|
|
77
|
-
|
|
76
|
+
Aspera.assert(!uri.host.nil?){"No host found in URL. Please check URL format: https://myorg.#{SAAS_DOMAIN_PROD}"}
|
|
78
77
|
parts = uri.host.split('.', 2)
|
|
79
78
|
Aspera.assert(parts.length == 2){"expecting a public FQDN for #{PRODUCT_NAME}"}
|
|
80
79
|
parts[0] = nil if parts[0].eql?('api')
|
|
@@ -90,7 +89,7 @@ module Aspera
|
|
|
90
89
|
# @param url [String] URL of AoC public link
|
|
91
90
|
# @return [Hash] information about public link, or nil if not a public link
|
|
92
91
|
def link_info(url)
|
|
93
|
-
final_uri = Rest.new(base_url: url, redirect_max: MAX_AOC_URL_REDIRECT).call(operation: 'GET')
|
|
92
|
+
final_uri = Rest.new(base_url: url, redirect_max: MAX_AOC_URL_REDIRECT).call(operation: 'GET', ret: :resp).uri
|
|
94
93
|
Log.dump(:final_uri, final_uri, level: :trace1)
|
|
95
94
|
org_domain = split_org_domain(final_uri)
|
|
96
95
|
if (m = final_uri.path.match(%r{/oauth2/([^/]+)/login$}))
|
|
@@ -98,7 +97,7 @@ module Aspera
|
|
|
98
97
|
else
|
|
99
98
|
Log.log.debug{"path=#{final_uri.path} does not end with /login"}
|
|
100
99
|
end
|
|
101
|
-
|
|
100
|
+
Aspera.assert(!final_uri.query.nil?, 'AoC shall redirect to login page with a query', type: Error)
|
|
102
101
|
query = Rest.query_to_h(final_uri.query)
|
|
103
102
|
Log.dump(:query, query, level: :trace1)
|
|
104
103
|
# is that a public link ?
|
|
@@ -140,15 +139,82 @@ module Aspera
|
|
|
140
139
|
organization: org_domain[:organization]
|
|
141
140
|
}
|
|
142
141
|
end
|
|
142
|
+
|
|
143
|
+
# Call block with same query using paging and response information.
|
|
144
|
+
# Block must return an Array with data and http response
|
|
145
|
+
# @return [Hash] {items: , total: }
|
|
146
|
+
def call_paging(query: nil, formatter: nil)
|
|
147
|
+
query = {} if query.nil?
|
|
148
|
+
Aspera.assert_type(query, Hash){'query'}
|
|
149
|
+
Aspera.assert(block_given?)
|
|
150
|
+
# set default large page if user does not specify own parameters. AoC Caps to 1000 anyway
|
|
151
|
+
query['per_page'] = 1000 unless query.key?('per_page')
|
|
152
|
+
max_items = query.delete(Rest::MAX_ITEMS)
|
|
153
|
+
max_pages = query.delete(Rest::MAX_PAGES)
|
|
154
|
+
item_list = []
|
|
155
|
+
total_count = nil
|
|
156
|
+
current_page = query['page']
|
|
157
|
+
current_page = 1 if current_page.nil?
|
|
158
|
+
page_count = 0
|
|
159
|
+
loop do
|
|
160
|
+
new_query = query.clone
|
|
161
|
+
new_query['page'] = current_page
|
|
162
|
+
result_data, result_http = yield(new_query)
|
|
163
|
+
Aspera.assert(result_http)
|
|
164
|
+
total_count = result_http['X-Total-Count']&.to_i
|
|
165
|
+
page_count += 1
|
|
166
|
+
current_page += 1
|
|
167
|
+
add_items = result_data
|
|
168
|
+
break if add_items.nil?
|
|
169
|
+
break if add_items.empty?
|
|
170
|
+
# append new items to full list
|
|
171
|
+
item_list += add_items
|
|
172
|
+
break if !max_items.nil? && item_list.count >= max_items
|
|
173
|
+
break if !max_pages.nil? && page_count >= max_pages
|
|
174
|
+
break if total_count&.<=(item_list.count)
|
|
175
|
+
formatter&.long_operation_running("#{item_list.count} / #{total_count}") unless total_count.eql?(item_list.count.to_s)
|
|
176
|
+
end
|
|
177
|
+
formatter&.long_operation_terminated
|
|
178
|
+
item_list = item_list[0..max_items - 1] if !max_items.nil? && item_list.count > max_items
|
|
179
|
+
return {items: item_list, total: total_count}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @param id [String] Identifier or workspace
|
|
183
|
+
# @return [Hash] suitable for permission filtering
|
|
184
|
+
def workspace_access(id)
|
|
185
|
+
{
|
|
186
|
+
'access_type' => 'user',
|
|
187
|
+
'access_id' => "#{ID_AK_ADMIN}_WS_#{id}"
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @param permission [Hash] Shared folder information
|
|
192
|
+
# @return [Boolean] `true` if internal access
|
|
193
|
+
def workspace_access?(permission)
|
|
194
|
+
permission['access_id'].start_with?("#{ID_AK_ADMIN}_WS_")
|
|
195
|
+
end
|
|
143
196
|
end
|
|
144
197
|
|
|
145
198
|
attr_reader :private_link
|
|
146
199
|
|
|
147
|
-
def initialize(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
200
|
+
def initialize(
|
|
201
|
+
url:,
|
|
202
|
+
auth:,
|
|
203
|
+
subpath: API_V1,
|
|
204
|
+
client_id: nil,
|
|
205
|
+
client_secret: nil,
|
|
206
|
+
scope: nil,
|
|
207
|
+
redirect_uri: nil,
|
|
208
|
+
private_key: nil,
|
|
209
|
+
passphrase: nil,
|
|
210
|
+
username: nil,
|
|
211
|
+
password: nil,
|
|
212
|
+
workspace: nil,
|
|
213
|
+
secret_finder: nil
|
|
214
|
+
)
|
|
215
|
+
# Test here because link may set url
|
|
216
|
+
Aspera.assert(url, 'Missing mandatory option: url', type: ParameterError)
|
|
217
|
+
Aspera.assert(scope, 'Missing mandatory option: scope', type: ParameterError)
|
|
152
218
|
# default values for client id
|
|
153
219
|
client_id, client_secret = self.class.get_client_info if client_id.nil?
|
|
154
220
|
# access key secrets are provided out of band to get node api access
|
|
@@ -173,22 +239,21 @@ module Aspera
|
|
|
173
239
|
auth_params[:grant_method] = if url_info.key?(:token)
|
|
174
240
|
:url_json
|
|
175
241
|
else
|
|
176
|
-
|
|
242
|
+
Aspera.assert(auth, 'Missing mandatory option: auth', type: ParameterError)
|
|
177
243
|
auth
|
|
178
244
|
end
|
|
179
245
|
# this is the base API url
|
|
180
246
|
api_url_base = self.class.api_base_url(api_domain: url_info[:instance_domain])
|
|
181
247
|
# auth URL
|
|
182
248
|
auth_params[:base_url] = "#{api_url_base}/#{OAUTH_API_SUBPATH}/#{url_info[:organization]}"
|
|
183
|
-
|
|
184
249
|
# fill other auth parameters based on OAuth method
|
|
185
250
|
case auth_params[:grant_method]
|
|
186
251
|
when :web
|
|
187
|
-
|
|
252
|
+
Aspera.assert(redirect_uri, 'Missing mandatory option: redirect_uri', type: ParameterError)
|
|
188
253
|
auth_params[:redirect_uri] = redirect_uri
|
|
189
254
|
when :jwt
|
|
190
|
-
|
|
191
|
-
|
|
255
|
+
Aspera.assert(private_key, 'Missing mandatory option: private_key', type: ParameterError)
|
|
256
|
+
Aspera.assert(username, 'Missing mandatory option: username', type: ParameterError)
|
|
192
257
|
auth_params[:private_key_obj] = OpenSSL::PKey::RSA.new(private_key, passphrase)
|
|
193
258
|
auth_params[:payload] = {
|
|
194
259
|
iss: auth_params[:client_id], # issuer
|
|
@@ -205,7 +270,7 @@ module Aspera
|
|
|
205
270
|
auth_params[:json][:password] = password unless password.nil?
|
|
206
271
|
# basic auth required for /token
|
|
207
272
|
auth_params[:auth] = {type: :basic, username: auth_params[:client_id], password: auth_params[:client_secret]}
|
|
208
|
-
else Aspera.error_unexpected_value(auth_params[:grant_method])
|
|
273
|
+
else Aspera.error_unexpected_value(auth_params[:grant_method]){'auth, use one of: :web, :jwt'}
|
|
209
274
|
end
|
|
210
275
|
super(
|
|
211
276
|
base_url: "#{api_url_base}/#{subpath}",
|
|
@@ -213,12 +278,12 @@ module Aspera
|
|
|
213
278
|
)
|
|
214
279
|
end
|
|
215
280
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
281
|
+
# read using the query and paging
|
|
282
|
+
# @return [Hash] {items: , total: }
|
|
283
|
+
def read_with_paging(subpath, query = nil, formatter: nil)
|
|
284
|
+
return self.class.call_paging(query: query, formatter: formatter) do |paged_query|
|
|
285
|
+
read(subpath, query: paged_query, ret: :both)
|
|
286
|
+
end
|
|
222
287
|
end
|
|
223
288
|
|
|
224
289
|
def assert_public_link_types(expected)
|
|
@@ -230,7 +295,17 @@ module Aspera
|
|
|
230
295
|
return [] # TODO : public_link['id'] ?
|
|
231
296
|
end
|
|
232
297
|
|
|
233
|
-
#
|
|
298
|
+
# Cached public link information
|
|
299
|
+
# @return [Hash, nil] token info if public link or nil
|
|
300
|
+
def public_link
|
|
301
|
+
return unless auth_params[:grant_method].eql?(:url_json)
|
|
302
|
+
return @cache_url_token_info unless @cache_url_token_info.nil?
|
|
303
|
+
# TODO: can there be several in list ?
|
|
304
|
+
@cache_url_token_info = read('url_tokens').first
|
|
305
|
+
return @cache_url_token_info
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Cached user information
|
|
234
309
|
def current_user_info(exception: false)
|
|
235
310
|
return @cache_user_info unless @cache_user_info.nil?
|
|
236
311
|
# get our user's default information
|
|
@@ -246,21 +321,9 @@ module Aspera
|
|
|
246
321
|
return @cache_user_info
|
|
247
322
|
end
|
|
248
323
|
|
|
324
|
+
# Cached workspace information
|
|
249
325
|
def workspace
|
|
250
|
-
|
|
251
|
-
@workspace_info
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def home
|
|
255
|
-
Aspera.assert(!@home_info.nil?){'AoC home context is not set'}
|
|
256
|
-
@home_info
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
# Set the application context
|
|
260
|
-
# @param application [Symbol,NilClass] :files or :packages
|
|
261
|
-
# @return [Hash] current context information: workspace, and home node/file if app is "Files"
|
|
262
|
-
def context=(application)
|
|
263
|
-
Aspera.assert_values(application, %i[files packages])
|
|
326
|
+
return @workspace_info unless @workspace_info.nil?
|
|
264
327
|
ws_id =
|
|
265
328
|
if !public_link.nil?
|
|
266
329
|
Log.log.debug('Using workspace of public link')
|
|
@@ -278,26 +341,21 @@ module Aspera
|
|
|
278
341
|
else
|
|
279
342
|
lookup_by_name('workspaces', @workspace_name)['id']
|
|
280
343
|
end
|
|
281
|
-
ws_info =
|
|
282
|
-
if ws_id.nil?
|
|
283
|
-
nil
|
|
284
|
-
else
|
|
285
|
-
read("workspaces/#{ws_id}")
|
|
286
|
-
end
|
|
287
344
|
@workspace_info =
|
|
288
|
-
if
|
|
345
|
+
if ws_id.nil?
|
|
289
346
|
{
|
|
290
|
-
|
|
291
|
-
name: "Shared #{application}"
|
|
347
|
+
name: 'Shared (no workspace)'
|
|
292
348
|
}
|
|
293
349
|
else
|
|
294
|
-
{
|
|
295
|
-
id: ws_info['id'],
|
|
296
|
-
name: ws_info['name']
|
|
297
|
-
}
|
|
350
|
+
read("workspaces/#{ws_id}").slice('id', 'name', 'home_node_id', 'home_file_id').symbolize_keys
|
|
298
351
|
end
|
|
299
|
-
Log.dump(:
|
|
300
|
-
|
|
352
|
+
Log.dump(:workspace_info, @workspace_info)
|
|
353
|
+
@workspace_info
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Cached Home information for current user in Files app
|
|
357
|
+
def home
|
|
358
|
+
return @home_info unless @home_info.nil?
|
|
301
359
|
@home_info =
|
|
302
360
|
if !public_link.nil?
|
|
303
361
|
assert_public_link_types(['view_shared_file'])
|
|
@@ -310,10 +368,10 @@ module Aspera
|
|
|
310
368
|
node_id: private_link[:node_id],
|
|
311
369
|
file_id: private_link[:file_id]
|
|
312
370
|
}
|
|
313
|
-
elsif
|
|
371
|
+
elsif workspace[:home_node_id] && workspace[:home_file_id]
|
|
314
372
|
{
|
|
315
|
-
node_id:
|
|
316
|
-
file_id:
|
|
373
|
+
node_id: workspace[:home_node_id],
|
|
374
|
+
file_id: workspace[:home_file_id]
|
|
317
375
|
}
|
|
318
376
|
else
|
|
319
377
|
# not part of any workspace, but has some folder shared
|
|
@@ -323,8 +381,9 @@ module Aspera
|
|
|
323
381
|
file_id: user_info['read_only_home_file_id']
|
|
324
382
|
}
|
|
325
383
|
end
|
|
326
|
-
|
|
384
|
+
Aspera.assert(!@home_info[:node_id].to_s.empty?, "Cannot get user's home node id, check your default workspace or specify one", type: Error)
|
|
327
385
|
Log.dump(:context, @home_info)
|
|
386
|
+
@home_info
|
|
328
387
|
end
|
|
329
388
|
|
|
330
389
|
# @param node_id [String] identifier of node in AoC
|
|
@@ -393,37 +452,38 @@ module Aspera
|
|
|
393
452
|
end
|
|
394
453
|
meta_schema.each do |field|
|
|
395
454
|
provided = pkg_meta.select{ |i| i['name'].eql?(field['name'])}
|
|
396
|
-
|
|
397
|
-
|
|
455
|
+
Aspera.assert(provided.count <= 1, type: Error){"only one field with name #{field['name']} allowed"}
|
|
456
|
+
Aspera.assert(!provided.empty?, type: Error){"missing mandatory field: #{field['name']}"} if field['required']
|
|
398
457
|
end
|
|
399
458
|
end
|
|
400
459
|
|
|
401
460
|
# Normalize package creation recipient lists as expected by AoC API
|
|
402
461
|
# AoC expects {type: , id: }, but ascli allows providing either the native values or just a name
|
|
403
462
|
# in that case, the name is resolved and replaced with {type: , id: }
|
|
404
|
-
# @param package_data
|
|
405
|
-
# @param
|
|
406
|
-
# @
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
463
|
+
# @param package_data [Hash] The whole package creation payload
|
|
464
|
+
# @param rcpt_lst_field [String] The field in structure, i.e. recipients or bcc_recipients
|
|
465
|
+
# @param new_user_option [Hash] Additionnal fields for contact creation
|
|
466
|
+
# @return nil, `package_data` is modified
|
|
467
|
+
def resolve_package_recipients(package_data, rcpt_lst_field, new_user_option)
|
|
468
|
+
return unless package_data.key?(rcpt_lst_field)
|
|
469
|
+
Aspera.assert_type(package_data[rcpt_lst_field], Array){rcpt_lst_field}
|
|
410
470
|
new_user_option = {'package_contact' => true} if new_user_option.nil?
|
|
411
471
|
Aspera.assert_type(new_user_option, Hash){'new_user_option'}
|
|
472
|
+
ws_id = package_data['workspace_id']
|
|
412
473
|
# list with resolved elements
|
|
413
474
|
resolved_list = []
|
|
414
|
-
package_data[
|
|
475
|
+
package_data[rcpt_lst_field].each do |short_recipient_info|
|
|
415
476
|
case short_recipient_info
|
|
416
477
|
when Hash # native API information, check keys
|
|
417
|
-
Aspera.assert(short_recipient_info.keys.sort.eql?(%w[id type])){"#{
|
|
478
|
+
Aspera.assert(short_recipient_info.keys.sort.eql?(%w[id type])){"#{rcpt_lst_field} element shall have fields: id and type"}
|
|
418
479
|
when String # CLI helper: need to resolve provided name to type/id
|
|
419
480
|
# email: user, else dropbox
|
|
420
481
|
entity_type = short_recipient_info.include?('@') ? 'contacts' : 'dropboxes'
|
|
421
482
|
begin
|
|
422
483
|
full_recipient_info = lookup_by_name(entity_type, short_recipient_info, query: {'current_workspace_id' => ws_id})
|
|
423
|
-
rescue
|
|
424
|
-
raise e unless e.message.start_with?(ENTITY_NOT_FOUND)
|
|
484
|
+
rescue EntityNotFound
|
|
425
485
|
# dropboxes cannot be created on the fly
|
|
426
|
-
|
|
486
|
+
Aspera.assert_values(entity_type, 'contacts', type: Error){"No such shared inbox in workspace #{ws_id}"}
|
|
427
487
|
# unknown user: create it as external user
|
|
428
488
|
full_recipient_info = create('contacts', {
|
|
429
489
|
'current_workspace_id' => ws_id,
|
|
@@ -435,14 +495,13 @@ module Aspera
|
|
|
435
495
|
else
|
|
436
496
|
{'id' => full_recipient_info['source_id'], 'type' => full_recipient_info['source_type']}
|
|
437
497
|
end
|
|
438
|
-
else #
|
|
439
|
-
raise "#{recipient_list_field} item must be a String (email, shared inbox) or Hash (id,type)"
|
|
498
|
+
else Aspera.error_unexpected_value(short_recipient_info.class.name){"#{rcpt_lst_field} item must be a String (email, shared inbox) or Hash (id,type)"}
|
|
440
499
|
end
|
|
441
500
|
# add original or resolved recipient info
|
|
442
501
|
resolved_list.push(short_recipient_info)
|
|
443
502
|
end
|
|
444
503
|
# replace with resolved elements
|
|
445
|
-
package_data[
|
|
504
|
+
package_data[rcpt_lst_field] = resolved_list
|
|
446
505
|
return
|
|
447
506
|
end
|
|
448
507
|
|
|
@@ -475,8 +534,8 @@ module Aspera
|
|
|
475
534
|
# package_data['file_names']||=[..list of filenames to transfer...]
|
|
476
535
|
|
|
477
536
|
# lookup users
|
|
478
|
-
resolve_package_recipients(package_data,
|
|
479
|
-
resolve_package_recipients(package_data,
|
|
537
|
+
resolve_package_recipients(package_data, 'recipients', new_user_option)
|
|
538
|
+
resolve_package_recipients(package_data, 'bcc_recipients', new_user_option)
|
|
480
539
|
|
|
481
540
|
validate_metadata(package_data) if validate_meta
|
|
482
541
|
|
|
@@ -592,8 +651,7 @@ module Aspera
|
|
|
592
651
|
when NilClass
|
|
593
652
|
when ''
|
|
594
653
|
# workspace shared folder
|
|
595
|
-
perm_data[
|
|
596
|
-
perm_data['access_id'] = "#{ID_AK_ADMIN}_WS_#{app_info[:workspace_id]}"
|
|
654
|
+
perm_data.merge!(self.class.workspace_access(app_info[:workspace_id]))
|
|
597
655
|
tag_workspace['shared_with_name'] = perm_data['access_id']
|
|
598
656
|
else
|
|
599
657
|
entity_info = lookup_by_name('contacts', shared_with, query: {'current_workspace_id' => app_info[:workspace_id]})
|
data/lib/aspera/api/cos_node.rb
CHANGED
|
@@ -55,8 +55,9 @@ module Aspera
|
|
|
55
55
|
operation: 'GET',
|
|
56
56
|
subpath: bucket,
|
|
57
57
|
headers: {'Accept' => 'application/xml'},
|
|
58
|
-
query: {'faspConnectionInfo' => nil}
|
|
59
|
-
|
|
58
|
+
query: {'faspConnectionInfo' => nil},
|
|
59
|
+
ret: :resp
|
|
60
|
+
).body
|
|
60
61
|
ats_info = XmlSimple.xml_in(xml_result_text, {'ForceArray' => false})
|
|
61
62
|
Log.dump(:ats_info, ats_info)
|
|
62
63
|
@storage_credentials = {
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/rest'
|
|
4
|
+
require 'aspera/oauth/base'
|
|
5
|
+
require 'digest'
|
|
6
|
+
|
|
7
|
+
module Aspera
|
|
8
|
+
# Implement OAuth for Faspex public link
|
|
9
|
+
class FaspexPubLink < OAuth::Base
|
|
10
|
+
class << self
|
|
11
|
+
attr_accessor :additional_info
|
|
12
|
+
end
|
|
13
|
+
# @param context The `context` query parameter in public link
|
|
14
|
+
# @param redirect_uri URI of web UI login
|
|
15
|
+
# @param path_authorize Path to provide passcode
|
|
16
|
+
def initialize(
|
|
17
|
+
context:,
|
|
18
|
+
redirect_uri:,
|
|
19
|
+
path_authorize: 'authorize_public_link',
|
|
20
|
+
**base_params
|
|
21
|
+
)
|
|
22
|
+
# a unique identifier could also be the passcode inside
|
|
23
|
+
super(**base_params, cache_ids: [Digest::SHA256.hexdigest(context)[0..23]])
|
|
24
|
+
@context = context
|
|
25
|
+
@redirect_uri = redirect_uri
|
|
26
|
+
@path_authorize = path_authorize
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_token
|
|
30
|
+
# Exchange context (passcode) for code
|
|
31
|
+
http = api.call(
|
|
32
|
+
operation: 'GET',
|
|
33
|
+
subpath: @path_authorize,
|
|
34
|
+
query: {
|
|
35
|
+
response_type: :code,
|
|
36
|
+
state: @context,
|
|
37
|
+
client_id: client_id,
|
|
38
|
+
redirect_uri: @redirect_uri
|
|
39
|
+
},
|
|
40
|
+
exception: false,
|
|
41
|
+
ret: :resp
|
|
42
|
+
)
|
|
43
|
+
# code / state located in redirected URL query
|
|
44
|
+
info = Rest.query_to_h(URI.parse(http['Location']).query)
|
|
45
|
+
Log.dump(:info, info)
|
|
46
|
+
raise Error, info['action_message'] if info['action_message']
|
|
47
|
+
Aspera.assert(info['code']){'Missing code in answer'}
|
|
48
|
+
# Exchange code for token
|
|
49
|
+
return create_token_call(optional_scope_client_id.merge(
|
|
50
|
+
grant_type: 'authorization_code',
|
|
51
|
+
code: info['code'],
|
|
52
|
+
redirect_uri: @redirect_uri
|
|
53
|
+
))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
OAuth::Factory.instance.register_token_creator(FaspexPubLink)
|
|
57
|
+
module Api
|
|
58
|
+
class Faspex < Aspera::Rest
|
|
59
|
+
# endpoint for authentication API
|
|
60
|
+
PATH_AUTH = 'auth'
|
|
61
|
+
PATH_API_V5 = 'api/v5'
|
|
62
|
+
PATH_HEALTH = 'configuration/ping'
|
|
63
|
+
private_constant :PATH_API_V5,
|
|
64
|
+
:PATH_HEALTH,
|
|
65
|
+
:PATH_AUTH
|
|
66
|
+
RECIPIENT_TYPES = %w[user workgroup external_user distribution_list shared_inbox].freeze
|
|
67
|
+
PACKAGE_TERMINATED = %w[completed failed].freeze
|
|
68
|
+
# list of supported mailbox types (to list packages)
|
|
69
|
+
API_LIST_MAILBOX_TYPES = %w[inbox inbox_history inbox_all inbox_all_history outbox outbox_history pending pending_history all].freeze
|
|
70
|
+
# PACKAGE_SEND_FROM_REMOTE_SOURCE = 'remote_source'
|
|
71
|
+
# Faspex API v5: get transfer spec for connect
|
|
72
|
+
TRANSFER_CONNECT = 'connect'
|
|
73
|
+
ADMIN_RESOURCES = %i[
|
|
74
|
+
accounts distribution_lists contacts jobs workgroups shared_inboxes nodes oauth_clients registrations saml_configs
|
|
75
|
+
metadata_profiles email_notifications alternate_addresses webhooks
|
|
76
|
+
].freeze
|
|
77
|
+
# states for jobs not in final state
|
|
78
|
+
JOB_RUNNING = %w[queued working].freeze
|
|
79
|
+
PATH_STANDARD_ROOT = '/aspera/faspex'
|
|
80
|
+
PATH_API_DETECT = "#{PATH_API_V5}/#{PATH_HEALTH}"
|
|
81
|
+
HEADER_ITERATION_TOKEN = 'X-Aspera-Next-Iteration-Token'
|
|
82
|
+
HEADER_FASPEX_VERSION = 'X-IBM-Aspera'
|
|
83
|
+
EMAIL_NOTIF_LIST = %w[
|
|
84
|
+
welcome_email
|
|
85
|
+
forgot_password
|
|
86
|
+
package_received
|
|
87
|
+
package_received_cc
|
|
88
|
+
package_sent_cc
|
|
89
|
+
package_downloaded
|
|
90
|
+
package_downloaded_cc
|
|
91
|
+
workgroup_package
|
|
92
|
+
upload_result
|
|
93
|
+
upload_result_cc
|
|
94
|
+
relay_started_cc
|
|
95
|
+
relay_finished_cc
|
|
96
|
+
relay_error_cc
|
|
97
|
+
shared_inbox_invitation
|
|
98
|
+
shared_inbox_submit
|
|
99
|
+
personal_invitation
|
|
100
|
+
personal_submit
|
|
101
|
+
account_approved
|
|
102
|
+
account_denied
|
|
103
|
+
package_file_processing_failed_sender
|
|
104
|
+
package_file_processing_failed_recipient
|
|
105
|
+
relay_failed_admin
|
|
106
|
+
relay_failed
|
|
107
|
+
admin_sync_failed
|
|
108
|
+
sync_failed
|
|
109
|
+
account_exist
|
|
110
|
+
mfa_code
|
|
111
|
+
]
|
|
112
|
+
class << self
|
|
113
|
+
# @return true if the URL is a public link
|
|
114
|
+
def public_link?(url)
|
|
115
|
+
url.include?('?context=')
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
attr_reader :pub_link_context
|
|
119
|
+
|
|
120
|
+
def initialize(
|
|
121
|
+
url:,
|
|
122
|
+
auth:,
|
|
123
|
+
password: nil,
|
|
124
|
+
client_id: nil,
|
|
125
|
+
client_secret: nil,
|
|
126
|
+
redirect_uri: nil,
|
|
127
|
+
username: nil,
|
|
128
|
+
private_key: nil,
|
|
129
|
+
passphrase: nil
|
|
130
|
+
)
|
|
131
|
+
auth = :public_link if self.class.public_link?(url)
|
|
132
|
+
@pub_link_context = nil
|
|
133
|
+
super(**
|
|
134
|
+
case auth
|
|
135
|
+
when :public_link
|
|
136
|
+
# Get URL of final redirect of public link
|
|
137
|
+
redir_url = Rest.new(base_url: url, redirect_max: 3).call(operation: 'GET', ret: :resp).uri.to_s
|
|
138
|
+
Log.dump(:redir_url, redir_url, level: :trace1)
|
|
139
|
+
# get context from query
|
|
140
|
+
encoded_context = Rest.query_to_h(URI.parse(redir_url).query)['context']
|
|
141
|
+
raise ParameterError, 'Bad faspex5 public link, missing context in query' if encoded_context.nil?
|
|
142
|
+
# public link information (contains passcode and allowed usage)
|
|
143
|
+
@pub_link_context = JSON.parse(Base64.decode64(encoded_context))
|
|
144
|
+
Log.dump(:pub_link_context, @pub_link_context, level: :trace1)
|
|
145
|
+
# Get the base url, i.e. .../aspera/faspex
|
|
146
|
+
base_url = redir_url.gsub(%r{/public/.*}, '').gsub(/\?.*/, '')
|
|
147
|
+
# Get web UI client_id and redirect_uri
|
|
148
|
+
# TODO: change this for something more reliable
|
|
149
|
+
config = JSON.parse(Rest.new(base_url: "#{base_url}/config.js", redirect_max: 3).call(operation: 'GET').sub(/^[^=]+=/, '').gsub(/([a-z_]+):/, '"\1":').delete("\n ").tr("'", '"')).symbolize_keys
|
|
150
|
+
Log.dump(:configjs, config)
|
|
151
|
+
{
|
|
152
|
+
base_url: "#{base_url}/#{PATH_API_V5}",
|
|
153
|
+
auth: {
|
|
154
|
+
type: :oauth2,
|
|
155
|
+
base_url: "#{base_url}/#{PATH_AUTH}",
|
|
156
|
+
grant_method: :faspex_pub_link,
|
|
157
|
+
context: encoded_context,
|
|
158
|
+
client_id: config[:client_id],
|
|
159
|
+
redirect_uri: config[:redirect_uri]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
# old: headers: {'Passcode' => @pub_link_context['passcode']}
|
|
163
|
+
when :boot
|
|
164
|
+
Aspera.assert(password, type: ParameterError){'Missing password'}
|
|
165
|
+
# the password here is the token copied directly from browser in developer mode
|
|
166
|
+
{
|
|
167
|
+
base_url: "#{url}/#{PATH_API_V5}",
|
|
168
|
+
headers: {'Authorization' => password}
|
|
169
|
+
}
|
|
170
|
+
when :web
|
|
171
|
+
Aspera.assert(client_id, type: ParameterError){'Missing client_id'}
|
|
172
|
+
Aspera.assert(redirect_uri, type: ParameterError){'Missing redirect_uri'}
|
|
173
|
+
# opens a browser and ask user to auth using web
|
|
174
|
+
{
|
|
175
|
+
base_url: "#{url}/#{PATH_API_V5}",
|
|
176
|
+
auth: {
|
|
177
|
+
type: :oauth2,
|
|
178
|
+
base_url: "#{url}/#{PATH_AUTH}",
|
|
179
|
+
grant_method: :web,
|
|
180
|
+
client_id: client_id,
|
|
181
|
+
redirect_uri: redirect_uri
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
when :jwt
|
|
185
|
+
Aspera.assert(client_id, type: ParameterError){'Missing client_id'}
|
|
186
|
+
Aspera.assert(private_key, type: ParameterError){'Missing private_key'}
|
|
187
|
+
{
|
|
188
|
+
base_url: "#{url}/#{PATH_API_V5}",
|
|
189
|
+
auth: {
|
|
190
|
+
type: :oauth2,
|
|
191
|
+
base_url: "#{url}/#{PATH_AUTH}",
|
|
192
|
+
grant_method: :jwt,
|
|
193
|
+
client_id: client_id,
|
|
194
|
+
payload: {
|
|
195
|
+
iss: client_id, # issuer
|
|
196
|
+
aud: client_id, # audience (this field is not clear...)
|
|
197
|
+
sub: "user:#{username}" # subject is a user
|
|
198
|
+
},
|
|
199
|
+
private_key_obj: OpenSSL::PKey::RSA.new(private_key, passphrase),
|
|
200
|
+
headers: {typ: 'JWT'}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else Aspera.error_unexpected_value(auth, type: ParameterError){'auth'}
|
|
204
|
+
end
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def auth_api
|
|
209
|
+
Rest.new(**params, base_url: base_url.sub(PATH_API_V5, PATH_AUTH))
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|