aspera-cli 4.9.0 → 4.11.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/BUGS.md +20 -0
- data/CHANGELOG.md +509 -0
- data/CONTRIBUTING.md +118 -0
- data/README.md +1241 -916
- data/bin/ascli +4 -4
- data/bin/asession +11 -11
- data/docs/test_env.conf +32 -21
- data/examples/aoc.rb +4 -4
- data/examples/dascli +16 -9
- data/examples/faspex4.rb +8 -8
- data/examples/node.rb +12 -12
- data/examples/server.rb +10 -10
- data/lib/aspera/aoc.rb +273 -266
- data/lib/aspera/ascmd.rb +56 -54
- data/lib/aspera/ats_api.rb +4 -4
- data/lib/aspera/cli/basic_auth_plugin.rb +15 -12
- data/lib/aspera/cli/extended_value.rb +5 -5
- data/lib/aspera/cli/formater.rb +64 -64
- data/lib/aspera/cli/info.rb +2 -2
- data/lib/aspera/cli/listener/line_dump.rb +1 -1
- data/lib/aspera/cli/listener/logger.rb +1 -1
- data/lib/aspera/cli/listener/progress.rb +5 -6
- data/lib/aspera/cli/listener/progress_multi.rb +14 -19
- data/lib/aspera/cli/main.rb +66 -67
- data/lib/aspera/cli/manager.rb +112 -110
- data/lib/aspera/cli/plugin.rb +57 -36
- data/lib/aspera/cli/plugins/alee.rb +4 -4
- data/lib/aspera/cli/plugins/aoc.rb +309 -670
- data/lib/aspera/cli/plugins/ats.rb +44 -46
- data/lib/aspera/cli/plugins/bss.rb +10 -10
- data/lib/aspera/cli/plugins/config.rb +497 -378
- data/lib/aspera/cli/plugins/console.rb +12 -12
- data/lib/aspera/cli/plugins/cos.rb +18 -20
- data/lib/aspera/cli/plugins/faspex.rb +112 -114
- data/lib/aspera/cli/plugins/faspex5.rb +71 -46
- data/lib/aspera/cli/plugins/node.rb +379 -283
- data/lib/aspera/cli/plugins/orchestrator.rb +46 -46
- data/lib/aspera/cli/plugins/preview.rb +122 -114
- data/lib/aspera/cli/plugins/server.rb +137 -83
- data/lib/aspera/cli/plugins/shares.rb +30 -29
- data/lib/aspera/cli/plugins/sync.rb +13 -33
- data/lib/aspera/cli/transfer_agent.rb +60 -59
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/colors.rb +3 -3
- data/lib/aspera/command_line_builder.rb +27 -27
- data/lib/aspera/cos_node.rb +22 -20
- data/lib/aspera/data_repository.rb +1 -1
- data/lib/aspera/environment.rb +35 -15
- data/lib/aspera/fasp/agent_base.rb +15 -15
- data/lib/aspera/fasp/agent_connect.rb +23 -21
- data/lib/aspera/fasp/agent_direct.rb +66 -64
- data/lib/aspera/fasp/agent_httpgw.rb +141 -78
- data/lib/aspera/fasp/agent_node.rb +23 -21
- data/lib/aspera/fasp/agent_trsdk.rb +20 -20
- data/lib/aspera/fasp/error.rb +3 -2
- data/lib/aspera/fasp/error_info.rb +11 -8
- data/lib/aspera/fasp/installation.rb +79 -79
- data/lib/aspera/fasp/listener.rb +1 -1
- data/lib/aspera/fasp/parameters.rb +86 -71
- data/lib/aspera/fasp/parameters.yaml +7 -4
- data/lib/aspera/fasp/resume_policy.rb +8 -8
- data/lib/aspera/fasp/transfer_spec.rb +35 -2
- data/lib/aspera/fasp/uri.rb +7 -7
- data/lib/aspera/faspex_gw.rb +7 -5
- data/lib/aspera/hash_ext.rb +3 -3
- data/lib/aspera/id_generator.rb +5 -5
- data/lib/aspera/keychain/encrypted_hash.rb +38 -105
- data/lib/aspera/keychain/macos_security.rb +128 -57
- data/lib/aspera/log.rb +7 -7
- data/lib/aspera/nagios.rb +19 -18
- data/lib/aspera/node.rb +209 -35
- data/lib/aspera/oauth.rb +37 -36
- data/lib/aspera/open_application.rb +19 -11
- data/lib/aspera/persistency_action_once.rb +4 -4
- data/lib/aspera/persistency_folder.rb +16 -15
- data/lib/aspera/preview/file_types.rb +8 -8
- data/lib/aspera/preview/generator.rb +67 -67
- data/lib/aspera/preview/utils.rb +27 -27
- data/lib/aspera/proxy_auto_config.js +41 -41
- data/lib/aspera/proxy_auto_config.rb +21 -14
- data/lib/aspera/rest.rb +72 -67
- data/lib/aspera/rest_call_error.rb +2 -1
- data/lib/aspera/rest_error_analyzer.rb +18 -17
- data/lib/aspera/rest_errors_aspera.rb +16 -16
- data/lib/aspera/secret_hider.rb +15 -13
- data/lib/aspera/ssh.rb +11 -10
- data/lib/aspera/sync.rb +158 -44
- data/lib/aspera/temp_file_manager.rb +2 -2
- data/lib/aspera/uri_reader.rb +4 -4
- data/lib/aspera/web_auth.rb +14 -13
- data.tar.gz.sig +0 -0
- metadata +11 -36
- metadata.gz.sig +0 -0
data/lib/aspera/aoc.rb
CHANGED
@@ -8,20 +8,23 @@ require 'aspera/fasp/transfer_spec'
|
|
8
8
|
require 'base64'
|
9
9
|
require 'cgi'
|
10
10
|
|
11
|
-
Aspera::Oauth.register_token_creator(
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
Aspera::Oauth.register_token_creator(
|
12
|
+
:aoc_pub_link,
|
13
|
+
lambda{|o|
|
14
|
+
o.api.call({
|
15
|
+
operation: 'POST',
|
16
|
+
subpath: o.gparams[:path_token],
|
17
|
+
headers: {'Accept' => 'application/json'},
|
18
|
+
json_params: o.sparams[:json],
|
19
|
+
url_params: o.sparams[:url].merge(scope: o.gparams[:scope]) # scope is here because it changes over time (node)
|
20
|
+
})
|
21
|
+
},
|
22
|
+
lambda { |oauth|
|
23
|
+
return [oauth.sparams.dig(:json, :url_token)]
|
18
24
|
})
|
19
|
-
},lambda { |oauth|
|
20
|
-
return [oauth.sparams.dig(:json,:url_token)]
|
21
|
-
})
|
22
25
|
|
23
26
|
module Aspera
|
24
|
-
class AoC < Rest
|
27
|
+
class AoC < Aspera::Rest
|
25
28
|
PRODUCT_NAME = 'Aspera on Cloud'
|
26
29
|
# Production domain of AoC
|
27
30
|
PROD_DOMAIN = 'ibmaspera.com'
|
@@ -32,7 +35,7 @@ module Aspera
|
|
32
35
|
# index offset in data repository of client app
|
33
36
|
DATA_REPO_INDEX_START = 4
|
34
37
|
# cookie prefix so that console can decode identity
|
35
|
-
|
38
|
+
COOKIE_PREFIX_CONSOLE_AOC = 'aspera.aoc'
|
36
39
|
# path in URL of public links
|
37
40
|
PUBLIC_LINK_PATHS = %w[/packages/public/receive /packages/public/send /files/public].freeze
|
38
41
|
JWT_AUDIENCE = 'https://api.asperafiles.com/api/v1/oauth2/token'
|
@@ -40,49 +43,50 @@ module Aspera
|
|
40
43
|
# minimum fields for user info if retrieval fails
|
41
44
|
USER_INFO_FIELDS_MIN = %w[name email id default_workspace_id organization_id].freeze
|
42
45
|
|
43
|
-
private_constant :MAX_REDIRECT
|
44
|
-
:
|
46
|
+
private_constant :MAX_REDIRECT,
|
47
|
+
:GLOBAL_CLIENT_APPS,
|
48
|
+
:DATA_REPO_INDEX_START,
|
49
|
+
:COOKIE_PREFIX_CONSOLE_AOC,
|
50
|
+
:PUBLIC_LINK_PATHS,
|
51
|
+
:JWT_AUDIENCE,
|
52
|
+
:OAUTH_API_SUBPATH,
|
53
|
+
:USER_INFO_FIELDS_MIN
|
45
54
|
|
46
55
|
# various API scopes supported
|
47
56
|
SCOPE_FILES_SELF = 'self'
|
48
57
|
SCOPE_FILES_USER = 'user:all'
|
49
58
|
SCOPE_FILES_ADMIN = 'admin:all'
|
50
59
|
SCOPE_FILES_ADMIN_USER = 'admin-user:all'
|
51
|
-
SCOPE_FILES_ADMIN_USER_USER = SCOPE_FILES_ADMIN_USER
|
60
|
+
SCOPE_FILES_ADMIN_USER_USER = "#{SCOPE_FILES_ADMIN_USER}+#{SCOPE_FILES_USER}"
|
52
61
|
SCOPE_NODE_USER = 'user:all'
|
53
62
|
SCOPE_NODE_ADMIN = 'admin:all'
|
54
|
-
PATH_SEPARATOR = '/'
|
55
63
|
FILES_APP = 'files'
|
56
64
|
PACKAGES_APP = 'packages'
|
57
65
|
API_V1 = 'api/v1'
|
58
66
|
# error message when entity not found
|
59
67
|
ENTITY_NOT_FOUND = 'No such'
|
60
68
|
|
61
|
-
# class instance variable, access with accessors on class
|
62
|
-
@use_standard_ports = true
|
63
|
-
|
64
69
|
# class static methods
|
65
70
|
class << self
|
66
|
-
attr_accessor :use_standard_ports
|
67
71
|
# strings /Applications/Aspera\ Drive.app/Contents/MacOS/AsperaDrive|grep -E '.{100}==$'|base64 --decode
|
68
72
|
def get_client_info(client_name=GLOBAL_CLIENT_APPS.first)
|
69
73
|
client_index = GLOBAL_CLIENT_APPS.index(client_name)
|
70
74
|
raise "no such pre-defined client: #{client_name}" if client_index.nil?
|
71
|
-
return client_name,Base64.urlsafe_encode64(DataRepository.instance.data(DATA_REPO_INDEX_START + client_index))
|
75
|
+
return client_name, Base64.urlsafe_encode64(DataRepository.instance.data(DATA_REPO_INDEX_START + client_index))
|
72
76
|
end
|
73
77
|
|
74
78
|
# @param url of AoC instance
|
75
79
|
# @return organization id in url and AoC domain: ibmaspera.com, asperafiles.com or qa.asperafiles.com, etc...
|
76
80
|
def parse_url(aoc_org_url)
|
77
|
-
uri = URI.parse(aoc_org_url.gsub(
|
81
|
+
uri = URI.parse(aoc_org_url.gsub(%r{/+$}, ''))
|
78
82
|
instance_fqdn = uri.host
|
79
|
-
Log.log.debug
|
83
|
+
Log.log.debug{"instance_fqdn=#{instance_fqdn}"}
|
80
84
|
raise "No host found in URL.Please check URL format: https://myorg.#{PROD_DOMAIN}" if instance_fqdn.nil?
|
81
|
-
organization,instance_domain = instance_fqdn.split('.',2)
|
82
|
-
Log.log.debug
|
83
|
-
Log.log.debug
|
85
|
+
organization, instance_domain = instance_fqdn.split('.', 2)
|
86
|
+
Log.log.debug{"instance_domain=#{instance_domain}"}
|
87
|
+
Log.log.debug{"organization=#{organization}"}
|
84
88
|
raise "expecting a public FQDN for #{PRODUCT_NAME}" if instance_domain.nil?
|
85
|
-
return organization,instance_domain
|
89
|
+
return organization, instance_domain
|
86
90
|
end
|
87
91
|
|
88
92
|
# base API url depends on domain, which could be "qa.xxx"
|
@@ -90,22 +94,22 @@ module Aspera
|
|
90
94
|
return "https://#{organization}.#{api_domain}"
|
91
95
|
end
|
92
96
|
|
93
|
-
def metering_api(entitlement_id,customer_id,api_domain=PROD_DOMAIN)
|
97
|
+
def metering_api(entitlement_id, customer_id, api_domain=PROD_DOMAIN)
|
94
98
|
return Rest.new({
|
95
99
|
base_url: "#{api_base_url(api_domain: api_domain)}/metering/v1",
|
96
|
-
headers: {'X-Aspera-Entitlement-Authorization' => Rest.basic_creds(entitlement_id,customer_id)}
|
100
|
+
headers: {'X-Aspera-Entitlement-Authorization' => Rest.basic_creds(entitlement_id, customer_id)}
|
97
101
|
})
|
98
102
|
end
|
99
103
|
|
100
104
|
# node API scopes
|
101
|
-
def node_scope(access_key,scope)
|
105
|
+
def node_scope(access_key, scope)
|
102
106
|
return "node.#{access_key}:#{scope}"
|
103
107
|
end
|
104
108
|
|
105
109
|
# check option "link"
|
106
110
|
# if present try to get token value (resolve redirection if short links used)
|
107
111
|
# then set options url/token/auth
|
108
|
-
def resolve_pub_link(a_auth,a_opt)
|
112
|
+
def resolve_pub_link(a_auth, a_opt)
|
109
113
|
public_link_url = a_opt[:link]
|
110
114
|
return if public_link_url.nil?
|
111
115
|
raise 'do not use both link and url options' unless a_opt[:url].nil?
|
@@ -115,7 +119,7 @@ module Aspera
|
|
115
119
|
# detect if it's an expected format
|
116
120
|
if PUBLIC_LINK_PATHS.include?(uri.path)
|
117
121
|
url_param_token_pair = URI.decode_www_form(uri.query).find{|e|e.first.eql?('token')}
|
118
|
-
raise ArgumentError,'link option must be URL with "token" parameter' if url_param_token_pair.nil?
|
122
|
+
raise ArgumentError, 'link option must be URL with "token" parameter' if url_param_token_pair.nil?
|
119
123
|
# ok we get it !
|
120
124
|
a_opt[:url] = 'https://' + uri.host
|
121
125
|
a_auth[:crtype] = :aoc_pub_link
|
@@ -127,60 +131,31 @@ module Aspera
|
|
127
131
|
a_auth[:aoc_pub_link][:json][:password] = a_opt[:password] unless a_opt[:password].nil?
|
128
132
|
return # SUCCESS
|
129
133
|
end
|
130
|
-
Log.log.debug
|
134
|
+
Log.log.debug{"no expected format: #{public_link_url}"}
|
131
135
|
r = Net::HTTP.get_response(uri)
|
132
136
|
# not a redirection
|
133
|
-
raise ArgumentError,'link option must be redirect or have token parameter' unless r.code.start_with?('3')
|
137
|
+
raise ArgumentError, 'link option must be redirect or have token parameter' unless r.code.start_with?('3')
|
134
138
|
public_link_url = r['location']
|
135
139
|
raise 'no location in redirection' if public_link_url.nil?
|
136
|
-
Log.log.debug
|
140
|
+
Log.log.debug{"redirect to: #{public_link_url}"}
|
137
141
|
end # loop
|
138
142
|
raise "exceeded max redirection: #{MAX_REDIRECT}"
|
139
143
|
end
|
140
|
-
|
141
|
-
# additional transfer spec (tags) for package information
|
142
|
-
def package_tags(package_info,operation)
|
143
|
-
return {'tags' => {'aspera' => {'files' => {
|
144
|
-
'package_id' => package_info['id'],
|
145
|
-
'package_name' => package_info['name'],
|
146
|
-
'package_operation' => operation
|
147
|
-
}}}}
|
148
|
-
end
|
149
|
-
|
150
|
-
# add details to show in analytics
|
151
|
-
def analytics_ts(app,direction,ws_id,ws_name)
|
152
|
-
# translate transfer to operation
|
153
|
-
operation =
|
154
|
-
case direction
|
155
|
-
when Fasp::TransferSpec::DIRECTION_SEND then 'upload'
|
156
|
-
when Fasp::TransferSpec::DIRECTION_RECEIVE then 'download'
|
157
|
-
else raise "ERROR: unexpected value: #{direction}"
|
158
|
-
end
|
159
|
-
|
160
|
-
return {
|
161
|
-
'tags' => {
|
162
|
-
'aspera' => {
|
163
|
-
'usage_id' => "aspera.files.workspace.#{ws_id}", # activity tracking
|
164
|
-
'files' => {
|
165
|
-
'files_transfer_action' => "#{operation}_#{app.gsub(/s$/,'')}",
|
166
|
-
'workspace_name' => ws_name, # activity tracking
|
167
|
-
'workspace_id' => ws_id
|
168
|
-
}
|
169
|
-
}
|
170
|
-
}
|
171
|
-
}
|
172
|
-
end
|
173
144
|
end # static methods
|
174
145
|
|
175
|
-
#
|
146
|
+
# CLI options that are also options to initialize
|
147
|
+
OPTIONS_NEW = %i[link url auth client_id client_secret scope redirect_uri private_key passphrase username password].freeze
|
148
|
+
|
149
|
+
# @param any of OPTIONS_NEW + subpath
|
176
150
|
def initialize(opt)
|
177
|
-
raise ArgumentError,'Missing mandatory option: scope' if opt[:scope].nil?
|
151
|
+
raise ArgumentError, 'Missing mandatory option: scope' if opt[:scope].nil?
|
178
152
|
|
179
153
|
# access key secrets are provided out of band to get node api access
|
180
154
|
# key: access key
|
181
155
|
# value: associated secret
|
182
|
-
@
|
183
|
-
@
|
156
|
+
@secret_finder = nil
|
157
|
+
@cache_user_info = nil
|
158
|
+
@cache_url_token_info = nil
|
184
159
|
|
185
160
|
# init rest params
|
186
161
|
aoc_rest_p = {auth: {type: :oauth2}}
|
@@ -188,13 +163,13 @@ module Aspera
|
|
188
163
|
aoc_auth_p = aoc_rest_p[:auth]
|
189
164
|
|
190
165
|
# sets opt[:url], aoc_rest_p[:auth][:crtype], [:auth][:aoc_pub_link] if there is a link
|
191
|
-
self.class.resolve_pub_link(aoc_auth_p,opt)
|
166
|
+
self.class.resolve_pub_link(aoc_auth_p, opt)
|
192
167
|
|
193
168
|
# test here because link may set url
|
194
|
-
raise ArgumentError,'Missing mandatory option: url' if opt[:url].nil?
|
169
|
+
raise ArgumentError, 'Missing mandatory option: url' if opt[:url].nil?
|
195
170
|
|
196
171
|
# get org name and domain from url
|
197
|
-
organization,instance_domain = self.class.parse_url(opt[:url])
|
172
|
+
organization, instance_domain = self.class.parse_url(opt[:url])
|
198
173
|
# this is the base API url
|
199
174
|
api_url_base = self.class.api_base_url(api_domain: instance_domain)
|
200
175
|
# API URL, including subpath (version ...)
|
@@ -206,25 +181,25 @@ module Aspera
|
|
206
181
|
aoc_auth_p[:scope] = opt[:scope]
|
207
182
|
|
208
183
|
# filled if pub link
|
209
|
-
if !aoc_auth_p.
|
210
|
-
raise ArgumentError,'Missing mandatory option: auth' if opt[:auth].nil?
|
184
|
+
if !aoc_auth_p.key?(:crtype)
|
185
|
+
raise ArgumentError, 'Missing mandatory option: auth' if opt[:auth].nil?
|
211
186
|
aoc_auth_p[:crtype] = opt[:auth]
|
212
187
|
end
|
213
188
|
|
214
189
|
if aoc_auth_p[:client_id].nil?
|
215
|
-
aoc_auth_p[:client_id],aoc_auth_p[:client_secret] = self.class.get_client_info
|
190
|
+
aoc_auth_p[:client_id], aoc_auth_p[:client_secret] = self.class.get_client_info
|
216
191
|
end
|
217
192
|
|
218
193
|
# fill other auth parameters based on Oauth method
|
219
194
|
case aoc_auth_p[:crtype]
|
220
195
|
when :web
|
221
|
-
raise ArgumentError,'Missing mandatory option: redirect_uri' if opt[:redirect_uri].nil?
|
196
|
+
raise ArgumentError, 'Missing mandatory option: redirect_uri' if opt[:redirect_uri].nil?
|
222
197
|
aoc_auth_p[:web] = {redirect_uri: opt[:redirect_uri]}
|
223
198
|
when :jwt
|
224
|
-
raise ArgumentError,'Missing mandatory option: private_key' if opt[:private_key].nil?
|
225
|
-
raise ArgumentError,'Missing mandatory option: username' if opt[:username].nil?
|
199
|
+
raise ArgumentError, 'Missing mandatory option: private_key' if opt[:private_key].nil?
|
200
|
+
raise ArgumentError, 'Missing mandatory option: username' if opt[:username].nil?
|
226
201
|
aoc_auth_p[:jwt] = {
|
227
|
-
private_key_obj: OpenSSL::PKey::RSA.new(opt[:private_key],opt[:passphrase]),
|
202
|
+
private_key_obj: OpenSSL::PKey::RSA.new(opt[:private_key], opt[:passphrase]),
|
228
203
|
payload: {
|
229
204
|
iss: aoc_auth_p[:client_id], # issuer
|
230
205
|
sub: opt[:username], # subject
|
@@ -235,7 +210,7 @@ module Aspera
|
|
235
210
|
aoc_auth_p[:jwt][:payload][:org] = organization if GLOBAL_CLIENT_APPS.include?(aoc_auth_p[:client_id])
|
236
211
|
when :aoc_pub_link
|
237
212
|
# basic auth required for /token
|
238
|
-
aoc_auth_p[:auth] = {type: :basic, username: aoc_auth_p[:client_id],password: aoc_auth_p[:client_secret]}
|
213
|
+
aoc_auth_p[:auth] = {type: :basic, username: aoc_auth_p[:client_id], password: aoc_auth_p[:client_secret]}
|
239
214
|
else raise "ERROR: unsupported auth method: #{aoc_auth_p[:crtype]}"
|
240
215
|
end
|
241
216
|
super(aoc_rest_p)
|
@@ -243,219 +218,77 @@ module Aspera
|
|
243
218
|
|
244
219
|
def url_token_data
|
245
220
|
return nil unless params[:auth][:crtype].eql?(:aoc_pub_link)
|
221
|
+
return @cache_url_token_info unless @cache_url_token_info.nil?
|
246
222
|
# TODO: can there be several in list ?
|
247
|
-
|
223
|
+
@cache_url_token_info = read('url_tokens')[:data].first
|
224
|
+
return @cache_url_token_info
|
225
|
+
end
|
226
|
+
|
227
|
+
def additional_persistence_ids
|
228
|
+
return [user_info['id']] if url_token_data.nil?
|
229
|
+
return [] # TODO : url_token_data['id'] ?
|
248
230
|
end
|
249
231
|
|
250
|
-
def
|
251
|
-
raise '
|
252
|
-
raise '
|
253
|
-
@
|
232
|
+
def secret_finder=(secret_finder)
|
233
|
+
raise 'secret finder already set' unless @secret_finder.nil?
|
234
|
+
raise 'secret finder must have lookup_secret' unless secret_finder.respond_to?(:lookup_secret)
|
235
|
+
@secret_finder = secret_finder
|
254
236
|
end
|
255
237
|
|
256
238
|
# cached user information
|
257
|
-
def user_info
|
258
|
-
if @
|
239
|
+
def user_info(exception: false)
|
240
|
+
if @cache_user_info.nil?
|
259
241
|
# get our user's default information
|
260
|
-
@
|
242
|
+
@cache_user_info =
|
261
243
|
begin
|
262
244
|
read('self')[:data]
|
263
245
|
rescue StandardError => e
|
264
|
-
|
246
|
+
raise e if exception
|
247
|
+
Log.log.debug{"ignoring error: #{e}"}
|
265
248
|
{}
|
266
249
|
end
|
267
|
-
USER_INFO_FIELDS_MIN.each{|f|@
|
268
|
-
end
|
269
|
-
return @user_info
|
270
|
-
end
|
271
|
-
|
272
|
-
# build ts addon for IBM Aspera Console (cookie)
|
273
|
-
def console_ts(app)
|
274
|
-
# we are sure that fields are not nil
|
275
|
-
elements = [app,user_info['name'],user_info['email']].map{|e|Base64.strict_encode64(e)}
|
276
|
-
elements.unshift(COOKIE_PREFIX)
|
277
|
-
return {'cookie' => elements.join(':')}
|
278
|
-
end
|
279
|
-
|
280
|
-
# build "transfer info", 2 elements array with:
|
281
|
-
# - transfer spec for aspera on cloud, based on node information and file id
|
282
|
-
# - source and token regeneration method
|
283
|
-
def tr_spec(app,direction,node_file,ts_add)
|
284
|
-
# get node api
|
285
|
-
node_api = get_node_api(node_file[:node_info])
|
286
|
-
# this lambda returns the bearer token for node, if
|
287
|
-
token_generation_lambda = lambda{|do_refresh|node_api.oauth_token(force_refresh: do_refresh)}
|
288
|
-
# prepare transfer specification
|
289
|
-
# note xfer_id and xfer_retry are set by the transfer agent itself
|
290
|
-
transfer_spec = {
|
291
|
-
'direction' => direction,
|
292
|
-
'token' => token_generation_lambda.call(false), # first time, use cache
|
293
|
-
'tags' => {
|
294
|
-
'aspera' => {
|
295
|
-
'app' => app,
|
296
|
-
'files' => {
|
297
|
-
'node_id' => node_file[:node_info]['id']
|
298
|
-
}, # files
|
299
|
-
'node' => {
|
300
|
-
'access_key' => node_file[:node_info]['access_key'],
|
301
|
-
#'file_id' => ts_add['source_root_id']
|
302
|
-
'file_id' => node_file[:file_id]
|
303
|
-
} # node
|
304
|
-
} # aspera
|
305
|
-
} # tags
|
306
|
-
}
|
307
|
-
# add remote host info
|
308
|
-
if self.class.use_standard_ports
|
309
|
-
# get default TCP/UDP ports and transfer user
|
310
|
-
transfer_spec.merge!(Fasp::TransferSpec::AK_TSPEC_BASE)
|
311
|
-
# by default: same address as node API
|
312
|
-
transfer_spec['remote_host'] = node_file[:node_info]['host']
|
313
|
-
# 30 it's necessarily https scheme: webui does not allow anything else
|
314
|
-
if node_file[:node_info]['transfer_url'].is_a?(String) && !node_file[:node_info]['transfer_url'].empty?
|
315
|
-
transfer_spec['remote_host'] = URI.parse(node_file[:node_info]['transfer_url']).host
|
316
|
-
end
|
317
|
-
else
|
318
|
-
# retrieve values from API
|
319
|
-
std_t_spec = node_api.create(
|
320
|
-
'files/download_setup',
|
321
|
-
{transfer_requests: [{ transfer_request: {paths: [{'source' => '/'}] } }] }
|
322
|
-
)[:data]['transfer_specs'].first['transfer_spec']
|
323
|
-
%w[remote_host remote_user ssh_port fasp_port].each {|i| transfer_spec[i] = std_t_spec[i]}
|
250
|
+
USER_INFO_FIELDS_MIN.each{|f|@cache_user_info[f] = 'unknown' if @cache_user_info[f].nil?}
|
324
251
|
end
|
325
|
-
|
326
|
-
transfer_spec.deep_merge!(ts_add)
|
327
|
-
# additional information for transfer agent
|
328
|
-
source_and_token_generator = {
|
329
|
-
src: :node_gen4,
|
330
|
-
regenerate_token: token_generation_lambda
|
331
|
-
}
|
332
|
-
return transfer_spec,source_and_token_generator
|
252
|
+
return @cache_user_info
|
333
253
|
end
|
334
254
|
|
335
|
-
# returns a node API for access key
|
336
|
-
# @param
|
337
|
-
# @param scope e.g. SCOPE_NODE_USER
|
338
|
-
|
339
|
-
|
340
|
-
def get_node_api(node_info, scope: SCOPE_NODE_USER, use_secret: true)
|
341
|
-
raise 'internal error' unless node_info.is_a?(Hash) && node_info.has_key?('url') && node_info.has_key?('access_key')
|
342
|
-
# get optional secret unless :use_secret is false
|
343
|
-
ak_secret = @key_chain.get_secret(url: node_info['url'], username: node_info['access_key'], mandatory: false) if use_secret && !@key_chain.nil?
|
344
|
-
raise "There must be at least one of: 'secret' or 'scope' for access key #{node_info['access_key']}" if ak_secret.nil? && scope.nil?
|
255
|
+
# @returns [Aspera::Node] a node API for access key
|
256
|
+
# @param node_id [String] identifier of node in AoC
|
257
|
+
# @param scope e.g. SCOPE_NODE_USER, or nil (requires secret)
|
258
|
+
def node_id_to_api(node_id:, plugin:, scope: nil, package_info: nil)
|
259
|
+
node_info = read("nodes/#{node_id}")[:data]
|
345
260
|
node_rest_params = {base_url: node_info['url']}
|
346
261
|
# if secret is available
|
347
|
-
if
|
262
|
+
if scope.nil?
|
348
263
|
node_rest_params[:auth] = {
|
349
264
|
type: :basic,
|
350
265
|
username: node_info['access_key'],
|
351
|
-
password:
|
266
|
+
password: @secret_finder&.lookup_secret(url: node_info['url'], username: node_info['access_key'], mandatory: true)
|
352
267
|
}
|
353
268
|
else
|
354
|
-
#
|
355
|
-
node_rest_params[:headers] = {'X-Aspera-AccessKey' => node_info['access_key']}
|
269
|
+
# OAuth bearer token
|
356
270
|
node_rest_params[:auth] = params[:auth].clone
|
357
|
-
node_rest_params[:auth][:scope] = self.class.node_scope(node_info['access_key'],scope)
|
271
|
+
node_rest_params[:auth][:scope] = self.class.node_scope(node_info['access_key'], scope)
|
272
|
+
# special header required for bearer token only
|
273
|
+
node_rest_params[:headers] = {Aspera::Node::X_ASPERA_ACCESSKEY => node_info['access_key']}
|
358
274
|
end
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
node_info = node_file[:node_info]
|
368
|
-
file_id = node_file[:file_id]
|
369
|
-
raise "node_info must be Hash (got #{node_info.class}: #{node_info})" unless node_info.is_a?(Hash)
|
370
|
-
raise 'node_info must have id' unless node_info.has_key?('id')
|
371
|
-
raise 'file_id is empty' if file_id.to_s.empty?
|
372
|
-
return node_info,file_id
|
373
|
-
end
|
374
|
-
|
375
|
-
# add entry to list if test block is success
|
376
|
-
def process_find_files(entry,path)
|
377
|
-
begin
|
378
|
-
# add to result if match filter
|
379
|
-
@find_state[:found].push(entry.merge({'path' => path})) if @find_state[:test_block].call(entry)
|
380
|
-
# process link
|
381
|
-
if entry[:type].eql?('link')
|
382
|
-
sub_node_info = read("nodes/#{entry['target_node_id']}")[:data]
|
383
|
-
sub_opt = {method: process_find_files, top_file_id: entry['target_id'], top_file_path: path}
|
384
|
-
get_node_api(sub_node_info).crawl(self,sub_opt)
|
385
|
-
end
|
386
|
-
rescue StandardError => e
|
387
|
-
Log.log.error("#{path}: #{e.message}")
|
388
|
-
end
|
389
|
-
# process all folders
|
390
|
-
return true
|
391
|
-
end
|
392
|
-
|
393
|
-
def find_files(top_node_file, test_block)
|
394
|
-
top_node_info,top_file_id = check_get_node_file(top_node_file)
|
395
|
-
Log.log.debug("find_files: node_info=#{top_node_info}, fileid=#{top_file_id}")
|
396
|
-
@find_state = {found: [], test_block: test_block}
|
397
|
-
get_node_api(top_node_info).crawl(self,{method: :process_find_files, top_file_id: top_file_id})
|
398
|
-
result = @find_state[:found]
|
399
|
-
@find_state = nil
|
400
|
-
return result
|
401
|
-
end
|
402
|
-
|
403
|
-
def process_resolve_node_file(entry,_path)
|
404
|
-
# stop digging here if not in right path
|
405
|
-
return false unless entry['name'].eql?(@resolve_state[:path].first)
|
406
|
-
# ok it matches, so we remove the match
|
407
|
-
@resolve_state[:path].shift
|
408
|
-
case entry['type']
|
409
|
-
when 'file'
|
410
|
-
# file must be terminal
|
411
|
-
raise "#{entry['name']} is a file, expecting folder to find: #{@resolve_state[:path]}" unless @resolve_state[:path].empty?
|
412
|
-
@resolve_state[:result][:file_id] = entry['id']
|
413
|
-
when 'link'
|
414
|
-
@resolve_state[:result][:node_info] = read("nodes/#{entry['target_node_id']}")[:data]
|
415
|
-
if @resolve_state[:path].empty?
|
416
|
-
@resolve_state[:result][:file_id] = entry['target_id']
|
417
|
-
else
|
418
|
-
get_node_api(@resolve_state[:result][:node_info]).crawl(self,{method: :process_resolve_node_file, top_file_id: entry['target_id']})
|
419
|
-
end
|
420
|
-
when 'folder'
|
421
|
-
if @resolve_state[:path].empty?
|
422
|
-
# found: store
|
423
|
-
@resolve_state[:result][:file_id] = entry['id']
|
424
|
-
return false
|
425
|
-
end
|
426
|
-
else
|
427
|
-
Log.log.warn("unknown element type: #{entry['type']}")
|
428
|
-
end
|
429
|
-
# continue to dig folder
|
430
|
-
return true
|
431
|
-
end
|
432
|
-
|
433
|
-
# @return Array(node_info,file_id) for the given path
|
434
|
-
# @param top_node_file Array [root node,file id]
|
435
|
-
# @param element_path_string String path of element
|
436
|
-
# supports links to secondary nodes
|
437
|
-
def resolve_node_file(top_node_file, element_path_string)
|
438
|
-
top_node_info,top_file_id = check_get_node_file(top_node_file)
|
439
|
-
path_elements = element_path_string.split(PATH_SEPARATOR).reject(&:empty?)
|
440
|
-
result = {node_info: top_node_info, file_id: nil}
|
441
|
-
if path_elements.empty?
|
442
|
-
result[:file_id] = top_file_id
|
443
|
-
else
|
444
|
-
@resolve_state = {path: path_elements, result: result}
|
445
|
-
get_node_api(top_node_info).crawl(self,{method: :process_resolve_node_file, top_file_id: top_file_id})
|
446
|
-
not_found = @resolve_state[:path]
|
447
|
-
@resolve_state = nil
|
448
|
-
raise "entry not found: #{not_found}" if result[:file_id].nil?
|
449
|
-
end
|
450
|
-
return result
|
275
|
+
app_info = {
|
276
|
+
plugin: plugin,
|
277
|
+
node_info: node_info,
|
278
|
+
app: package_info.nil? ? FILES_APP : PACKAGES_APP,
|
279
|
+
api: self # for callback
|
280
|
+
}
|
281
|
+
app_info[:package_info] = package_info unless package_info.nil?
|
282
|
+
return Node.new(params: node_rest_params, app_info: app_info)
|
451
283
|
end
|
452
284
|
|
285
|
+
# Query entity type by name and returns the id if a single entry only
|
453
286
|
# @param entity_type path of entuty in API
|
454
287
|
# @param entity_name name of searched entity
|
455
288
|
# @param options additional search options
|
456
|
-
def lookup_entity_by_name(entity_type,entity_name,options={})
|
289
|
+
def lookup_entity_by_name(entity_type, entity_name, options={})
|
457
290
|
# returns entities whose name contains value (case insensitive)
|
458
|
-
matching_items = read(entity_type,options.merge({'q' => CGI.escape(entity_name)}))[:data]
|
291
|
+
matching_items = read(entity_type, options.merge({'q' => CGI.escape(entity_name)}))[:data]
|
459
292
|
case matching_items.length
|
460
293
|
when 1 then return matching_items.first
|
461
294
|
when 0 then raise %Q{#{ENTITY_NOT_FOUND} #{entity_type}: "#{entity_name}"}
|
@@ -470,5 +303,179 @@ module Aspera
|
|
470
303
|
end
|
471
304
|
end
|
472
305
|
end
|
306
|
+
|
307
|
+
# Check metadata: remove when validation is done server side
|
308
|
+
def validate_metadata(pkg_data)
|
309
|
+
# validate only for shared inboxes
|
310
|
+
return unless pkg_data['recipients'].is_a?(Array) &&
|
311
|
+
pkg_data['recipients'].first.is_a?(Hash) &&
|
312
|
+
pkg_data['recipients'].first.key?('type') &&
|
313
|
+
pkg_data['recipients'].first['type'].eql?('dropbox')
|
314
|
+
|
315
|
+
shbx_kid = pkg_data['recipients'].first['id']
|
316
|
+
meta_schema = read("dropboxes/#{shbx_kid}")[:data]['metadata_schema']
|
317
|
+
if meta_schema.nil? || meta_schema.empty?
|
318
|
+
Log.log.debug('no metadata in shared inbox')
|
319
|
+
return
|
320
|
+
end
|
321
|
+
pkg_meta = pkg_data['metadata']
|
322
|
+
raise "package requires metadata: #{meta_schema}" unless pkg_data.key?('metadata')
|
323
|
+
raise 'metadata must be an Array' unless pkg_meta.is_a?(Array)
|
324
|
+
Log.dump(:metadata, pkg_meta)
|
325
|
+
pkg_meta.each do |field|
|
326
|
+
raise 'metadata field must be Hash' unless field.is_a?(Hash)
|
327
|
+
raise 'metadata field must have name' unless field.key?('name')
|
328
|
+
raise 'metadata field must have values' unless field.key?('values')
|
329
|
+
raise 'metadata values must be an Array' unless field['values'].is_a?(Array)
|
330
|
+
raise "unknown metadata field: #{field['name']}" if meta_schema.select{|i|i['name'].eql?(field['name'])}.empty?
|
331
|
+
end
|
332
|
+
meta_schema.each do |field|
|
333
|
+
provided = pkg_meta.select{|i|i['name'].eql?(field['name'])}
|
334
|
+
raise "only one field with name #{field['name']} allowed" if provided.count > 1
|
335
|
+
raise "missing mandatory field: #{field['name']}" if field['required'] && provided.empty?
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
# Normalize package creation recipient lists as expected by AoC API
|
340
|
+
# AoC expects {type: , id: }, but ascli allows providing either the native values or just a name
|
341
|
+
# in that case, the name is resolved and replaced with {type: , id: }
|
342
|
+
# @param package_data The whole package creation payload
|
343
|
+
# @param recipient_list_field The field in structure, i.e. recipients or bcc_recipients
|
344
|
+
# @return nil package_data is modified
|
345
|
+
def resolve_package_recipients(package_data, ws_id, recipient_list_field, new_user_option)
|
346
|
+
return unless package_data.key?(recipient_list_field)
|
347
|
+
raise CliBadArgument, "#{recipient_list_field} must be an Array" unless package_data[recipient_list_field].is_a?(Array)
|
348
|
+
new_user_option = {'package_contact' => true} if new_user_option.nil?
|
349
|
+
# list with resolved elements
|
350
|
+
resolved_list = []
|
351
|
+
package_data[recipient_list_field].each do |short_recipient_info|
|
352
|
+
case short_recipient_info
|
353
|
+
when Hash # native API information, check keys
|
354
|
+
raise "#{recipient_list_field} element shall have fields: id and type" unless short_recipient_info.keys.sort.eql?(%w[id type])
|
355
|
+
when String # CLI helper: need to resolve provided name to type/id
|
356
|
+
# email: user, else dropbox
|
357
|
+
entity_type = short_recipient_info.include?('@') ? 'contacts' : 'dropboxes'
|
358
|
+
begin
|
359
|
+
full_recipient_info = lookup_entity_by_name(entity_type, short_recipient_info, {'current_workspace_id' => ws_id})
|
360
|
+
rescue RuntimeError => e
|
361
|
+
raise e unless e.message.start_with?(ENTITY_NOT_FOUND)
|
362
|
+
# dropboxes cannot be created on the fly
|
363
|
+
raise "No such shared inbox in workspace #{ws_id}" if entity_type.eql?('dropboxes')
|
364
|
+
# unknown user: create it as external user
|
365
|
+
full_recipient_info = create('contacts', {
|
366
|
+
'current_workspace_id' => ws_id,
|
367
|
+
'email' => short_recipient_info}.merge(new_user_option))[:data]
|
368
|
+
end
|
369
|
+
short_recipient_info = if entity_type.eql?('dropboxes')
|
370
|
+
{'id' => full_recipient_info['id'], 'type' => 'dropbox'}
|
371
|
+
else
|
372
|
+
{'id' => full_recipient_info['source_id'], 'type' => full_recipient_info['source_type']}
|
373
|
+
end
|
374
|
+
else # unexpected extended value, must be String or Hash
|
375
|
+
raise "#{recipient_list_field} item must be a String (email, shared inbox) or Hash (id,type)"
|
376
|
+
end # type of recipient info
|
377
|
+
# add original or resolved recipient info
|
378
|
+
resolved_list.push(short_recipient_info)
|
379
|
+
end
|
380
|
+
# replace with resolved elements
|
381
|
+
package_data[recipient_list_field] = resolved_list
|
382
|
+
return nil
|
383
|
+
end
|
384
|
+
|
385
|
+
# Add transferspec
|
386
|
+
# callback in Aspera::Node (transfer_spec_gen4)
|
387
|
+
def add_ts_tags(transfer_spec:, app_info:)
|
388
|
+
# translate transfer direction to upload/download
|
389
|
+
transfer_type = Fasp::TransferSpec.action(transfer_spec)
|
390
|
+
# Analytics tags
|
391
|
+
################
|
392
|
+
ws_info = app_info[:plugin].current_workspace_info
|
393
|
+
transfer_spec.deep_merge!({
|
394
|
+
'tags' => {
|
395
|
+
'aspera' => {
|
396
|
+
'usage_id' => "aspera.files.workspace.#{ws_info['id']}", # activity tracking
|
397
|
+
'files' => {
|
398
|
+
'files_transfer_action' => "#{transfer_type}_#{app_info[:app].gsub(/s$/, '')}",
|
399
|
+
'workspace_name' => ws_info['name'], # activity tracking
|
400
|
+
'workspace_id' => ws_info['id']
|
401
|
+
}
|
402
|
+
}
|
403
|
+
}
|
404
|
+
})
|
405
|
+
# Console cookie
|
406
|
+
################
|
407
|
+
# we are sure that fields are not nil
|
408
|
+
cookie_elements = [app_info[:app], user_info['name'], user_info['email']].map{|e|Base64.strict_encode64(e)}
|
409
|
+
cookie_elements.unshift(COOKIE_PREFIX_CONSOLE_AOC)
|
410
|
+
transfer_spec['cookie'] = cookie_elements.join(':')
|
411
|
+
# Application tags
|
412
|
+
##################
|
413
|
+
case app_info[:app]
|
414
|
+
when FILES_APP
|
415
|
+
file_id = transfer_spec['tags']['aspera']['node']['file_id']
|
416
|
+
transfer_spec.deep_merge!({'tags' => {'aspera' => {'files' => {'parentCwd' => "#{app_info[:node_info]['id']}:#{file_id}"}}}}) \
|
417
|
+
unless transfer_spec.key?('remote_access_key')
|
418
|
+
when PACKAGES_APP
|
419
|
+
transfer_spec.deep_merge!({
|
420
|
+
'tags' => {
|
421
|
+
'aspera' => {
|
422
|
+
'files' => {
|
423
|
+
'package_id' => app_info[:package_info]['id'],
|
424
|
+
'package_name' => app_info[:package_info]['name'],
|
425
|
+
'package_operation' => transfer_type
|
426
|
+
}}}})
|
427
|
+
end
|
428
|
+
transfer_spec['tags']['aspera']['files']['node_id'] = app_info[:node_info]['id']
|
429
|
+
transfer_spec['tags']['aspera']['app'] = app_info[:app]
|
430
|
+
end
|
431
|
+
|
432
|
+
ID_AK_ADMIN = 'ASPERA_ACCESS_KEY_ADMIN'
|
433
|
+
# Callback from Plugins::Node
|
434
|
+
def permissions_create_params(create_param:, app_info:)
|
435
|
+
# workspace shared folder:
|
436
|
+
# access_id = "#{ID_AK_ADMIN}_WS_#{ app_info[:plugin].current_workspace_info['id']}"
|
437
|
+
default_params = {
|
438
|
+
# 'access_type' => 'user', # mandatory: user or group
|
439
|
+
# 'access_id' => access_id, # id of user or group
|
440
|
+
'tags' => {
|
441
|
+
'aspera' => {
|
442
|
+
'files' => {
|
443
|
+
'workspace' => {
|
444
|
+
'id' => app_info[:plugin].current_workspace_info['id'],
|
445
|
+
'workspace_name' => app_info[:plugin].current_workspace_info['name'],
|
446
|
+
'user_name' => user_info['name'],
|
447
|
+
'shared_by_user_id' => user_info['id'],
|
448
|
+
'shared_by_name' => user_info['name'],
|
449
|
+
'shared_by_email' => user_info['email'],
|
450
|
+
# 'shared_with_name' => access_id,
|
451
|
+
'access_key' => app_info[:node_info]['access_key'],
|
452
|
+
'node' => app_info[:node_info]['name']}}}}}
|
453
|
+
create_param.deep_merge!(default_params)
|
454
|
+
if create_param.key?('with')
|
455
|
+
contact_info = lookup_entity_by_name(
|
456
|
+
'contacts',
|
457
|
+
create_param['with'],
|
458
|
+
{'current_workspace_id' => app_info[:plugin].current_workspace_info['id'], 'context' => 'share_folder'})
|
459
|
+
create_param.delete('with')
|
460
|
+
create_param['access_type'] = contact_info['source_type']
|
461
|
+
create_param['access_id'] = contact_info['source_id']
|
462
|
+
create_param['tags']['aspera']['files']['workspace']['shared_with_name'] = contact_info['email']
|
463
|
+
end
|
464
|
+
# optionnal
|
465
|
+
app_info[:opt_link_name] = create_param.delete('link_name')
|
466
|
+
end
|
467
|
+
|
468
|
+
# Callback from Plugins::Node
|
469
|
+
def permissions_create_event(created_data:, app_info:)
|
470
|
+
event_creation = {
|
471
|
+
'types' => ['permission.created'],
|
472
|
+
'node_id' => app_info[:node_info]['id'],
|
473
|
+
'workspace_id' => app_info[:plugin].current_workspace_info['id'],
|
474
|
+
'data' => created_data # Response from previous step
|
475
|
+
}
|
476
|
+
# (optional). The name of the folder to be displayed to the destination user. Use it if its value is different from the "share_as" field.
|
477
|
+
event_creation['link_name'] = app_info[:opt_link_name] unless app_info[:opt_link_name].nil?
|
478
|
+
create('events', event_creation)
|
479
|
+
end
|
473
480
|
end # AoC
|
474
481
|
end # Aspera
|