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