aspera-cli 4.16.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 (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