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