aspera-cli 4.16.0 → 4.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +50 -19
  4. data/CONTRIBUTING.md +3 -1
  5. data/README.md +965 -793
  6. data/bin/asession +29 -21
  7. data/lib/aspera/{fasp/agent_alpha.rb → agent/alpha.rb} +26 -25
  8. data/lib/aspera/{fasp/agent_base.rb → agent/base.rb} +15 -12
  9. data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
  10. data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +49 -53
  11. data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +20 -19
  12. data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +20 -33
  13. data/lib/aspera/{fasp/agent_trsdk.rb → agent/trsdk.rb} +11 -11
  14. data/lib/aspera/api/aoc.rb +586 -0
  15. data/lib/aspera/api/ats.rb +46 -0
  16. data/lib/aspera/api/cos_node.rb +95 -0
  17. data/lib/aspera/api/node.rb +344 -0
  18. data/lib/aspera/ascmd.rb +46 -10
  19. data/lib/aspera/{fasp → ascp}/installation.rb +5 -5
  20. data/lib/aspera/{fasp → ascp}/management.rb +3 -8
  21. data/lib/aspera/{fasp → ascp}/products.rb +1 -1
  22. data/lib/aspera/assert.rb +30 -30
  23. data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
  24. data/lib/aspera/cli/extended_value.rb +1 -1
  25. data/lib/aspera/cli/formatter.rb +13 -13
  26. data/lib/aspera/cli/hints.rb +5 -5
  27. data/lib/aspera/cli/main.rb +35 -28
  28. data/lib/aspera/cli/manager.rb +25 -24
  29. data/lib/aspera/cli/plugin.rb +22 -15
  30. data/lib/aspera/cli/plugin_factory.rb +61 -0
  31. data/lib/aspera/cli/plugins/alee.rb +7 -7
  32. data/lib/aspera/cli/plugins/aoc.rb +83 -77
  33. data/lib/aspera/cli/plugins/ats.rb +32 -33
  34. data/lib/aspera/cli/plugins/bss.rb +3 -4
  35. data/lib/aspera/cli/plugins/config.rb +169 -186
  36. data/lib/aspera/cli/plugins/console.rb +8 -6
  37. data/lib/aspera/cli/plugins/cos.rb +19 -18
  38. data/lib/aspera/cli/plugins/faspex.rb +61 -54
  39. data/lib/aspera/cli/plugins/faspex5.rb +150 -103
  40. data/lib/aspera/cli/plugins/node.rb +68 -73
  41. data/lib/aspera/cli/plugins/orchestrator.rb +34 -44
  42. data/lib/aspera/cli/plugins/preview.rb +31 -31
  43. data/lib/aspera/cli/plugins/server.rb +31 -33
  44. data/lib/aspera/cli/plugins/shares.rb +13 -11
  45. data/lib/aspera/cli/sync_actions.rb +8 -8
  46. data/lib/aspera/cli/transfer_agent.rb +32 -19
  47. data/lib/aspera/cli/transfer_progress.rb +1 -1
  48. data/lib/aspera/cli/version.rb +1 -1
  49. data/lib/aspera/colors.rb +5 -0
  50. data/lib/aspera/command_line_builder.rb +14 -14
  51. data/lib/aspera/coverage.rb +1 -2
  52. data/lib/aspera/data_repository.rb +1 -1
  53. data/lib/aspera/environment.rb +2 -3
  54. data/lib/aspera/faspex_gw.rb +5 -6
  55. data/lib/aspera/faspex_postproc.rb +1 -1
  56. data/lib/aspera/id_generator.rb +2 -2
  57. data/lib/aspera/json_rpc.rb +5 -5
  58. data/lib/aspera/keychain/encrypted_hash.rb +6 -6
  59. data/lib/aspera/keychain/macos_security.rb +27 -22
  60. data/lib/aspera/log.rb +2 -2
  61. data/lib/aspera/nagios.rb +3 -3
  62. data/lib/aspera/node_simulator.rb +5 -6
  63. data/lib/aspera/oauth/base.rb +143 -0
  64. data/lib/aspera/oauth/factory.rb +124 -0
  65. data/lib/aspera/oauth/generic.rb +34 -0
  66. data/lib/aspera/oauth/jwt.rb +51 -0
  67. data/lib/aspera/oauth/url_json.rb +31 -0
  68. data/lib/aspera/oauth/web.rb +50 -0
  69. data/lib/aspera/oauth.rb +5 -331
  70. data/lib/aspera/open_application.rb +7 -7
  71. data/lib/aspera/persistency_action_once.rb +4 -4
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/generator.rb +5 -5
  74. data/lib/aspera/preview/terminal.rb +3 -2
  75. data/lib/aspera/preview/utils.rb +3 -3
  76. data/lib/aspera/proxy_auto_config.rb +4 -4
  77. data/lib/aspera/rest.rb +175 -144
  78. data/lib/aspera/rest_errors_aspera.rb +3 -3
  79. data/lib/aspera/resumer.rb +77 -0
  80. data/lib/aspera/ssh.rb +6 -1
  81. data/lib/aspera/{fasp → transfer}/error.rb +3 -3
  82. data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
  83. data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
  84. data/lib/aspera/{fasp → transfer}/parameters.rb +58 -89
  85. data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +18 -16
  86. data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
  87. data/lib/aspera/{fasp → transfer}/sync.rb +32 -32
  88. data/lib/aspera/{fasp → transfer}/uri.rb +9 -8
  89. data/lib/aspera/web_server_simple.rb +11 -3
  90. data.tar.gz.sig +0 -0
  91. metadata +36 -63
  92. metadata.gz.sig +0 -0
  93. data/lib/aspera/aoc.rb +0 -601
  94. data/lib/aspera/ats_api.rb +0 -47
  95. data/lib/aspera/cos_node.rb +0 -94
  96. data/lib/aspera/fasp/resume_policy.rb +0 -79
  97. data/lib/aspera/node.rb +0 -339
data/lib/aspera/aoc.rb DELETED
@@ -1,601 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'aspera/log'
4
- require 'aspera/assert'
5
- require 'aspera/rest'
6
- require 'aspera/hash_ext'
7
- require 'aspera/data_repository'
8
- require 'aspera/fasp/transfer_spec'
9
- require 'aspera/node'
10
- require 'base64'
11
- require 'cgi'
12
-
13
- Aspera::Oauth.register_token_creator(
14
- :aoc_pub_link,
15
- lambda{|o|
16
- o.api.call({
17
- operation: 'POST',
18
- subpath: o.generic_parameters[:path_token],
19
- headers: {'Accept' => 'application/json'},
20
- json_params: o.specific_parameters[:json],
21
- url_params: o.specific_parameters[:url].merge(scope: o.generic_parameters[:scope]) # scope is here because it changes over time (node)
22
- })
23
- },
24
- lambda { |oauth|
25
- return [oauth.specific_parameters.dig(:json, :url_token)]
26
- })
27
-
28
- module Aspera
29
- class AoC < Aspera::Rest
30
- PRODUCT_NAME = 'Aspera on Cloud'
31
- # Production domain of AoC
32
- PROD_DOMAIN = 'ibmaspera.com' # cspell:disable-line
33
- # to avoid infinite loop in pub link redirection
34
- MAX_AOC_URL_REDIRECT = 10
35
- CLIENT_ID_PREFIX = 'aspera.'
36
- # Well-known AoC globals client apps
37
- GLOBAL_CLIENT_APPS = DataRepository::ELEMENTS.select{|i|i.to_s.start_with?(CLIENT_ID_PREFIX)}.freeze
38
- # cookie prefix so that console can decode identity
39
- COOKIE_PREFIX_CONSOLE_AOC = 'aspera.aoc'
40
- # path in URL of public links
41
- PUBLIC_LINK_PATHS = %w[/packages/public/receive /packages/public/send /files/public /public/files /public/send].freeze
42
- JWT_AUDIENCE = 'https://api.asperafiles.com/api/v1/oauth2/token'
43
- OAUTH_API_SUBPATH = 'api/v1/oauth2'
44
- # minimum fields for user info if retrieval fails
45
- USER_INFO_FIELDS_MIN = %w[name email id default_workspace_id organization_id].freeze
46
- # types of events for shared folder creation
47
- # Node events: permission.created permission.modified permission.deleted
48
- PERMISSIONS_CREATED = ['permission.created'].freeze
49
- DEFAULT_WORKSPACE = ''
50
-
51
- private_constant :MAX_AOC_URL_REDIRECT,
52
- :GLOBAL_CLIENT_APPS,
53
- :COOKIE_PREFIX_CONSOLE_AOC,
54
- :PUBLIC_LINK_PATHS,
55
- :JWT_AUDIENCE,
56
- :OAUTH_API_SUBPATH,
57
- :USER_INFO_FIELDS_MIN,
58
- :PERMISSIONS_CREATED
59
-
60
- # various API scopes supported
61
- SCOPE_FILES_SELF = 'self'
62
- SCOPE_FILES_USER = 'user:all'
63
- SCOPE_FILES_ADMIN = 'admin:all'
64
- SCOPE_FILES_ADMIN_USER = 'admin-user:all'
65
- SCOPE_FILES_ADMIN_USER_USER = "#{SCOPE_FILES_ADMIN_USER}+#{SCOPE_FILES_USER}"
66
- FILES_APP = 'files'
67
- PACKAGES_APP = 'packages'
68
- API_V1 = 'api/v1'
69
-
70
- # class static methods
71
- class << self
72
- # strings /Applications/Aspera\ Drive.app/Contents/MacOS/AsperaDrive|grep -E '.{100}==$'|base64 --decode
73
- def get_client_info(client_name=nil)
74
- client_key = client_name.nil? ? GLOBAL_CLIENT_APPS.first : client_name.to_sym
75
- return client_key, DataRepository.instance.item(client_key)
76
- end
77
-
78
- # base API url depends on domain, which could be "qa.xxx"
79
- def api_base_url(organization: 'api', api_domain: PROD_DOMAIN)
80
- return "https://#{organization}.#{api_domain}"
81
- end
82
-
83
- def metering_api(entitlement_id, customer_id, api_domain=PROD_DOMAIN)
84
- return Rest.new({
85
- base_url: "#{api_base_url(api_domain: api_domain)}/metering/v1",
86
- headers: {'X-Aspera-Entitlement-Authorization' => Rest.basic_token(entitlement_id, customer_id)}
87
- })
88
- end
89
-
90
- # split host of http://myorg.asperafiles.com in org and domain
91
- def url_parts(uri)
92
- raise "No host found in URL.Please check URL format: https://myorg.#{PROD_DOMAIN}" if uri.host.nil?
93
- parts = uri.host.split('.', 2)
94
- assert(parts.length == 2){"expecting a public FQDN for #{PRODUCT_NAME}"}
95
- return parts
96
- end
97
-
98
- # @param url [String] URL of AoC public link
99
- # @return [Hash] information about public link, or nil if not a public link
100
- def link_info(url)
101
- final_uri = Rest.new({base_url: url, redirect_max: MAX_AOC_URL_REDIRECT}).read('')[:http].uri
102
- raise 'AoC shall redirect to login page' if final_uri.query.nil?
103
- decoded_query = Rest.decode_query(final_uri.query)
104
- # is that a public link ?
105
- if decoded_query.key?('token')
106
- Log.log.warn{"Unknown pub link path: #{final_uri.path}"} unless PUBLIC_LINK_PATHS.include?(final_uri.path)
107
- # ok we get it !
108
- return {
109
- instance_domain: url_parts(final_uri)[1],
110
- url: 'https://' + final_uri.host,
111
- token: decoded_query['token']
112
- }
113
- end
114
- Log.log.debug{"path=#{final_uri.path} does not end with /login"} unless final_uri.path.end_with?('/login')
115
- if decoded_query['state']
116
- # can be a private link
117
- state_uri = URI.parse(decoded_query['state'])
118
- if state_uri.query && decoded_query['redirect_uri']
119
- decoded_state = Rest.decode_query(state_uri.query)
120
- if decoded_state.key?('short_link_url')
121
- if (m = state_uri.path.match(%r{/files/workspaces/([0-9]+)/all/([0-9]+):([0-9]+)}))
122
- redirect_uri = URI.parse(decoded_query['redirect_uri'])
123
- parts = url_parts(redirect_uri)
124
- return {
125
- instance_domain: parts[1],
126
- organization: parts[0],
127
- url: 'https://' + redirect_uri.host,
128
- private_link: {
129
- workspace_id: m[1],
130
- node_id: m[2],
131
- file_id: m[3]
132
- }
133
- }
134
- end
135
- end
136
- end
137
- end
138
- parts = url_parts(URI.parse(url))
139
- return {
140
- instance_domain: parts[1],
141
- organization: parts[0]
142
- }
143
- end
144
- end # static methods
145
-
146
- attr_reader :private_link
147
-
148
- def initialize(subpath: API_V1, url:, auth:, client_id: nil, client_secret: nil, scope: nil, redirect_uri: nil, private_key: nil, passphrase: nil, username: nil,
149
- password: nil, workspace: nil, secret_finder: nil)
150
- # test here because link may set url
151
- raise ArgumentError, 'Missing mandatory option: url' if url.nil?
152
- raise ArgumentError, 'Missing mandatory option: scope' if scope.nil?
153
- # default values for client id
154
- client_id, client_secret = self.class.get_client_info if client_id.nil?
155
- # access key secrets are provided out of band to get node api access
156
- # key: access key
157
- # value: associated secret
158
- @secret_finder = secret_finder
159
- @workspace_name = workspace
160
- @cache_user_info = nil
161
- @cache_url_token_info = nil
162
- @context_cache = nil
163
- # init rest params
164
- aoc_rest_p = {auth: {type: :oauth2}}
165
- # shortcut to auth section
166
- aoc_auth_p = aoc_rest_p[:auth]
167
- # analyze type of url
168
- url_info = AoC.link_info(url)
169
- Log.log.debug{Log.dump(:url_info, url_info)}
170
- @private_link = url_info[:private_link]
171
- aoc_auth_p[:grant_method] = if url_info.key?(:token)
172
- :aoc_pub_link
173
- else
174
- raise ArgumentError, 'Missing mandatory option: auth' if auth.nil?
175
- auth
176
- end
177
- # this is the base API url
178
- api_url_base = self.class.api_base_url(api_domain: url_info[:instance_domain])
179
- # API URL, including subpath (version ...)
180
- aoc_rest_p[:base_url] = "#{api_url_base}/#{subpath}"
181
- # auth URL
182
- aoc_auth_p[:base_url] = "#{api_url_base}/#{OAUTH_API_SUBPATH}/#{url_info[:organization]}"
183
- aoc_auth_p[:client_id] = client_id
184
- aoc_auth_p[:client_secret] = client_secret
185
- aoc_auth_p[:scope] = scope
186
-
187
- # fill other auth parameters based on Oauth method
188
- case aoc_auth_p[:grant_method]
189
- when :web
190
- raise ArgumentError, 'Missing mandatory option: redirect_uri' if redirect_uri.nil?
191
- aoc_auth_p[:web] = {redirect_uri: redirect_uri}
192
- when :jwt
193
- raise ArgumentError, 'Missing mandatory option: private_key' if private_key.nil?
194
- raise ArgumentError, 'Missing mandatory option: username' if username.nil?
195
- aoc_auth_p[:jwt] = {
196
- private_key_obj: OpenSSL::PKey::RSA.new(private_key, passphrase),
197
- payload: {
198
- iss: aoc_auth_p[:client_id], # issuer
199
- sub: username, # subject
200
- aud: JWT_AUDIENCE
201
- }
202
- }
203
- # add jwt payload for global ids
204
- aoc_auth_p[:jwt][:payload][:org] = url_info[:organization] if GLOBAL_CLIENT_APPS.include?(aoc_auth_p[:client_id])
205
- when :aoc_pub_link
206
- aoc_auth_p[:aoc_pub_link] = {
207
- url: {grant_type: 'url_token'}, # URL arguments
208
- json: {url_token: url_info[:token]} # JSON body
209
- }
210
- # password protection of link
211
- aoc_auth_p[:aoc_pub_link][:json][:password] = password unless password.nil?
212
- # basic auth required for /token
213
- aoc_auth_p[:auth] = {type: :basic, username: aoc_auth_p[:client_id], password: aoc_auth_p[:client_secret]}
214
- else error_unexpected_value(aoc_auth_p[:grant_method])
215
- end
216
- super(aoc_rest_p)
217
- end
218
-
219
- def public_link
220
- return nil unless params[:auth][:grant_method].eql?(:aoc_pub_link)
221
- return @cache_url_token_info unless @cache_url_token_info.nil?
222
- # TODO: can there be several in list ?
223
- @cache_url_token_info = read('url_tokens')[:data].first
224
- return @cache_url_token_info
225
- end
226
-
227
- def assert_public_link_types(expected)
228
- assert_values(public_link['purpose'], expected){'public link type'}
229
- end
230
-
231
- def additional_persistence_ids
232
- return [current_user_info['id']] if public_link.nil?
233
- return [] # TODO : public_link['id'] ?
234
- end
235
-
236
- # cached user information
237
- def current_user_info(exception: false)
238
- return @cache_user_info unless @cache_user_info.nil?
239
- # get our user's default information
240
- @cache_user_info =
241
- begin
242
- read('self')[:data]
243
- rescue StandardError => e
244
- raise e if exception
245
- Log.log.debug{"ignoring error: #{e}"}
246
- {}
247
- end
248
- USER_INFO_FIELDS_MIN.each{|f|@cache_user_info[f] = nil if @cache_user_info[f].nil?}
249
- return @cache_user_info
250
- end
251
-
252
- # @param application [Symbol] :files or :packages
253
- # @return [Hash] current context information: workspace, and home node/file if app is "Files"
254
- def context(application = nil)
255
- return @context_cache unless @context_cache.nil?
256
- assert(!application.nil?){'application must be set once'}
257
- assert_values(application, %i[files packages])
258
- ws_id =
259
- if !public_link.nil?
260
- Log.log.debug('Using workspace of public link')
261
- public_link['data']['workspace_id']
262
- elsif !private_link.nil?
263
- Log.log.debug('Using workspace of private link')
264
- private_link[:workspace_id]
265
- elsif @workspace_name.eql?(DEFAULT_WORKSPACE)
266
- Log.log.debug('Using default workspace'.green)
267
- raise 'User does not have default workspace, please specify workspace' if current_user_info['default_workspace_id'].nil?
268
- current_user_info['default_workspace_id']
269
- elsif @workspace_name.nil?
270
- nil
271
- else
272
- lookup_by_name('workspaces', @workspace_name)['id']
273
- end
274
- ws_info =
275
- if ws_id.nil?
276
- nil
277
- else
278
- read("workspaces/#{ws_id}")[:data]
279
- end
280
- @context_cache = if ws_info.nil?
281
- {
282
- workspace_id: nil,
283
- workspace_name: 'Shared folders'
284
- }
285
- else
286
- {
287
- workspace_id: ws_info['id'],
288
- workspace_name: ws_info['name']
289
- }
290
- end
291
- return @context_cache unless application.eql?(:files)
292
- if !public_link.nil?
293
- assert_public_link_types(['view_shared_file'])
294
- @context_cache[:home_node_id] = public_link['data']['node_id']
295
- @context_cache[:home_file_id] = public_link['data']['file_id']
296
- elsif !private_link.nil?
297
- @context_cache[:home_node_id] = private_link[:node_id]
298
- @context_cache[:home_file_id] = private_link[:file_id]
299
- elsif ws_info
300
- @context_cache[:home_node_id] = ws_info['home_node_id']
301
- @context_cache[:home_file_id] = ws_info['home_file_id']
302
- else
303
- # not part of any workspace, but has some folder shared
304
- user_info = current_user_info(exception: true) rescue {'read_only_home_node_id' => nil, 'read_only_home_file_id' => nil}
305
- @context_cache[:home_node_id] = user_info['read_only_home_node_id']
306
- @context_cache[:home_file_id] = user_info['read_only_home_file_id']
307
- end
308
- raise "Cannot get user's home node id, check your default workspace or specify one" if @context_cache[:home_node_id].to_s.empty?
309
- Log.log.debug{Log.dump(:context, @context_cache)}
310
- return @context_cache
311
- end
312
-
313
- # @param node_id [String] identifier of node in AoC
314
- # @param workspace_id [String] workspace identifier
315
- # @param workspace_name [String] workspace name
316
- # @param scope e.g. Aspera::Node::SCOPE_USER, or nil (requires secret)
317
- # @param package_info [Hash] created package information
318
- # @returns [Aspera::Node] a node API for access key
319
- def node_api_from(node_id:, workspace_id: nil, workspace_name: nil, scope: Aspera::Node::SCOPE_USER, package_info: nil)
320
- assert_type(node_id, String)
321
- node_info = read("nodes/#{node_id}")[:data]
322
- if workspace_name.nil? && !workspace_id.nil?
323
- workspace_name = read("workspaces/#{workspace_id}")[:data]['name']
324
- end
325
- app_info = {
326
- api: self, # for callback
327
- app: package_info.nil? ? FILES_APP : PACKAGES_APP,
328
- node_info: node_info,
329
- workspace_id: workspace_id,
330
- workspace_name: workspace_name
331
- }
332
- if PACKAGES_APP.eql?(app_info[:app])
333
- raise 'package info required' if package_info.nil?
334
- app_info[:package_id] = package_info['id']
335
- app_info[:package_name] = package_info['name']
336
- end
337
- node_rest_params = {base_url: node_info['url']}
338
- # if secret is available
339
- if scope.nil?
340
- node_rest_params[:auth] = {
341
- type: :basic,
342
- username: node_info['access_key'],
343
- password: @secret_finder&.lookup_secret(url: node_info['url'], username: node_info['access_key'], mandatory: true)
344
- }
345
- else
346
- # OAuth bearer token
347
- node_rest_params[:auth] = params[:auth].clone
348
- node_rest_params[:auth][:scope] = Aspera::Node.token_scope(node_info['access_key'], scope)
349
- # special header required for bearer token only
350
- node_rest_params[:headers] = {Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => node_info['access_key']}
351
- end
352
- return Node.new(params: node_rest_params, app_info: app_info)
353
- end
354
-
355
- # Check metadata: remove when validation is done server side
356
- def validate_metadata(pkg_data)
357
- # validate only for shared inboxes
358
- return unless pkg_data['recipients'].is_a?(Array) &&
359
- pkg_data['recipients'].first.is_a?(Hash) &&
360
- pkg_data['recipients'].first.key?('type') &&
361
- pkg_data['recipients'].first['type'].eql?('dropbox')
362
- meta_schema = read("dropboxes/#{pkg_data['recipients'].first['id']}")[:data]['metadata_schema']
363
- if meta_schema.nil? || meta_schema.empty?
364
- Log.log.debug('no metadata in shared inbox')
365
- return
366
- end
367
- assert(pkg_data.key?('metadata')){"package requires metadata: #{meta_schema}"}
368
- pkg_meta = pkg_data['metadata']
369
- assert_type(pkg_meta, Array){'metadata'}
370
- Log.log.debug{Log.dump(:metadata, pkg_meta)}
371
- pkg_meta.each do |field|
372
- assert_type(field, Hash){'metadata field'}
373
- assert(field.key?('name')){'metadata field must have name'}
374
- assert(field.key?('values')){'metadata field must have values'}
375
- assert_type(field['values'], Array){'metadata field values'}
376
- assert(!meta_schema.select{|i|i['name'].eql?(field['name'])}.empty?){"unknown metadata field: #{field['name']}"}
377
- end
378
- meta_schema.each do |field|
379
- provided = pkg_meta.select{|i|i['name'].eql?(field['name'])}
380
- raise "only one field with name #{field['name']} allowed" if provided.count > 1
381
- raise "missing mandatory field: #{field['name']}" if field['required'] && provided.empty?
382
- end
383
- end
384
-
385
- # Normalize package creation recipient lists as expected by AoC API
386
- # AoC expects {type: , id: }, but ascli allows providing either the native values or just a name
387
- # in that case, the name is resolved and replaced with {type: , id: }
388
- # @param package_data The whole package creation payload
389
- # @param recipient_list_field The field in structure, i.e. recipients or bcc_recipients
390
- # @return nil package_data is modified
391
- def resolve_package_recipients(package_data, ws_id, recipient_list_field, new_user_option)
392
- return unless package_data.key?(recipient_list_field)
393
- assert_type(package_data[recipient_list_field], Array){recipient_list_field}
394
- new_user_option = {'package_contact' => true} if new_user_option.nil?
395
- assert_type(new_user_option, Hash){'new_user_option'}
396
- # list with resolved elements
397
- resolved_list = []
398
- package_data[recipient_list_field].each do |short_recipient_info|
399
- case short_recipient_info
400
- when Hash # native API information, check keys
401
- assert(short_recipient_info.keys.sort.eql?(%w[id type])){"#{recipient_list_field} element shall have fields: id and type"}
402
- when String # CLI helper: need to resolve provided name to type/id
403
- # email: user, else dropbox
404
- entity_type = short_recipient_info.include?('@') ? 'contacts' : 'dropboxes'
405
- begin
406
- full_recipient_info = lookup_by_name(entity_type, short_recipient_info, {'current_workspace_id' => ws_id})
407
- rescue RuntimeError => e
408
- raise e unless e.message.start_with?(ENTITY_NOT_FOUND)
409
- # dropboxes cannot be created on the fly
410
- raise "No such shared inbox in workspace #{ws_id}" if entity_type.eql?('dropboxes')
411
- # unknown user: create it as external user
412
- full_recipient_info = create('contacts', {
413
- 'current_workspace_id' => ws_id,
414
- 'email' => short_recipient_info
415
- }.merge(new_user_option))[:data]
416
- end
417
- short_recipient_info = if entity_type.eql?('dropboxes')
418
- {'id' => full_recipient_info['id'], 'type' => 'dropbox'}
419
- else
420
- {'id' => full_recipient_info['source_id'], 'type' => full_recipient_info['source_type']}
421
- end
422
- else # unexpected extended value, must be String or Hash
423
- raise "#{recipient_list_field} item must be a String (email, shared inbox) or Hash (id,type)"
424
- end # type of recipient info
425
- # add original or resolved recipient info
426
- resolved_list.push(short_recipient_info)
427
- end
428
- # replace with resolved elements
429
- package_data[recipient_list_field] = resolved_list
430
- return nil
431
- end
432
-
433
- # CLI allows simplified format for metadata: transform if necessary for API
434
- def update_package_metadata_for_api(pkg_data)
435
- case pkg_data['metadata']
436
- when Array, NilClass # no action
437
- when Hash
438
- api_meta = []
439
- pkg_data['metadata'].each do |k, v|
440
- api_meta.push({
441
- # 'input_type' => 'single-dropdown',
442
- 'name' => k,
443
- 'values' => v.is_a?(Array) ? v : [v]
444
- })
445
- end
446
- pkg_data['metadata'] = api_meta
447
- else error_unexpected_value(pkg_meta.class)
448
- end
449
- return nil
450
- end
451
-
452
- # create a package
453
- # @param package_data [Hash] package creation (with extensions...)
454
- # @param validate_meta [TrueClass,FalseClass] true to validate parameters locally
455
- # @param new_user_option [Hash] options if an unknown user is specified
456
- # @return transfer spec, node api and package information
457
- def create_package_simple(package_data, validate_meta, new_user_option)
458
- update_package_metadata_for_api(package_data)
459
- # list of files to include in package, optional
460
- # package_data['file_names']||=[..list of filenames to transfer...]
461
-
462
- # lookup users
463
- resolve_package_recipients(package_data, package_data['workspace_id'], 'recipients', new_user_option)
464
- resolve_package_recipients(package_data, package_data['workspace_id'], 'bcc_recipients', new_user_option)
465
-
466
- validate_metadata(package_data) if validate_meta
467
-
468
- # create a new package container
469
- created_package = create('packages', package_data)[:data]
470
-
471
- package_node_api = node_api_from(
472
- node_id: created_package['node_id'],
473
- workspace_id: created_package['workspace_id'],
474
- package_info: created_package)
475
-
476
- # tell AoC what to expect in package: 1 transfer (can also be done after transfer)
477
- # TODO: if multi session was used we should probably tell
478
- # also, currently no "multi-source" , i.e. only from client-side files, unless "node" agent is used
479
- update("packages/#{created_package['id']}", {'sent' => true, 'transfers_expected' => 1})[:data]
480
-
481
- return {
482
- spec: package_node_api.transfer_spec_gen4(created_package['contents_file_id'], Fasp::TransferSpec::DIRECTION_SEND),
483
- node: package_node_api,
484
- info: created_package
485
- }
486
- end
487
-
488
- # Add transfer spec
489
- # callback in Aspera::Node (transfer_spec_gen4)
490
- def add_ts_tags(transfer_spec:, app_info:)
491
- # translate transfer direction to upload/download
492
- transfer_type = Fasp::TransferSpec.action(transfer_spec)
493
- # Analytics tags
494
- ################
495
- transfer_spec.deep_merge!({
496
- 'tags' => {
497
- Fasp::TransferSpec::TAG_RESERVED => {
498
- 'usage_id' => "aspera.files.workspace.#{app_info[:workspace_id]}", # activity tracking
499
- 'files' => {
500
- 'files_transfer_action' => "#{transfer_type}_#{app_info[:app].gsub(/s$/, '')}",
501
- 'workspace_name' => app_info[:workspace_name], # activity tracking
502
- 'workspace_id' => app_info[:workspace_id]
503
- }
504
- }
505
- }
506
- })
507
- # Console cookie
508
- ################
509
- # we are sure that fields are not nil
510
- cookie_elements = [app_info[:app], current_user_info['name'] || 'public link', current_user_info['email'] || 'none'].map{|e|Base64.strict_encode64(e)}
511
- cookie_elements.unshift(COOKIE_PREFIX_CONSOLE_AOC)
512
- transfer_spec['cookie'] = cookie_elements.join(':')
513
- # Application tags
514
- ##################
515
- case app_info[:app]
516
- when FILES_APP
517
- file_id = transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['node']['file_id']
518
- transfer_spec.deep_merge!({'tags' => {Fasp::TransferSpec::TAG_RESERVED => {'files' => {'parentCwd' => "#{app_info[:node_info]['id']}:#{file_id}"}}}}) \
519
- unless transfer_spec.key?('remote_access_key')
520
- when PACKAGES_APP
521
- transfer_spec.deep_merge!({
522
- 'tags' => {
523
- Fasp::TransferSpec::TAG_RESERVED => {
524
- 'files' => {
525
- 'package_id' => app_info[:package_id],
526
- 'package_name' => app_info[:package_name],
527
- 'package_operation' => transfer_type
528
- }
529
- }
530
- }
531
- })
532
- end
533
- transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['files']['node_id'] = app_info[:node_info]['id']
534
- transfer_spec['tags'][Fasp::TransferSpec::TAG_RESERVED]['app'] = app_info[:app]
535
- end
536
-
537
- ID_AK_ADMIN = 'ASPERA_ACCESS_KEY_ADMIN'
538
- # Callback from Plugins::Node
539
- # add application specific tags to permissions creation
540
- # @param create_param [Hash] parameters for creating permissions
541
- # @param app_info [Hash] application information
542
- def permissions_set_create_params(create_param:, app_info:)
543
- # workspace shared folder:
544
- # access_id = "#{ID_AK_ADMIN}_WS_#{app_info[:workspace_id]}"
545
- default_params = {
546
- # 'access_type' => 'user', # mandatory: user or group
547
- # 'access_id' => access_id, # id of user or group
548
- 'tags' => {
549
- Fasp::TransferSpec::TAG_RESERVED => {
550
- 'files' => {
551
- 'workspace' => {
552
- 'id' => app_info[:workspace_id],
553
- 'workspace_name' => app_info[:workspace_name],
554
- 'user_name' => current_user_info['name'],
555
- 'shared_by_user_id' => current_user_info['id'],
556
- 'shared_by_name' => current_user_info['name'],
557
- 'shared_by_email' => current_user_info['email'],
558
- # 'shared_with_name' => access_id,
559
- 'access_key' => app_info[:node_info]['access_key'],
560
- 'node' => app_info[:node_info]['name']
561
- }
562
- }
563
- }
564
- }
565
- }
566
- create_param.deep_merge!(default_params)
567
- if create_param.key?('with')
568
- contact_info = lookup_by_name(
569
- 'contacts',
570
- create_param['with'],
571
- {'current_workspace_id' => app_info[:workspace_id], 'context' => 'share_folder'})
572
- create_param.delete('with')
573
- create_param['access_type'] = contact_info['source_type']
574
- create_param['access_id'] = contact_info['source_id']
575
- create_param['tags'][Fasp::TransferSpec::TAG_RESERVED]['files']['workspace']['shared_with_name'] = contact_info['email']
576
- end
577
- # optional
578
- app_info[:opt_link_name] = create_param.delete('link_name')
579
- end
580
-
581
- # Callback from Plugins::Node
582
- # send shared folder event to AoC
583
- # @param created_data [Hash] response from permission creation
584
- # @param app_info [Hash] hash with app info
585
- # @param types [Array] event types
586
- def permissions_send_event(created_data:, app_info:, types: PERMISSIONS_CREATED)
587
- assert_type(types, Array)
588
- assert(!types.empty?)
589
- event_creation = {
590
- 'types' => types,
591
- 'node_id' => app_info[:node_info]['id'],
592
- 'workspace_id' => app_info[:workspace_id],
593
- 'data' => created_data
594
- }
595
- # (optional). The name of the folder to be displayed to the destination user.
596
- # Use it if its value is different from the "share_as" field.
597
- event_creation['link_name'] = app_info[:opt_link_name] unless app_info[:opt_link_name].nil?
598
- create('events', event_creation)
599
- end
600
- end # AoC
601
- end # Aspera
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'aspera/log'
4
- require 'aspera/rest'
5
-
6
- module Aspera
7
- class AtsApi < Aspera::Rest
8
- # currently supported clouds
9
- # Note to Aspera: shall be an API call
10
- CLOUD_NAME = {
11
- aws: 'Amazon Web Services',
12
- azure: 'Microsoft Azure',
13
- google: 'Google Cloud',
14
- limelight: 'Limelight',
15
- rackspace: 'Rackspace',
16
- softlayer: 'IBM Cloud'
17
- }.freeze
18
-
19
- private_constant :CLOUD_NAME
20
-
21
- class << self
22
- def base_url; 'https://ats.aspera.io'; end
23
- end
24
-
25
- def initialize
26
- super({base_url: AtsApi.base_url + '/pub/v1'})
27
- # cache of server data
28
- @all_servers_cache = nil
29
- end
30
-
31
- def cloud_names; CLOUD_NAME; end
32
-
33
- # all available ATS servers
34
- # NOTE to Aspera: an API shall be created to retrieve all servers at once
35
- def all_servers
36
- if @all_servers_cache.nil?
37
- @all_servers_cache = []
38
- CLOUD_NAME.each_key do |name|
39
- read("servers/#{name.to_s.upcase}")[:data].each do |i|
40
- @all_servers_cache.push(i)
41
- end
42
- end
43
- end
44
- return @all_servers_cache
45
- end
46
- end # AtsApi
47
- end # Aspera