aspera-cli 4.15.0 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/BUGS.md +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}")
|