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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +1 -0
  3. data/README.md +1894 -1574
  4. data/bin/ascli +21 -1
  5. data/bin/asession +38 -34
  6. data/docs/test_env.conf +14 -3
  7. data/examples/aoc.rb +17 -15
  8. data/examples/dascli +26 -0
  9. data/examples/faspex4.rb +42 -35
  10. data/examples/proxy.pac +1 -1
  11. data/examples/transfer.rb +38 -37
  12. data/lib/aspera/aoc.rb +245 -205
  13. data/lib/aspera/ascmd.rb +111 -90
  14. data/lib/aspera/ats_api.rb +16 -14
  15. data/lib/aspera/cli/basic_auth_plugin.rb +19 -18
  16. data/lib/aspera/cli/extended_value.rb +50 -39
  17. data/lib/aspera/cli/formater.rb +161 -135
  18. data/lib/aspera/cli/info.rb +18 -0
  19. data/lib/aspera/cli/listener/line_dump.rb +4 -2
  20. data/lib/aspera/cli/listener/logger.rb +3 -1
  21. data/lib/aspera/cli/listener/progress.rb +20 -21
  22. data/lib/aspera/cli/listener/progress_multi.rb +29 -31
  23. data/lib/aspera/cli/main.rb +194 -183
  24. data/lib/aspera/cli/manager.rb +213 -206
  25. data/lib/aspera/cli/plugin.rb +71 -49
  26. data/lib/aspera/cli/plugins/alee.rb +8 -7
  27. data/lib/aspera/cli/plugins/aoc.rb +675 -558
  28. data/lib/aspera/cli/plugins/ats.rb +116 -109
  29. data/lib/aspera/cli/plugins/bss.rb +35 -34
  30. data/lib/aspera/cli/plugins/config.rb +722 -542
  31. data/lib/aspera/cli/plugins/console.rb +28 -22
  32. data/lib/aspera/cli/plugins/cos.rb +28 -37
  33. data/lib/aspera/cli/plugins/faspex.rb +281 -227
  34. data/lib/aspera/cli/plugins/faspex5.rb +129 -84
  35. data/lib/aspera/cli/plugins/node.rb +426 -232
  36. data/lib/aspera/cli/plugins/orchestrator.rb +106 -98
  37. data/lib/aspera/cli/plugins/preview.rb +196 -191
  38. data/lib/aspera/cli/plugins/server.rb +131 -126
  39. data/lib/aspera/cli/plugins/shares.rb +49 -36
  40. data/lib/aspera/cli/plugins/sync.rb +27 -28
  41. data/lib/aspera/cli/transfer_agent.rb +84 -79
  42. data/lib/aspera/cli/version.rb +3 -1
  43. data/lib/aspera/colors.rb +37 -28
  44. data/lib/aspera/command_line_builder.rb +84 -63
  45. data/lib/aspera/cos_node.rb +68 -34
  46. data/lib/aspera/data_repository.rb +4 -2
  47. data/lib/aspera/environment.rb +61 -46
  48. data/lib/aspera/fasp/agent_base.rb +36 -31
  49. data/lib/aspera/fasp/agent_connect.rb +44 -37
  50. data/lib/aspera/fasp/agent_direct.rb +101 -104
  51. data/lib/aspera/fasp/agent_httpgw.rb +91 -90
  52. data/lib/aspera/fasp/agent_node.rb +36 -33
  53. data/lib/aspera/fasp/agent_trsdk.rb +28 -31
  54. data/lib/aspera/fasp/error.rb +3 -1
  55. data/lib/aspera/fasp/error_info.rb +81 -54
  56. data/lib/aspera/fasp/installation.rb +171 -151
  57. data/lib/aspera/fasp/listener.rb +2 -0
  58. data/lib/aspera/fasp/parameters.rb +105 -111
  59. data/lib/aspera/fasp/parameters.yaml +305 -249
  60. data/lib/aspera/fasp/resume_policy.rb +20 -20
  61. data/lib/aspera/fasp/transfer_spec.rb +27 -0
  62. data/lib/aspera/fasp/uri.rb +31 -29
  63. data/lib/aspera/faspex_gw.rb +95 -118
  64. data/lib/aspera/hash_ext.rb +12 -13
  65. data/lib/aspera/id_generator.rb +11 -9
  66. data/lib/aspera/keychain/encrypted_hash.rb +73 -57
  67. data/lib/aspera/keychain/macos_security.rb +27 -29
  68. data/lib/aspera/log.rb +40 -39
  69. data/lib/aspera/nagios.rb +24 -22
  70. data/lib/aspera/node.rb +38 -30
  71. data/lib/aspera/oauth.rb +217 -248
  72. data/lib/aspera/open_application.rb +9 -7
  73. data/lib/aspera/persistency_action_once.rb +15 -14
  74. data/lib/aspera/persistency_folder.rb +15 -18
  75. data/lib/aspera/preview/file_types.rb +266 -270
  76. data/lib/aspera/preview/generator.rb +94 -92
  77. data/lib/aspera/preview/image_error.png +0 -0
  78. data/lib/aspera/preview/options.rb +20 -17
  79. data/lib/aspera/preview/utils.rb +99 -102
  80. data/lib/aspera/preview/video_error.png +0 -0
  81. data/lib/aspera/{proxy_auto_config.erb.js → proxy_auto_config.js} +23 -31
  82. data/lib/aspera/proxy_auto_config.rb +114 -21
  83. data/lib/aspera/rest.rb +144 -142
  84. data/lib/aspera/rest_call_error.rb +3 -2
  85. data/lib/aspera/rest_error_analyzer.rb +31 -31
  86. data/lib/aspera/rest_errors_aspera.rb +18 -16
  87. data/lib/aspera/secret_hider.rb +68 -0
  88. data/lib/aspera/ssh.rb +20 -16
  89. data/lib/aspera/sync.rb +57 -54
  90. data/lib/aspera/temp_file_manager.rb +20 -14
  91. data/lib/aspera/timer_limiter.rb +10 -8
  92. data/lib/aspera/uri_reader.rb +14 -15
  93. data/lib/aspera/web_auth.rb +85 -80
  94. data.tar.gz.sig +0 -0
  95. metadata +169 -40
  96. metadata.gz.sig +2 -0
  97. data/bin/dascli +0 -13
  98. data/docs/Makefile +0 -63
  99. data/docs/README.erb.md +0 -4221
  100. data/docs/README.md +0 -13
  101. data/docs/diagrams.txt +0 -49
  102. data/docs/doc_tools.rb +0 -58
  103. data/lib/aspera/cli/plugins/shares2.rb +0 -114
  104. 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/default'
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
- private
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
- CLIENT_APPS=['aspera.global-cli-client','aspera.drive']
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=['/packages/public/receive','/packages/public/send','/files/public']
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=['name','email','id','default_workspace_id','organization_id']
40
+ USER_INFO_FIELDS_MIN = %w[name email id default_workspace_id organization_id].freeze
32
41
 
33
- private_constant :PRODUCT_NAME,:PROD_DOMAIN,:MAX_REDIRECT,:CLIENT_APPS,:PUBLIC_LINK_PATHS,:JWT_AUDIENCE,
34
- :OAUTH_API_SUBPATH,:COOKIE_PREFIX,:USER_INFO_FIELDS_MIN
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=CLIENT_APPS.first)
53
- client_index=CLIENT_APPS.index(client_name)
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.get_bin(DATA_REPO_INDEX_START+client_index))
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=PROD_DOMAIN)
74
- return "https://api.#{api_domain}"
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
- :base_url => "#{api_base_url(api_domain)}/metering/v1",
80
- :headers => {'X-Aspera-Entitlement-Authorization' => Rest.basic_creds(entitlement_id,customer_id)}
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 'node.'+access_key+':'+scope
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(rest_opts,public_link_url)
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
- # set to token if available after redirection
99
- url_param_token_pair=nil
100
- redirect_count=0
101
- loop do
102
- uri=URI.parse(public_link_url)
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::decode_www_form(uri.query).select{|e|e.first.eql?('token')}.first
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
- rest_opts[:org_url]='https://'+uri.host
110
- rest_opts[:auth][:grant]=:url_token
111
- rest_opts[:auth][:url_token]=url_param_token_pair.last
112
- return
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
- if r.code.start_with?("3")
118
- public_link_url = r['location']
119
- raise "no location in redirection" if public_link_url.nil?
120
- Log.log.debug("redirect to: #{public_link_url}")
121
- else
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=case direction
143
- when 'send'; 'upload'
144
- when 'receive'; 'download'
145
- else raise "ERROR: unexpected value: #{direction}"
146
- end
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
- 'aspera' => {
151
- 'usage_id' => "aspera.files.workspace.#{ws_id}", # activity tracking
152
- 'files' => {
153
- 'files_transfer_action' => "#{operation}_#{app.gsub(/s$/,'')}",
154
- 'workspace_name' => ws_name, # activity tracking
155
- 'workspace_id' => ws_id,
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={:auth=>{:type =>:oauth2}}
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 [:org_url], [:auth][:grant], [:auth][:url_token]
177
- self.class.resolve_pub_link(aoc_rest_p,opt[: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
- if aoc_rest_p.has_key?(:org_url)
180
- # Pub Link only: get org url from pub link
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 !aoc_auth_p.has_key?(:grant)
200
- raise ArgumentError,"Missing mandatory option: auth" if opt[:auth].nil?
201
- aoc_auth_p[:grant] = opt[:auth]
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[:grant]
218
+ case aoc_auth_p[:crtype]
213
219
  when :web
214
- raise ArgumentError,"Missing mandatory option: redirect_uri" if opt[:redirect_uri].nil?
215
- aoc_auth_p[:redirect_uri] = opt[:redirect_uri]
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 CLIENT_APPS.include?(aoc_auth_p[:client_id])
219
- aoc_auth_p.merge!({:jwt_add=>{org: organization}})
220
- end
221
- raise ArgumentError,"Missing mandatory option: private_key" if opt[:private_key].nil?
222
- raise ArgumentError,"Missing mandatory option: username" if opt[:username].nil?
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 "keychain already set" unless @key_chain.nil?
237
- raise "keychain must have get_secret" unless keychain.respond_to?(:get_secret)
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=self.read('self')[:data] rescue nil
247
- @user_info=USER_INFO_FIELDS_MIN.inject({}){|m,f|m[f]=nil;m} if @user_info.nil?
248
- USER_INFO_FIELDS_MIN.each{|f|@user_info[f]='unknown' if @user_info[f].nil?}
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
- # prepare the rest end point is used to generate the bearer token
266
- token_generation_lambda=lambda {|do_refresh|self.oauth_token(scope: self.class.node_scope(node_file[:node_info]['access_key'],SCOPE_NODE_USER), refresh: do_refresh)}
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' => direction,
271
- 'token' => token_generation_lambda.call(false), # first time, use cache
272
- 'tags' => {
273
- 'aspera' => {
274
- 'app' => app,
275
- 'files' => {
276
- 'node_id' => node_file[:node_info]['id'],
277
- }, # files
278
- 'node' => {
279
- 'access_key' => node_file[:node_info]['access_key'],
280
- #'file_id' => ts_add['source_root_id']
281
- 'file_id' => node_file[:file_id]
282
- } # node
283
- } # aspera
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 @@use_standard_ports
307
+ if self.class.use_standard_ports
288
308
  # get default TCP/UDP ports and transfer user
289
- transfer_spec.merge!(Fasp::Default::AK_TSPEC_BASE)
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) and !node_file[:node_info]['transfer_url'].empty?
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=get_node_api(node_file[:node_info],scope: SCOPE_NODE_USER).create('files/download_setup',{:transfer_requests => [ { :transfer_request => {:paths => [ {"source"=>'/'} ] } } ] } )[:data]['transfer_specs'].first['transfer_spec']
299
- ['remote_host','remote_user','ssh_port','fasp_port'].each {|i| transfer_spec[i]=std_t_spec[i]}
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
- :src => :node_gen4,
306
- :regenerate_token => token_generation_lambda
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,options={})
316
- raise "INTERNAL ERROR: method parameters: options must be Hash" unless options.is_a?(Hash)
317
- options.keys.each {|k| raise "INTERNAL ERROR: not valid option: #{k}" unless [:scope,:use_secret].include?(k)}
318
- # get optional secret unless :use_secret is false (default is true)
319
- ak_secret=@key_chain.get_secret(url: node_info['url'], username: node_info['access_key'], mandatory: false) if !@key_chain.nil? and ( !options.has_key?(:use_secret) or options[:use_secret] )
320
- if ak_secret.nil? and !options.has_key?(:scope)
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]=self.params[:auth].clone
335
- node_rest_params[:auth][:scope]=self.class.node_scope(node_info['access_key'],options[:scope])
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 "node_file must have 2 keys: :file_id and :node_info" unless node_file.keys.sort.eql?([:file_id,:node_info])
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=self.read("nodes/#{entry['target_node_id']}")[:data]
361
- sub_opt={method: process_find_files, top_file_id: entry['target_id'], top_file_path: path}
362
- get_node_api(sub_node_info,scope: SCOPE_NODE_USER).crawl(self,sub_opt)
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( top_node_file, test_block )
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,scope: SCOPE_NODE_USER).crawl(self,{method: :process_find_files, top_file_id: top_file_id})
376
- result=@find_state[:found]
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,path)
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]=self.read("nodes/#{entry['target_node_id']}")[:data]
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],scope: SCOPE_NODE_USER).crawl(self,{method: :process_resolve_node_file, top_file_id: entry['target_id']})
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( top_node_file, element_path_string )
416
- top_node_info,top_file_id=check_get_node_file(top_node_file)
417
- path_elements=element_path_string.split(PATH_SEPARATOR).select{|i| !i.empty?}
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,scope: SCOPE_NODE_USER).crawl(self,{method: :process_resolve_node_file, top_file_id: top_file_id})
424
- not_found=@resolve_state[:path]
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