aspera-cli 4.15.0 → 4.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/BUGS.md +29 -3
- data/CHANGELOG.md +292 -228
- data/CONTRIBUTING.md +69 -18
- data/README.md +1102 -952
- data/bin/ascli +13 -31
- data/bin/asession +3 -1
- data/examples/dascli +2 -2
- data/lib/aspera/aoc.rb +28 -33
- data/lib/aspera/ascmd.rb +3 -6
- data/lib/aspera/assert.rb +45 -0
- data/lib/aspera/cli/extended_value.rb +5 -5
- data/lib/aspera/cli/formatter.rb +26 -13
- data/lib/aspera/cli/hints.rb +4 -3
- data/lib/aspera/cli/main.rb +16 -3
- data/lib/aspera/cli/manager.rb +45 -36
- data/lib/aspera/cli/plugin.rb +20 -13
- data/lib/aspera/cli/plugins/aoc.rb +103 -73
- data/lib/aspera/cli/plugins/ats.rb +4 -3
- data/lib/aspera/cli/plugins/config.rb +114 -119
- data/lib/aspera/cli/plugins/cos.rb +2 -2
- data/lib/aspera/cli/plugins/faspex.rb +23 -19
- data/lib/aspera/cli/plugins/faspex5.rb +75 -43
- data/lib/aspera/cli/plugins/node.rb +28 -15
- data/lib/aspera/cli/plugins/orchestrator.rb +4 -2
- data/lib/aspera/cli/plugins/preview.rb +9 -7
- data/lib/aspera/cli/plugins/server.rb +6 -3
- data/lib/aspera/cli/plugins/shares.rb +30 -26
- data/lib/aspera/cli/sync_actions.rb +9 -9
- data/lib/aspera/cli/transfer_agent.rb +21 -14
- data/lib/aspera/cli/transfer_progress.rb +2 -3
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +13 -11
- data/lib/aspera/cos_node.rb +3 -2
- data/lib/aspera/coverage.rb +22 -0
- data/lib/aspera/data_repository.rb +33 -2
- data/lib/aspera/environment.rb +4 -2
- data/lib/aspera/fasp/{agent_aspera.rb → agent_alpha.rb} +29 -39
- data/lib/aspera/fasp/agent_base.rb +17 -7
- data/lib/aspera/fasp/agent_direct.rb +88 -84
- data/lib/aspera/fasp/agent_httpgw.rb +4 -3
- data/lib/aspera/fasp/agent_node.rb +3 -2
- data/lib/aspera/fasp/agent_trsdk.rb +79 -37
- data/lib/aspera/fasp/installation.rb +51 -12
- data/lib/aspera/fasp/management.rb +11 -6
- data/lib/aspera/fasp/parameters.rb +53 -47
- data/lib/aspera/fasp/resume_policy.rb +7 -5
- data/lib/aspera/fasp/sync.rb +273 -0
- data/lib/aspera/fasp/transfer_spec.rb +10 -8
- data/lib/aspera/fasp/uri.rb +2 -2
- data/lib/aspera/faspex_gw.rb +11 -8
- data/lib/aspera/faspex_postproc.rb +6 -5
- data/lib/aspera/id_generator.rb +3 -1
- data/lib/aspera/json_rpc.rb +10 -8
- data/lib/aspera/keychain/encrypted_hash.rb +46 -11
- data/lib/aspera/keychain/macos_security.rb +15 -13
- data/lib/aspera/log.rb +4 -3
- data/lib/aspera/nagios.rb +7 -2
- data/lib/aspera/node.rb +17 -16
- data/lib/aspera/node_simulator.rb +214 -0
- data/lib/aspera/oauth.rb +22 -19
- data/lib/aspera/persistency_action_once.rb +13 -14
- data/lib/aspera/persistency_folder.rb +3 -2
- data/lib/aspera/preview/file_types.rb +53 -267
- data/lib/aspera/preview/generator.rb +7 -5
- data/lib/aspera/preview/terminal.rb +14 -5
- data/lib/aspera/preview/utils.rb +8 -7
- data/lib/aspera/proxy_auto_config.rb +6 -3
- data/lib/aspera/rest.rb +29 -13
- data/lib/aspera/rest_error_analyzer.rb +1 -0
- data/lib/aspera/rest_errors_aspera.rb +2 -0
- data/lib/aspera/secret_hider.rb +5 -2
- data/lib/aspera/ssh.rb +10 -8
- data/lib/aspera/temp_file_manager.rb +1 -1
- data/lib/aspera/web_server_simple.rb +2 -1
- data.tar.gz.sig +0 -0
- metadata +96 -45
- metadata.gz.sig +0 -0
- data/lib/aspera/sync.rb +0 -219
data/lib/aspera/log.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'aspera/colors'
|
4
4
|
require 'aspera/secret_hider'
|
5
5
|
require 'aspera/environment'
|
6
|
+
require 'aspera/assert'
|
6
7
|
require 'logger'
|
7
8
|
require 'pp'
|
8
9
|
require 'json'
|
@@ -39,6 +40,7 @@ class Logger
|
|
39
40
|
end
|
40
41
|
EOM
|
41
42
|
end
|
43
|
+
# declare methods for all levels
|
42
44
|
Logger::Severity.constants.each { |severity| make_methods(severity) }
|
43
45
|
end
|
44
46
|
|
@@ -97,8 +99,7 @@ module Aspera
|
|
97
99
|
Logger::Severity.constants.each do |name|
|
98
100
|
return name.downcase.to_sym if @logger.level.eql?(Logger::Severity.const_get(name))
|
99
101
|
end
|
100
|
-
|
101
|
-
raise "INTERNAL ERROR: unexpected level #{@logger.level}"
|
102
|
+
error_unexpected_value(@logger.level){'log level'}
|
102
103
|
end
|
103
104
|
|
104
105
|
# change underlying logger, but keep log level
|
@@ -123,7 +124,7 @@ module Aspera
|
|
123
124
|
end
|
124
125
|
@logger = Syslog::Logger.new(@program_name, Syslog::LOG_LOCAL2)
|
125
126
|
else
|
126
|
-
raise "unknown log type: #{new_log_type
|
127
|
+
raise "unknown log type: #{new_log_type}, use one of: #{LOG_TYPES.join(', ')}"
|
127
128
|
end
|
128
129
|
@logger.level = current_severity_integer
|
129
130
|
@logger_type = new_log_type
|
data/lib/aspera/nagios.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'aspera/log'
|
4
|
+
require 'aspera/assert'
|
3
5
|
require 'date'
|
4
6
|
|
5
7
|
module Aspera
|
@@ -21,8 +23,11 @@ module Aspera
|
|
21
23
|
class << self
|
22
24
|
# process results of a analysis and display status and exit with code
|
23
25
|
def process(data)
|
24
|
-
|
25
|
-
|
26
|
+
assert_type(data, Array)
|
27
|
+
assert(!data.empty?){'data is empty'}
|
28
|
+
%w[status component message].each do |c|
|
29
|
+
assert(data.first.key?(c)){"result must have #{c}"}
|
30
|
+
end
|
26
31
|
res_errors = data.reject{|s|s['status'].eql?('ok')}
|
27
32
|
# keep only errors in case of problem, other ok are assumed so
|
28
33
|
data = res_errors unless res_errors.empty?
|
data/lib/aspera/node.rb
CHANGED
@@ -5,6 +5,7 @@ require 'aspera/fasp/transfer_spec'
|
|
5
5
|
require 'aspera/rest'
|
6
6
|
require 'aspera/oauth'
|
7
7
|
require 'aspera/log'
|
8
|
+
require 'aspera/assert'
|
8
9
|
require 'aspera/environment'
|
9
10
|
require 'zlib'
|
10
11
|
require 'base64'
|
@@ -50,7 +51,7 @@ module Aspera
|
|
50
51
|
end
|
51
52
|
return lambda{|f|File.fnmatch(match_expression, f['name'], File::FNM_DOTMATCH)}
|
52
53
|
when NilClass then return ->(_){true}
|
53
|
-
else
|
54
|
+
else error_unexpected_value(match_expression.class.name, exception_class: Cli::BadArgument)
|
54
55
|
end
|
55
56
|
end
|
56
57
|
|
@@ -65,8 +66,8 @@ module Aspera
|
|
65
66
|
|
66
67
|
def decode_scope(scope)
|
67
68
|
items = scope.split(SCOPE_SEPARATOR, 2)
|
68
|
-
|
69
|
-
|
69
|
+
assert(items.length.eql?(2)){"invalid scope: #{scope}"}
|
70
|
+
assert(items[0].start_with?(SCOPE_PREFIX)){"invalid scope: #{scope}"}
|
70
71
|
return {access_key: items[0][SCOPE_PREFIX.length..-1], scope: items[1]}
|
71
72
|
end
|
72
73
|
|
@@ -74,11 +75,11 @@ module Aspera
|
|
74
75
|
# @param payload [String] JSON payload to be included in the token
|
75
76
|
# @param private_key [OpenSSL::PKey::RSA] Private key to sign the token
|
76
77
|
def bearer_token(access_key:, payload:, private_key:)
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
78
|
+
assert_type(payload, Hash)
|
79
|
+
assert(payload.key?('user_id'))
|
80
|
+
assert_type(payload['user_id'], String)
|
81
|
+
assert(!payload['user_id'].empty?)
|
82
|
+
assert_type(private_key, OpenSSL::PKey::RSA)
|
82
83
|
# manage convenience parameters
|
83
84
|
expiration_sec = payload['_validity'] || BEARER_TOKEN_VALIDITY_DEFAULT
|
84
85
|
payload.delete('_validity')
|
@@ -104,7 +105,7 @@ module Aspera
|
|
104
105
|
# if username is not provided, use the access key from the token
|
105
106
|
if access_key.nil?
|
106
107
|
access_key = Aspera::Node.decode_scope(Aspera::Node.decode_bearer_token(Oauth.bearer_extract(bearer_auth))['scope'])[:access_key]
|
107
|
-
|
108
|
+
assert(!access_key.nil?)
|
108
109
|
end
|
109
110
|
return {
|
110
111
|
Aspera::Node::HEADER_X_ASPERA_ACCESS_KEY => access_key,
|
@@ -131,10 +132,10 @@ module Aspera
|
|
131
132
|
@add_tspec = add_tspec
|
132
133
|
if !@app_info.nil?
|
133
134
|
REQUIRED_APP_INFO_FIELDS.each do |field|
|
134
|
-
|
135
|
+
assert(@app_info.key?(field)){"app_info lacks field #{field}"}
|
135
136
|
end
|
136
137
|
REQUIRED_APP_API_METHODS.each do |method|
|
137
|
-
|
138
|
+
assert(@app_info[:api].respond_to?(method)){"#{@app_info[:api].class} lacks method #{method}"}
|
138
139
|
end
|
139
140
|
end
|
140
141
|
end
|
@@ -165,8 +166,8 @@ module Aspera
|
|
165
166
|
# @param top_file_path [String] path of top folder (default = /)
|
166
167
|
# @param block [Proc] processing method, arguments: entry, path, state
|
167
168
|
def process_folder_tree(state:, top_file_id:, top_file_path: '/', &block)
|
168
|
-
|
169
|
-
|
169
|
+
assert(!top_file_path.nil?){'top_file_path not set'}
|
170
|
+
assert(block){'Missing block'}
|
170
171
|
# start at top folder
|
171
172
|
folders_to_explore = [{id: top_file_id, path: top_file_path}]
|
172
173
|
Log.log.debug{Log.dump(:folders_to_explore, folders_to_explore)}
|
@@ -207,7 +208,7 @@ module Aspera
|
|
207
208
|
# @param path [String] file path
|
208
209
|
# @return [Hash] {.api,.file_id}
|
209
210
|
def resolve_api_fid(top_file_id, path)
|
210
|
-
|
211
|
+
assert_type(top_file_id, String)
|
211
212
|
process_last_link = path.end_with?(PATH_SEPARATOR)
|
212
213
|
path_elements = path.split(PATH_SEPARATOR).reject(&:empty?)
|
213
214
|
return {api: self, file_id: top_file_id} if path_elements.empty?
|
@@ -276,14 +277,14 @@ module Aspera
|
|
276
277
|
case params[:auth][:type]
|
277
278
|
when :basic
|
278
279
|
ak_name = params[:auth][:username]
|
279
|
-
|
280
|
+
assert(params[:auth][:password]){'no secret in node object'}
|
280
281
|
ak_token = Rest.basic_token(params[:auth][:username], params[:auth][:password])
|
281
282
|
when :oauth2
|
282
283
|
ak_name = params[:headers][HEADER_X_ASPERA_ACCESS_KEY]
|
283
284
|
# TODO: token_generation_lambda = lambda{|do_refresh|oauth_token(force_refresh: do_refresh)}
|
284
285
|
# get bearer token, possibly use cache
|
285
286
|
ak_token = oauth_token(force_refresh: false)
|
286
|
-
else
|
287
|
+
else error_unexpected_value(params[:auth][:type])
|
287
288
|
end
|
288
289
|
transfer_spec = {
|
289
290
|
'direction' => direction,
|
@@ -0,0 +1,214 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'aspera/log'
|
4
|
+
require 'aspera/fasp/installation'
|
5
|
+
require 'aspera/fasp/agent_direct'
|
6
|
+
require 'webrick'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
module Aspera
|
10
|
+
# this class answers the Faspex /send API and creates a package on Aspera on Cloud
|
11
|
+
class NodeSimulatorServlet < WEBrick::HTTPServlet::AbstractServlet
|
12
|
+
PATH_TRANSFERS = '/ops/transfers'
|
13
|
+
PATH_ONE_TRANSFER = %r{/ops/transfers/(.+)$}
|
14
|
+
# @param app_api [Aspera::AoC]
|
15
|
+
# @param app_context [String]
|
16
|
+
def initialize(server, credentials, transfer)
|
17
|
+
super(server)
|
18
|
+
@credentials = credentials
|
19
|
+
@xfer_manager = Aspera::Fasp::AgentDirect.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def do_POST(request, response)
|
23
|
+
case request.path
|
24
|
+
when PATH_TRANSFERS
|
25
|
+
job_id = @xfer_manager.start_transfer(JSON.parse(request.body))
|
26
|
+
session = @xfer_manager.sessions_by_job(job_id).first
|
27
|
+
result = session[:ts].clone
|
28
|
+
result['id'] = job_id
|
29
|
+
set_json_response(response, result)
|
30
|
+
Log.log.debug{">>> transfer started: #{job_id}"}
|
31
|
+
else
|
32
|
+
set_json_response(response, [{error: 'Bad request'}], code: 400)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def do_GET(request, response)
|
37
|
+
case request.path
|
38
|
+
when '/info'
|
39
|
+
info = Aspera::Fasp::Installation.instance.ascp_info
|
40
|
+
set_json_response(response, {
|
41
|
+
application: 'node',
|
42
|
+
current_time: Time.now.utc.iso8601(0),
|
43
|
+
version: info['ascp_version'].gsub(/ .*$/, ''),
|
44
|
+
license_expiration_date: info['expiration_date'],
|
45
|
+
license_max_rate: info['maximum_bandwidth'],
|
46
|
+
os: %x(uname -srv).chomp,
|
47
|
+
aej_status: 'disconnected',
|
48
|
+
async_reporting: 'yes',
|
49
|
+
transfer_activity_reporting: 'yes',
|
50
|
+
transfer_user: 'xfer',
|
51
|
+
docroot: 'file:////data/aoc/eudemo-sedemo',
|
52
|
+
node_id: '2bbdcc39-f789-4d47-8163-6767fc14f421',
|
53
|
+
cluster_id: '6dae2844-d1a9-47a5-916d-9b3eac3ea466',
|
54
|
+
acls: [],
|
55
|
+
access_key_configuration_capabilities: {
|
56
|
+
transfer: %w[
|
57
|
+
cipher
|
58
|
+
policy
|
59
|
+
target_rate_cap_kbps
|
60
|
+
target_rate_kbps
|
61
|
+
preserve_timestamps
|
62
|
+
content_protection_secret
|
63
|
+
aggressiveness
|
64
|
+
token_encryption_key
|
65
|
+
byok_enabled
|
66
|
+
bandwidth_flow_network_rc_module
|
67
|
+
file_checksum_type],
|
68
|
+
server: %w[
|
69
|
+
activity_event_logging
|
70
|
+
activity_file_event_logging
|
71
|
+
recursive_counts
|
72
|
+
aej_logging
|
73
|
+
wss_enabled
|
74
|
+
activity_transfer_ignore_skipped_files
|
75
|
+
activity_files_max
|
76
|
+
access_key_credentials_encryption_type
|
77
|
+
discovery
|
78
|
+
auto_delete
|
79
|
+
allow
|
80
|
+
deny]
|
81
|
+
},
|
82
|
+
capabilities: [
|
83
|
+
{name: 'sync', value: true},
|
84
|
+
{name: 'watchfolder', value: true},
|
85
|
+
{name: 'symbolic_links', value: true},
|
86
|
+
{name: 'move_file', value: true},
|
87
|
+
{name: 'move_directory', value: true},
|
88
|
+
{name: 'filelock', value: false},
|
89
|
+
{name: 'ssh_fingerprint', value: false},
|
90
|
+
{name: 'aej_version', value: '1.0'},
|
91
|
+
{name: 'page', value: true},
|
92
|
+
{name: 'file_id_version', value: '2.0'},
|
93
|
+
{name: 'auto_delete', value: false}],
|
94
|
+
settings: [
|
95
|
+
{name: 'content_protection_required', value: false},
|
96
|
+
{name: 'content_protection_strong_pass_required', value: false},
|
97
|
+
{name: 'filelock_restriction', value: 'none'},
|
98
|
+
{name: 'ssh_fingerprint', value: nil},
|
99
|
+
{name: 'wss_enabled', value: false},
|
100
|
+
{name: 'wss_port', value: 443}
|
101
|
+
]})
|
102
|
+
when PATH_TRANSFERS
|
103
|
+
result = @xfer_manager.sessions.map { |session| job_to_transfer(session) }
|
104
|
+
set_json_response(response, result)
|
105
|
+
when PATH_ONE_TRANSFER
|
106
|
+
job_id = request.path.match(PATH_ONE_TRANSFER)[1]
|
107
|
+
set_json_response(response, job_to_transfer(@xfer_manager.sessions_by_job(job_id).first))
|
108
|
+
else
|
109
|
+
set_json_response(response, [{error: 'Unknown request'}], code: 400)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def set_json_response(response, json, code: 200)
|
114
|
+
response.status = code
|
115
|
+
response['Content-Type'] = 'application/json'
|
116
|
+
response.body = json.to_json
|
117
|
+
Log.log.trace1{Log.dump('response', json)}
|
118
|
+
end
|
119
|
+
|
120
|
+
def job_to_transfer(job)
|
121
|
+
session = {
|
122
|
+
id: 'bafc72b8-366c-4501-8095-47208183d6b8',
|
123
|
+
client_node_id: '',
|
124
|
+
server_node_id: '2bbdcc39-f789-4d47-8163-6767fc14f421',
|
125
|
+
client_ip_address: '192.168.0.100',
|
126
|
+
server_ip_address: '5.10.114.4',
|
127
|
+
status: 'running',
|
128
|
+
retry_timeout: 3600,
|
129
|
+
retry_count: 0,
|
130
|
+
start_time_usec: 1701094040000000,
|
131
|
+
end_time_usec: nil,
|
132
|
+
elapsed_usec: 405312,
|
133
|
+
bytes_transferred: 26,
|
134
|
+
bytes_written: 26,
|
135
|
+
bytes_lost: 0,
|
136
|
+
files_completed: 1,
|
137
|
+
directories_completed: 0,
|
138
|
+
target_rate_kbps: 500000,
|
139
|
+
min_rate_kbps: 0,
|
140
|
+
calc_rate_kbps: 9900,
|
141
|
+
network_delay_usec: 40000,
|
142
|
+
avg_rate_kbps: 0.51,
|
143
|
+
error_code: 0,
|
144
|
+
error_desc: '',
|
145
|
+
source_statistics: {
|
146
|
+
args_scan_attempted: 1,
|
147
|
+
args_scan_completed: 1,
|
148
|
+
paths_scan_attempted: 1,
|
149
|
+
paths_scan_failed: 0,
|
150
|
+
paths_scan_skipped: 0,
|
151
|
+
paths_scan_excluded: 0,
|
152
|
+
dirs_scan_completed: 0,
|
153
|
+
files_scan_completed: 1,
|
154
|
+
dirs_xfer_attempted: 0,
|
155
|
+
dirs_xfer_fail: 0,
|
156
|
+
files_xfer_attempted: 1,
|
157
|
+
files_xfer_fail: 0,
|
158
|
+
files_xfer_noxfer: 0
|
159
|
+
},
|
160
|
+
precalc: {
|
161
|
+
enabled: true,
|
162
|
+
status: 'ready',
|
163
|
+
bytes_expected: 0,
|
164
|
+
directories_expected: 0,
|
165
|
+
files_expected: 0,
|
166
|
+
files_excluded: 0,
|
167
|
+
files_special: 0,
|
168
|
+
files_failed: 1
|
169
|
+
}}
|
170
|
+
return {
|
171
|
+
id: '609a667d-642e-4290-9312-b4d20d3c0159',
|
172
|
+
status: 'running',
|
173
|
+
start_spec: job[:ts],
|
174
|
+
sessions: [session],
|
175
|
+
bytes_transferred: 26,
|
176
|
+
bytes_written: 26,
|
177
|
+
bytes_lost: 0,
|
178
|
+
avg_rate_kbps: 0.51,
|
179
|
+
files_completed: 1,
|
180
|
+
files_skipped: 0,
|
181
|
+
directories_completed: 0,
|
182
|
+
start_time_usec: 1701094040000000,
|
183
|
+
end_time_usec: 1701094040405312,
|
184
|
+
elapsed_usec: 405312,
|
185
|
+
error_code: 0,
|
186
|
+
error_desc: '',
|
187
|
+
precalc: {
|
188
|
+
status: 'ready',
|
189
|
+
bytes_expected: 0,
|
190
|
+
files_expected: 0,
|
191
|
+
directories_expected: 0,
|
192
|
+
files_special: 0,
|
193
|
+
files_failed: 1
|
194
|
+
},
|
195
|
+
files: [{
|
196
|
+
id: 'd1b5c112-82b75425-860745fc-93851671-64541bdd',
|
197
|
+
path: '/workspaces/45071/packages/bYA_ilq73g.asp-package/contents/data_file.bin',
|
198
|
+
start_time_usec: 1701094040000000,
|
199
|
+
elapsed_usec: 105616,
|
200
|
+
end_time_usec: 1701094040001355,
|
201
|
+
status: 'completed',
|
202
|
+
error_code: 0,
|
203
|
+
error_desc: '',
|
204
|
+
size: 26,
|
205
|
+
type: 'file',
|
206
|
+
checksum_type: 'none',
|
207
|
+
checksum: nil,
|
208
|
+
start_byte: 0,
|
209
|
+
bytes_written: 26,
|
210
|
+
session_id: 'bafc72b8-366c-4501-8095-47208183d6b8'}]
|
211
|
+
}
|
212
|
+
end
|
213
|
+
end # NodeSimulatorServlet
|
214
|
+
end # Aspera
|
data/lib/aspera/oauth.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
require 'aspera/open_application'
|
4
4
|
require 'aspera/web_auth'
|
5
5
|
require 'aspera/id_generator'
|
6
|
+
require 'aspera/log'
|
7
|
+
require 'aspera/assert'
|
6
8
|
require 'base64'
|
7
9
|
require 'date'
|
8
10
|
require 'socket'
|
@@ -55,7 +57,7 @@ module Aspera
|
|
55
57
|
end
|
56
58
|
|
57
59
|
def bearer_extract(token)
|
58
|
-
|
60
|
+
assert(bearer?(token)){'not a bearer token, wrong prefix'}
|
59
61
|
return token[BEARER_PREFIX.length..-1]
|
60
62
|
end
|
61
63
|
|
@@ -106,26 +108,28 @@ module Aspera
|
|
106
108
|
# @param id_create called to generate unique id for token, for cache
|
107
109
|
def register_token_creator(id, lambda_create, id_create)
|
108
110
|
Log.log.debug{"registering token creator #{id}"}
|
109
|
-
|
111
|
+
assert_type(id, Symbol)
|
112
|
+
assert_type(lambda_create, Proc)
|
113
|
+
assert_type(id_create, Proc)
|
110
114
|
@create_handlers[id] = lambda_create
|
111
115
|
@id_handlers[id] = id_create
|
112
116
|
end
|
113
117
|
|
114
118
|
# @return one of the registered creators for the given create type
|
115
119
|
def token_creator(id)
|
116
|
-
|
120
|
+
assert(@create_handlers.key?(id)){"token grant method unknown: '#{id}' (#{id.class})"}
|
117
121
|
@create_handlers[id]
|
118
122
|
end
|
119
123
|
|
120
124
|
# list of identifiers found in creation parameters that can be used to uniquely identify the token
|
121
125
|
def id_creator(id)
|
122
|
-
|
126
|
+
assert(@id_handlers.key?(id)){"id creator type unknown: #{id}/#{id.class}"}
|
123
127
|
@id_handlers[id]
|
124
128
|
end
|
125
129
|
end # self
|
126
130
|
|
127
131
|
# JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
|
128
|
-
register_decoder lambda { |token| parts = token.split('.');
|
132
|
+
register_decoder lambda { |token| parts = token.split('.'); assert(parts.length.eql?(3)){'not aoc token'}; JSON.parse(Base64.decode64(parts[1]))} # rubocop:disable Style/Semicolon, Layout/LineLength
|
129
133
|
|
130
134
|
# generic token creation, parameters are provided in :generic
|
131
135
|
register_token_creator :generic, lambda { |oauth|
|
@@ -152,7 +156,7 @@ module Aspera
|
|
152
156
|
OpenApplication.instance.uri(login_page_url)
|
153
157
|
# wait for code in request
|
154
158
|
received_params = web_server.received_request
|
155
|
-
|
159
|
+
assert(random_state.eql?(received_params['state'])){'wrong received state'}
|
156
160
|
# exchange code for token
|
157
161
|
return oauth.create_token(oauth.optional_scope_client_id(add_secret: true).merge(
|
158
162
|
grant_type: 'authorization_code',
|
@@ -169,7 +173,7 @@ module Aspera
|
|
169
173
|
require 'jwt'
|
170
174
|
seconds_since_epoch = Time.new.to_i
|
171
175
|
Log.log.info{"seconds=#{seconds_since_epoch}"}
|
172
|
-
|
176
|
+
assert(oauth.specific_parameters[:payload].is_a?(Hash)){'missing JWT payload'}
|
173
177
|
jwt_payload = {
|
174
178
|
exp: seconds_since_epoch + @@globals[:jwt_expiry_offset_sec], # expiration time
|
175
179
|
nbf: seconds_since_epoch - @@globals[:jwt_accepted_offset_sec], # not before
|
@@ -191,14 +195,14 @@ module Aspera
|
|
191
195
|
private
|
192
196
|
|
193
197
|
# [M]=mandatory [D]=has default value [0]=accept nil
|
194
|
-
# :base_url [M]
|
198
|
+
# :base_url [M] URL of authentication API
|
195
199
|
# :auth
|
196
|
-
# :grant_method [M]
|
200
|
+
# :grant_method [M] :generic, :web, :jwt, [custom types]
|
197
201
|
# :client_id [0]
|
198
202
|
# :client_secret [0]
|
199
203
|
# :scope [0]
|
200
|
-
# :path_token [D]
|
201
|
-
# :token_field [D]
|
204
|
+
# :path_token [D] API end point to create a token
|
205
|
+
# :token_field [D] field in result that contains the token
|
202
206
|
# :jwt:private_key_obj [M] for type :jwt
|
203
207
|
# :jwt:payload [M] for type :jwt
|
204
208
|
# :jwt:headers [0] for type :jwt
|
@@ -207,18 +211,16 @@ module Aspera
|
|
207
211
|
# :generic [M] for type :generic
|
208
212
|
def initialize(a_params)
|
209
213
|
Log.log.debug{"auth=#{a_params}"}
|
210
|
-
#
|
214
|
+
# set default values if not set in parameters common to all types
|
211
215
|
@generic_parameters = DEFAULT_CREATE_PARAMS.deep_merge(a_params)
|
212
|
-
# legacy
|
213
|
-
@generic_parameters[:grant_method] ||= @generic_parameters.delete(:crtype) if @generic_parameters.key?(:crtype) # cspell: disable-line
|
214
216
|
# check that type is known
|
215
217
|
self.class.token_creator(@generic_parameters[:grant_method])
|
216
218
|
# specific parameters for the creation type
|
217
219
|
@specific_parameters = @generic_parameters[@generic_parameters[:grant_method]]
|
218
220
|
if @generic_parameters[:grant_method].eql?(:web) && @specific_parameters.key?(:redirect_uri)
|
219
221
|
uri = URI.parse(@specific_parameters[:redirect_uri])
|
220
|
-
|
221
|
-
|
222
|
+
assert(%w[http https].include?(uri.scheme)){'redirect_uri scheme must be http or https'}
|
223
|
+
assert(!uri.port.nil?){'redirect_uri must have a port'}
|
222
224
|
# TODO: we could check that host is localhost or local address
|
223
225
|
end
|
224
226
|
rest_params = {
|
@@ -226,8 +228,9 @@ module Aspera
|
|
226
228
|
redirect_max: 2
|
227
229
|
}
|
228
230
|
rest_params[:auth] = a_params[:auth] if a_params.key?(:auth)
|
231
|
+
# this is the OAuth API
|
229
232
|
@api = Rest.new(rest_params)
|
230
|
-
# if needed use from api
|
233
|
+
# if those are needed use from @api
|
231
234
|
@generic_parameters.delete(:base_url)
|
232
235
|
@generic_parameters.delete(:auth)
|
233
236
|
@generic_parameters.delete(@generic_parameters[:grant_method])
|
@@ -266,7 +269,7 @@ module Aspera
|
|
266
269
|
@generic_parameters[:grant_method],
|
267
270
|
self.class.id_creator(@generic_parameters[:grant_method]).call(self), # array, so we flatten later
|
268
271
|
@generic_parameters[:scope],
|
269
|
-
@api.params.dig(
|
272
|
+
@api.params.dig(*%i[auth username])
|
270
273
|
].flatten)
|
271
274
|
|
272
275
|
# get token_data from cache (or nil), token_data is what is returned by /token
|
@@ -322,7 +325,7 @@ module Aspera
|
|
322
325
|
token_data = JSON.parse(json_data)
|
323
326
|
self.class.persist_mgr.put(token_id, json_data)
|
324
327
|
end # if ! in_cache
|
325
|
-
|
328
|
+
assert(token_data.key?(@generic_parameters[:token_field])){"API error: No such field in answer: #{@generic_parameters[:token_field]}"}
|
326
329
|
# ok we shall have a token here
|
327
330
|
return self.class.bearer_build(token_data[@generic_parameters[:token_field]])
|
328
331
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'json'
|
4
4
|
require 'aspera/log'
|
5
|
+
require 'aspera/assert'
|
5
6
|
|
6
7
|
module Aspera
|
7
8
|
# Persist data on file system
|
@@ -13,21 +14,19 @@ module Aspera
|
|
13
14
|
# @param :parse Optional parse method (default to JSON)
|
14
15
|
# @param :format Optional dump method (default to JSON)
|
15
16
|
# @param :merge Optional merge data from file to current data
|
16
|
-
def initialize(
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
@
|
24
|
-
@persisted_object = options[:data]
|
25
|
-
@object_id = options[:id]
|
17
|
+
def initialize(manager:, data:, id:, delete: nil, parse: nil, format: nil, merge: nil)
|
18
|
+
assert(!manager.nil?)
|
19
|
+
assert(!data.nil?)
|
20
|
+
assert_type(id, String)
|
21
|
+
assert(!id.empty?)
|
22
|
+
@manager = manager
|
23
|
+
@persisted_object = data
|
24
|
+
@object_id = id
|
26
25
|
# by default , at save time, file is deleted if data is nil
|
27
|
-
@delete_condition =
|
28
|
-
@persist_format =
|
29
|
-
persist_parse =
|
30
|
-
persist_merge =
|
26
|
+
@delete_condition = delete || lambda{|d|d.empty?}
|
27
|
+
@persist_format = format || lambda {|h| JSON.generate(h)}
|
28
|
+
persist_parse = parse || lambda {|t| JSON.parse(t)}
|
29
|
+
persist_merge = merge || lambda {|current, file| current.concat(file).uniq rescue current}
|
31
30
|
value = @manager.get(@object_id)
|
32
31
|
persist_merge.call(@persisted_object, persist_parse.call(value)) unless value.nil?
|
33
32
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'fileutils'
|
4
4
|
require 'aspera/log'
|
5
|
+
require 'aspera/assert'
|
5
6
|
require 'aspera/environment'
|
6
7
|
|
7
8
|
# search: persistency_folder PersistencyFolder
|
@@ -34,7 +35,7 @@ module Aspera
|
|
34
35
|
end
|
35
36
|
|
36
37
|
def put(object_id, value)
|
37
|
-
|
38
|
+
assert_type(value, String)
|
38
39
|
persist_filepath = id_to_filepath(object_id)
|
39
40
|
Log.log.debug{"persistency saving: #{persist_filepath}"}
|
40
41
|
FileUtils.rm_f(persist_filepath)
|
@@ -67,7 +68,7 @@ module Aspera
|
|
67
68
|
|
68
69
|
# @param object_id String or Array
|
69
70
|
def id_to_filepath(object_id)
|
70
|
-
|
71
|
+
assert_type(object_id, String)
|
71
72
|
FileUtils.mkdir_p(@folder)
|
72
73
|
Environment.restrict_file_access(@folder)
|
73
74
|
return File.join(@folder, "#{object_id}#{FILE_SUFFIX}")
|