aspera-cli 4.18.1 → 4.20.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/CHANGELOG.md +33 -0
- data/CONTRIBUTING.md +17 -12
- data/README.md +396 -185
- data/bin/asession +26 -19
- data/examples/build_exec +74 -0
- data/examples/{rubyc → build_exec_rubyc} +18 -2
- data/examples/get_proto_file.rb +7 -0
- data/lib/aspera/agent/alpha.rb +8 -8
- data/lib/aspera/agent/base.rb +4 -18
- data/lib/aspera/agent/connect.rb +14 -13
- data/lib/aspera/agent/direct.rb +123 -120
- data/lib/aspera/agent/httpgw.rb +2 -3
- data/lib/aspera/agent/node.rb +10 -10
- data/lib/aspera/agent/trsdk.rb +17 -20
- data/lib/aspera/api/alee.rb +15 -0
- data/lib/aspera/api/aoc.rb +128 -99
- data/lib/aspera/api/ats.rb +1 -1
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/httpgw.rb +104 -64
- data/lib/aspera/api/node.rb +33 -12
- data/lib/aspera/ascmd.rb +56 -48
- data/lib/aspera/ascp/installation.rb +142 -70
- data/lib/aspera/ascp/management.rb +7 -3
- data/lib/aspera/ascp/products.rb +13 -7
- data/lib/aspera/assert.rb +10 -5
- data/lib/aspera/cli/formatter.rb +42 -26
- data/lib/aspera/cli/hints.rb +2 -1
- data/lib/aspera/cli/info.rb +12 -10
- data/lib/aspera/cli/main.rb +16 -13
- data/lib/aspera/cli/manager.rb +15 -10
- data/lib/aspera/cli/plugin.rb +17 -31
- data/lib/aspera/cli/plugin_factory.rb +10 -1
- data/lib/aspera/cli/plugins/alee.rb +3 -3
- data/lib/aspera/cli/plugins/aoc.rb +222 -194
- data/lib/aspera/cli/plugins/ats.rb +16 -14
- data/lib/aspera/cli/plugins/config.rb +66 -53
- data/lib/aspera/cli/plugins/console.rb +3 -3
- data/lib/aspera/cli/plugins/faspex.rb +11 -21
- data/lib/aspera/cli/plugins/faspex5.rb +44 -42
- data/lib/aspera/cli/plugins/faspio.rb +2 -2
- data/lib/aspera/cli/plugins/httpgw.rb +1 -1
- data/lib/aspera/cli/plugins/node.rb +155 -96
- data/lib/aspera/cli/plugins/orchestrator.rb +14 -13
- data/lib/aspera/cli/plugins/preview.rb +8 -9
- data/lib/aspera/cli/plugins/server.rb +6 -10
- data/lib/aspera/cli/plugins/shares.rb +13 -9
- data/lib/aspera/cli/sync_actions.rb +72 -31
- data/lib/aspera/cli/transfer_agent.rb +13 -14
- data/lib/aspera/cli/transfer_progress.rb +36 -18
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +3 -4
- data/lib/aspera/coverage.rb +13 -1
- data/lib/aspera/environment.rb +59 -10
- data/lib/aspera/faspex_gw.rb +3 -3
- data/lib/aspera/json_rpc.rb +1 -1
- data/lib/aspera/keychain/encrypted_hash.rb +2 -0
- data/lib/aspera/keychain/macos_security.rb +7 -12
- data/lib/aspera/log.rb +4 -4
- data/lib/aspera/node_simulator.rb +1 -1
- data/lib/aspera/oauth/base.rb +39 -45
- data/lib/aspera/oauth/factory.rb +11 -4
- data/lib/aspera/oauth/generic.rb +4 -8
- data/lib/aspera/oauth/jwt.rb +4 -4
- data/lib/aspera/oauth/url_json.rb +3 -2
- data/lib/aspera/oauth/web.rb +10 -6
- data/lib/aspera/persistency_action_once.rb +16 -8
- data/lib/aspera/preview/utils.rb +5 -16
- data/lib/aspera/rest.rb +100 -76
- data/lib/aspera/secret_hider.rb +3 -2
- data/lib/aspera/ssh.rb +1 -1
- data/lib/aspera/transfer/faux_file.rb +7 -5
- data/lib/aspera/transfer/parameters.rb +41 -35
- data/lib/aspera/transfer/spec.rb +16 -18
- data/lib/aspera/transfer/sync.rb +51 -50
- data/lib/aspera/transfer/uri.rb +1 -1
- data/lib/aspera/uri_reader.rb +1 -1
- data/lib/aspera/web_auth.rb +166 -18
- data/lib/aspera/web_server_simple.rb +27 -15
- data/lib/transfer_pb.rb +84 -0
- data/lib/transfer_services_pb.rb +82 -0
- data.tar.gz.sig +0 -0
- metadata +25 -6
- metadata.gz.sig +0 -0
data/lib/aspera/environment.rb
CHANGED
@@ -5,6 +5,7 @@ require 'aspera/log'
|
|
5
5
|
require 'aspera/assert'
|
6
6
|
require 'rbconfig'
|
7
7
|
require 'singleton'
|
8
|
+
require 'English'
|
8
9
|
|
9
10
|
# cspell:words MEBI mswin bccwin
|
10
11
|
|
@@ -31,7 +32,6 @@ module Aspera
|
|
31
32
|
BYTES_PER_MEBIBIT = MEBI / BITS_PER_BYTE
|
32
33
|
|
33
34
|
class << self
|
34
|
-
@terminal_supports_unicode = nil
|
35
35
|
def ruby_version
|
36
36
|
return RbConfig::CONFIG['RUBY_PROGRAM_VERSION']
|
37
37
|
end
|
@@ -66,10 +66,13 @@ module Aspera
|
|
66
66
|
raise "Unknown CPU: #{RbConfig::CONFIG['host_cpu']}"
|
67
67
|
end
|
68
68
|
|
69
|
+
# normalized architecture name
|
70
|
+
# see constants: OS_* and CPU_*
|
69
71
|
def architecture
|
70
72
|
return "#{os}-#{cpu}"
|
71
73
|
end
|
72
74
|
|
75
|
+
# executable file extension for current OS
|
73
76
|
def exe_extension
|
74
77
|
return '.exe' if os.eql?(OS_WINDOWS)
|
75
78
|
return ''
|
@@ -83,6 +86,7 @@ module Aspera
|
|
83
86
|
Log.log.debug{"Windows: set HOME to USERPROFILE: #{Dir.home}"}
|
84
87
|
end
|
85
88
|
|
89
|
+
# empty variable binding for secure eval
|
86
90
|
def empty_binding
|
87
91
|
return Kernel.binding
|
88
92
|
end
|
@@ -92,7 +96,46 @@ module Aspera
|
|
92
96
|
Kernel.send('lave'.reverse, code, empty_binding, file, line)
|
93
97
|
end
|
94
98
|
|
95
|
-
|
99
|
+
def log_spawn(env:, exec:, args:)
|
100
|
+
[
|
101
|
+
'execute:'.red,
|
102
|
+
env.map{|k, v| "#{k}=#{Shellwords.shellescape(v)}"},
|
103
|
+
Shellwords.shellescape(exec),
|
104
|
+
args.map{|a|Shellwords.shellescape(a)}
|
105
|
+
].flatten.join(' ')
|
106
|
+
end
|
107
|
+
|
108
|
+
# start process in background, or raise exception
|
109
|
+
# caller can call Process.wait on returned value
|
110
|
+
def secure_spawn(exec:, args: [], env: [])
|
111
|
+
Log.log.debug {log_spawn(env: env, exec: exec, args: args)}
|
112
|
+
# start ascp in separate process
|
113
|
+
ascp_pid = Process.spawn(env, [exec, exec], *args, close_others: true)
|
114
|
+
Log.log.debug{"pid: #{ascp_pid}"}
|
115
|
+
return ascp_pid
|
116
|
+
end
|
117
|
+
|
118
|
+
# @param exec [String] path to executable
|
119
|
+
# @param args [Array] arguments to executable
|
120
|
+
# @param opts [Hash] options to capture3
|
121
|
+
# @return stdout of executable or raise expcetion
|
122
|
+
def secure_capture(exec:, args: [], **opts)
|
123
|
+
Aspera.assert_type(exec, String)
|
124
|
+
Aspera.assert_type(args, Array)
|
125
|
+
Aspera.assert_type(opts, Hash)
|
126
|
+
Log.log.debug {log_spawn(env: {}, exec: exec, args: args)}
|
127
|
+
stdout, stderr, status = Open3.capture3(exec, *args, **opts)
|
128
|
+
Log.log.debug{"status=#{status}, stderr=#{stderr}"}
|
129
|
+
Log.log.trace1{"stdout=#{stdout}"}
|
130
|
+
raise "process failed: #{status.exitstatus} : #{stderr}" unless status.success?
|
131
|
+
return stdout
|
132
|
+
end
|
133
|
+
|
134
|
+
# Write content to a file, with restricted access
|
135
|
+
# @param path [String] the file path
|
136
|
+
# @param force [Boolean] if true, overwrite the file
|
137
|
+
# @param mode [Integer] the file mode (permissions)
|
138
|
+
# @block [Proc] return the content to write to the file
|
96
139
|
def write_file_restricted(path, force: false, mode: nil)
|
97
140
|
Aspera.assert(block_given?, exception_class: Aspera::InternalError)
|
98
141
|
if force || !File.exist?(path)
|
@@ -105,6 +148,7 @@ module Aspera
|
|
105
148
|
return path
|
106
149
|
end
|
107
150
|
|
151
|
+
# restrict access to a file or folder to user only
|
108
152
|
def restrict_file_access(path, mode: nil)
|
109
153
|
if mode.nil?
|
110
154
|
# or FileUtils ?
|
@@ -121,18 +165,12 @@ module Aspera
|
|
121
165
|
Log.log.warn(e.message)
|
122
166
|
end
|
123
167
|
|
168
|
+
# @return true if we are in a terminal
|
124
169
|
def terminal?
|
125
170
|
$stdout.tty?
|
126
171
|
end
|
127
172
|
|
128
|
-
# @return
|
129
|
-
# https://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html
|
130
|
-
# https://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html
|
131
|
-
def terminal_supports_unicode?
|
132
|
-
@terminal_supports_unicode = terminal? && %w(LC_ALL LC_CTYPE LANG).any?{|var|ENV[var]&.include?('UTF-8')} if @terminal_supports_unicode.nil?
|
133
|
-
return @terminal_supports_unicode
|
134
|
-
end
|
135
|
-
|
173
|
+
# @return :text or :graphical depending on the environment
|
136
174
|
def default_gui_mode
|
137
175
|
# assume not remotely connected on macos and windows
|
138
176
|
return :graphical if [Environment::OS_WINDOWS, Environment::OS_MACOS].include?(Environment.os)
|
@@ -141,6 +179,7 @@ module Aspera
|
|
141
179
|
return :text
|
142
180
|
end
|
143
181
|
|
182
|
+
# open a URI in a graphical browser
|
144
183
|
# command must be non blocking
|
145
184
|
def open_uri_graphical(uri)
|
146
185
|
case Environment.os
|
@@ -152,6 +191,7 @@ module Aspera
|
|
152
191
|
end
|
153
192
|
end
|
154
193
|
|
194
|
+
# open a file in an editor
|
155
195
|
def open_editor(file_path)
|
156
196
|
if ENV.key?('EDITOR')
|
157
197
|
system(ENV['EDITOR'], file_path.to_s)
|
@@ -166,6 +206,15 @@ module Aspera
|
|
166
206
|
|
167
207
|
def initialize
|
168
208
|
@url_method = self.class.default_gui_mode
|
209
|
+
@terminal_supports_unicode = nil
|
210
|
+
end
|
211
|
+
|
212
|
+
# @return true if we can display Unicode characters
|
213
|
+
# https://www.gnu.org/software/libc/manual/html_node/Locale-Categories.html
|
214
|
+
# https://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html
|
215
|
+
def terminal_supports_unicode?
|
216
|
+
@terminal_supports_unicode = self.class.terminal? && %w(LC_ALL LC_CTYPE LANG).any?{|var|ENV[var]&.include?('UTF-8')} if @terminal_supports_unicode.nil?
|
217
|
+
return @terminal_supports_unicode
|
169
218
|
end
|
170
219
|
|
171
220
|
# Allows a user to open a Url
|
data/lib/aspera/faspex_gw.rb
CHANGED
@@ -29,7 +29,7 @@ module Aspera
|
|
29
29
|
'recipients' => faspex_pkg_delivery['recipients'],
|
30
30
|
'workspace_id' => @app_context
|
31
31
|
}
|
32
|
-
created_package = @app_api.create_package_simple(package_data, true,
|
32
|
+
created_package = @app_api.create_package_simple(package_data, true, nil)
|
33
33
|
# but we place it in a Faspex package creation response
|
34
34
|
return {
|
35
35
|
'links' => { 'status' => 'unused' },
|
@@ -44,7 +44,7 @@ module Aspera
|
|
44
44
|
'note' => faspex_pkg_delivery['note'],
|
45
45
|
'recipients' => faspex_pkg_delivery['recipients'].map{|name|{'name'=>name}}
|
46
46
|
}
|
47
|
-
package = @app_api.create('packages', package_data)
|
47
|
+
package = @app_api.create('packages', package_data)
|
48
48
|
# TODO: option to send from remote source or httpgw
|
49
49
|
transfer_spec = @app_api.call(
|
50
50
|
operation: 'POST',
|
@@ -85,7 +85,7 @@ module Aspera
|
|
85
85
|
rescue => e
|
86
86
|
response.status = 500
|
87
87
|
response['Content-Type'] = 'application/json'
|
88
|
-
response.body = {error: e.message}.to_json
|
88
|
+
response.body = {error: e.message, stacktrace: e.backtrace}.to_json
|
89
89
|
Log.log.error(e.message)
|
90
90
|
Log.log.debug{e.backtrace.join("\n")}
|
91
91
|
end
|
data/lib/aspera/json_rpc.rb
CHANGED
@@ -34,7 +34,7 @@ module Aspera
|
|
34
34
|
method: "#{@namespace}#{method}",
|
35
35
|
params: args,
|
36
36
|
id: @request_id += 1
|
37
|
-
})
|
37
|
+
})
|
38
38
|
Aspera.assert_type(data, Hash){'response'}
|
39
39
|
Aspera.assert(data['jsonrpc'] == JSON_RPC_VERSION){'bad version in response'}
|
40
40
|
Aspera.assert(data.key?('id')){'missing id in response'}
|
@@ -16,10 +16,12 @@ module Aspera
|
|
16
16
|
FILE_TYPE = 'encrypted_hash_vault'
|
17
17
|
CONTENT_KEYS = %i[label username password url description].freeze
|
18
18
|
FILE_KEYS = %w[version type cipher data].sort.freeze
|
19
|
+
private_constant :LEGACY_CIPHER_NAME, :DEFAULT_CIPHER_NAME, :FILE_TYPE, :CONTENT_KEYS, :FILE_KEYS
|
19
20
|
def initialize(path, current_password)
|
20
21
|
Aspera.assert_type(path, String){'path to vault file'}
|
21
22
|
@path = path
|
22
23
|
@all_secrets = {}
|
24
|
+
@cipher_name = DEFAULT_CIPHER_NAME
|
23
25
|
vault_encrypted_data = nil
|
24
26
|
if File.exist?(@path)
|
25
27
|
vault_file = File.read(@path)
|
@@ -4,7 +4,7 @@
|
|
4
4
|
require 'aspera/cli/info'
|
5
5
|
require 'aspera/log'
|
6
6
|
require 'aspera/assert'
|
7
|
-
require '
|
7
|
+
require 'aspera/environment'
|
8
8
|
|
9
9
|
# enhance the gem to support other key chains
|
10
10
|
module Aspera
|
@@ -48,20 +48,15 @@ module Aspera
|
|
48
48
|
options[:path] = uri.path unless ['', '/'].include?(uri.path)
|
49
49
|
options[:port] = uri.port unless uri.port.eql?(443) && !url.include?(':443/')
|
50
50
|
end
|
51
|
-
|
51
|
+
command_args = [command]
|
52
52
|
options&.each do |k, v|
|
53
53
|
Aspera.assert(supported.key?(k)){"unknown option: #{k}"}
|
54
54
|
next if v.nil?
|
55
|
-
|
56
|
-
|
55
|
+
command_args.push("-#{supported[k]}")
|
56
|
+
command_args.push(v.shellescape) unless v.empty?
|
57
57
|
end
|
58
|
-
|
59
|
-
|
60
|
-
stdout, stderr, status = Open3.capture3(*command_line)
|
61
|
-
Log.log.debug{"status=#{status}, stderr=#{stderr}"}
|
62
|
-
Log.log.trace1{"stdout=#{stdout}"}
|
63
|
-
raise "#{SECURITY_UTILITY} failed: #{status.exitstatus} : #{stderr}" unless status.success?
|
64
|
-
return stdout
|
58
|
+
command_args.push(last_opt) unless last_opt.nil?
|
59
|
+
return Environment.secure_capture(exec: SECURITY_UTILITY, args: command_args)
|
65
60
|
end
|
66
61
|
|
67
62
|
def key_chains(output)
|
@@ -78,7 +73,7 @@ module Aspera
|
|
78
73
|
|
79
74
|
def list(options={})
|
80
75
|
Aspera.assert_values(options[:domain], DOMAINS, exception_class: ArgumentError){'domain'} unless options[:domain].nil?
|
81
|
-
key_chains(execute('list-
|
76
|
+
key_chains(execute('list-keychains', options, LIST_OPTIONS))
|
82
77
|
end
|
83
78
|
|
84
79
|
def by_name(name)
|
data/lib/aspera/log.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'aspera/assert'
|
3
4
|
require 'aspera/colors'
|
4
5
|
require 'aspera/secret_hider'
|
5
6
|
require 'logger'
|
6
7
|
require 'pp'
|
7
8
|
require 'json'
|
8
9
|
require 'singleton'
|
10
|
+
require 'stringio'
|
9
11
|
|
10
12
|
old_verbose = $VERBOSE
|
11
13
|
$VERBOSE = nil
|
@@ -72,8 +74,7 @@ module Aspera
|
|
72
74
|
JSON.pretty_generate(object) rescue PP.pp(object, +'')
|
73
75
|
when :ruby
|
74
76
|
PP.pp(object, +'')
|
75
|
-
else
|
76
|
-
raise 'wrong parameter, expect ruby or json'
|
77
|
+
else error_unexpected_value(@@format){'dump format'}
|
77
78
|
end
|
78
79
|
"#{name.to_s.green} (#{@@format})=\n#{result}"
|
79
80
|
end
|
@@ -126,8 +127,7 @@ module Aspera
|
|
126
127
|
Syslog::Logger.make_methods(severity.downcase)
|
127
128
|
end
|
128
129
|
@logger = Syslog::Logger.new(@program_name, Syslog::LOG_LOCAL2)
|
129
|
-
else
|
130
|
-
raise "unknown log type: #{new_log_type}, use one of: #{LOG_TYPES.join(', ')}"
|
130
|
+
else error_unexpected_value(new_log_type){"log type (#{LOG_TYPES.join(', ')})"}
|
131
131
|
end
|
132
132
|
@logger.level = current_severity_integer
|
133
133
|
@logger_type = new_log_type
|
@@ -39,7 +39,7 @@ module Aspera
|
|
39
39
|
set_json_response(response, {
|
40
40
|
application: 'node',
|
41
41
|
current_time: Time.now.utc.iso8601(0),
|
42
|
-
version: info['
|
42
|
+
version: info['sdk_ascp_version'].gsub(/ .*$/, ''),
|
43
43
|
license_expiration_date: info['expiration_date'],
|
44
44
|
license_max_rate: info['maximum_bandwidth'],
|
45
45
|
os: %x(uname -srv).chomp,
|
data/lib/aspera/oauth/base.rb
CHANGED
@@ -3,64 +3,66 @@
|
|
3
3
|
require 'aspera/oauth/factory'
|
4
4
|
require 'aspera/log'
|
5
5
|
require 'aspera/assert'
|
6
|
-
require 'aspera/id_generator'
|
7
6
|
require 'date'
|
8
7
|
|
9
8
|
module Aspera
|
10
9
|
module OAuth
|
11
|
-
#
|
12
|
-
# bearer
|
10
|
+
# OAuth 2 client for the REST client
|
11
|
+
# Generate bearer token
|
12
|
+
# Bearer tokens are cached in memory and in a file cache for later re-use
|
13
13
|
# https://tools.ietf.org/html/rfc6749
|
14
14
|
class Base
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
#
|
19
|
-
# @param
|
20
|
-
# @param
|
21
|
-
# @param
|
22
|
-
# @param
|
23
|
-
# @param scope [O]
|
24
|
-
# @param path_token [D] API end point to create a token
|
25
|
-
# @param token_field [D] field in result that contains the token
|
15
|
+
# @param ** Parameters for REST
|
16
|
+
# @param client_id [String, nil]
|
17
|
+
# @param client_secret [String, nil]
|
18
|
+
# @param scope [String, nil]
|
19
|
+
# @param use_query [bool] Provide parameters in query instead of body
|
20
|
+
# @param path_token [String] API end point to create a token
|
21
|
+
# @param token_field [String] Field in result that contains the token
|
22
|
+
# @param cache_ids [Array, nil] List of unique identifiers for cache id generation
|
26
23
|
def initialize(
|
27
|
-
base_url:,
|
28
|
-
auth: nil,
|
29
24
|
client_id: nil,
|
30
25
|
client_secret: nil,
|
31
26
|
scope: nil,
|
32
27
|
use_query: false,
|
33
|
-
path_token: 'token',
|
34
|
-
token_field: 'access_token'
|
28
|
+
path_token: 'token',
|
29
|
+
token_field: 'access_token',
|
30
|
+
cache_ids: nil,
|
31
|
+
**rest_params
|
35
32
|
)
|
36
|
-
Aspera.assert_type(base_url, String)
|
37
33
|
Aspera.assert(respond_to?(:create_token), 'create_token method must be defined', exception_class: InternalError)
|
38
|
-
|
34
|
+
# this is the OAuth API
|
35
|
+
@api = Rest.new(**rest_params)
|
39
36
|
@path_token = path_token
|
40
37
|
@token_field = token_field
|
41
38
|
@client_id = client_id
|
42
39
|
@client_secret = client_secret
|
43
|
-
@scope = scope
|
44
40
|
@use_query = use_query
|
45
|
-
@
|
46
|
-
@
|
47
|
-
|
48
|
-
@api
|
49
|
-
|
50
|
-
|
51
|
-
|
41
|
+
@base_cache_ids = cache_ids.clone
|
42
|
+
@base_cache_ids = [] if @base_cache_ids.nil?
|
43
|
+
Aspera.assert_type(@base_cache_ids, Array)
|
44
|
+
if @api.auth_params.key?(:username)
|
45
|
+
cache_ids.push(@api.auth_params[:username])
|
46
|
+
end
|
47
|
+
@base_cache_ids.freeze
|
48
|
+
self.scope = scope
|
49
|
+
end
|
50
|
+
|
51
|
+
# Scope can be modified after creation, then update identifier for cache
|
52
|
+
def scope=(scope)
|
53
|
+
@scope = scope
|
54
|
+
# generate token unique identifier for persistency (memory/disk cache)
|
55
|
+
@token_cache_id = Factory.cache_id(@api.base_url, self.class, @base_cache_ids, @scope)
|
52
56
|
end
|
53
57
|
|
54
58
|
# helper method to create token as per RFC
|
55
59
|
def create_token_call(creation_params)
|
56
60
|
Log.log.debug{'Generating a new token'.bg_green}
|
57
|
-
payload = {
|
58
|
-
body: creation_params,
|
59
|
-
body_type: :www
|
60
|
-
}
|
61
|
+
payload = { body_type: :www }
|
61
62
|
if @use_query
|
62
63
|
payload[:query] = creation_params
|
63
|
-
|
64
|
+
else
|
65
|
+
payload[:body] = creation_params
|
64
66
|
end
|
65
67
|
return @api.call(
|
66
68
|
operation: 'POST',
|
@@ -85,16 +87,8 @@ module Aspera
|
|
85
87
|
# @param cache set to false to disable cache
|
86
88
|
# @param refresh set to true to force refresh or re-generation (if previous failed)
|
87
89
|
def token(cache: true, refresh: false)
|
88
|
-
# generate token unique identifier for persistency (memory/disk cache)
|
89
|
-
token_id = IdGenerator.from_list(Factory.id(
|
90
|
-
@base_url,
|
91
|
-
Factory.class_to_id(self.class),
|
92
|
-
@identifiers,
|
93
|
-
@scope
|
94
|
-
))
|
95
|
-
|
96
90
|
# get token_data from cache (or nil), token_data is what is returned by /token
|
97
|
-
token_data = Factory.instance.persist_mgr.get(
|
91
|
+
token_data = Factory.instance.persist_mgr.get(@token_cache_id) if cache
|
98
92
|
token_data = JSON.parse(token_data) unless token_data.nil?
|
99
93
|
# Optional optimization: check if node token is expired based on decoded content then force refresh if close enough
|
100
94
|
# might help in case the transfer agent cannot refresh himself
|
@@ -120,7 +114,7 @@ module Aspera
|
|
120
114
|
refresh_token = token_data['refresh_token']
|
121
115
|
end
|
122
116
|
# delete cache
|
123
|
-
Factory.instance.persist_mgr.delete(
|
117
|
+
Factory.instance.persist_mgr.delete(@token_cache_id)
|
124
118
|
token_data = nil
|
125
119
|
# lets try the existing refresh token
|
126
120
|
if !refresh_token.nil?
|
@@ -132,7 +126,7 @@ module Aspera
|
|
132
126
|
# save only if success
|
133
127
|
json_data = resp[:http].body
|
134
128
|
token_data = JSON.parse(json_data)
|
135
|
-
Factory.instance.persist_mgr.put(
|
129
|
+
Factory.instance.persist_mgr.put(@token_cache_id, json_data)
|
136
130
|
else
|
137
131
|
Log.log.debug{"refresh failed: #{resp[:http].body}".bg_red}
|
138
132
|
end
|
@@ -144,7 +138,7 @@ module Aspera
|
|
144
138
|
resp = create_token
|
145
139
|
json_data = resp[:http].body
|
146
140
|
token_data = JSON.parse(json_data)
|
147
|
-
Factory.instance.persist_mgr.put(
|
141
|
+
Factory.instance.persist_mgr.put(@token_cache_id, json_data)
|
148
142
|
end
|
149
143
|
Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"}
|
150
144
|
# ok we shall have a token here
|
data/lib/aspera/oauth/factory.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'aspera/id_generator'
|
4
4
|
require 'aspera/assert'
|
5
|
+
require 'singleton'
|
5
6
|
require 'base64'
|
6
7
|
module Aspera
|
7
8
|
module OAuth
|
@@ -29,11 +30,17 @@ module Aspera
|
|
29
30
|
return token[BEARER_PREFIX.length..-1]
|
30
31
|
end
|
31
32
|
|
32
|
-
|
33
|
-
|
33
|
+
# @return a cache identifier
|
34
|
+
def cache_id(url, creator_class, *params)
|
35
|
+
return IdGenerator.from_list([
|
36
|
+
PERSIST_CATEGORY_TOKEN,
|
37
|
+
url,
|
38
|
+
Factory.class_to_id(creator_class),
|
39
|
+
*params
|
40
|
+
].flatten)
|
34
41
|
end
|
35
42
|
|
36
|
-
# snake version of class name
|
43
|
+
# @return snake version of class name
|
37
44
|
def class_to_id(creator_class)
|
38
45
|
return creator_class.name.split('::').last.capital_to_snake.to_sym
|
39
46
|
end
|
data/lib/aspera/oauth/generic.rb
CHANGED
@@ -13,17 +13,13 @@ module Aspera
|
|
13
13
|
receiver_client_ids: nil,
|
14
14
|
**base_params
|
15
15
|
)
|
16
|
-
super(**base_params)
|
16
|
+
super(**base_params, cache_ids: [grant_type&.split(':')&.last, apikey, response_type])
|
17
17
|
@create_params = {
|
18
18
|
grant_type: grant_type
|
19
19
|
}
|
20
|
-
@create_params[:response_type] = response_type
|
21
|
-
@create_params[:apikey] = apikey
|
22
|
-
@create_params[:receiver_client_ids] = receiver_client_ids
|
23
|
-
@identifiers.push(
|
24
|
-
@create_params[:grant_type]&.split(':')&.last,
|
25
|
-
@create_params[:apikey],
|
26
|
-
@create_params[:response_type])
|
20
|
+
@create_params[:response_type] = response_type unless response_type.nil?
|
21
|
+
@create_params[:apikey] = apikey unless apikey.nil?
|
22
|
+
@create_params[:receiver_client_ids] = receiver_client_ids unless receiver_client_ids.nil?
|
27
23
|
end
|
28
24
|
|
29
25
|
def create_token
|
data/lib/aspera/oauth/jwt.rb
CHANGED
@@ -13,6 +13,7 @@ module Aspera
|
|
13
13
|
# https://tools.ietf.org/html/rfc7523
|
14
14
|
# https://tools.ietf.org/html/rfc7519
|
15
15
|
class Jwt < Base
|
16
|
+
GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
|
16
17
|
# @param private_key_obj private key object
|
17
18
|
# @param payload payload to be included in the JWT
|
18
19
|
# @param headers headers to be included in the JWT
|
@@ -25,17 +26,16 @@ module Aspera
|
|
25
26
|
Aspera.assert_type(private_key_obj, OpenSSL::PKey::RSA){'private_key_obj'}
|
26
27
|
Aspera.assert_type(payload, Hash){'payload'}
|
27
28
|
Aspera.assert_type(headers, Hash){'headers'}
|
28
|
-
super(**base_params)
|
29
|
+
super(**base_params, cache_ids: [payload[:sub]])
|
29
30
|
@private_key_obj = private_key_obj
|
30
31
|
@additional_payload = payload
|
31
32
|
@headers = headers
|
32
|
-
@identifiers.push(@additional_payload[:sub])
|
33
33
|
end
|
34
34
|
|
35
35
|
def create_token
|
36
36
|
require 'jwt'
|
37
37
|
seconds_since_epoch = Time.new.to_i
|
38
|
-
Log.log.
|
38
|
+
Log.log.debug{"seconds_since_epoch=#{seconds_since_epoch}"}
|
39
39
|
jwt_payload = {
|
40
40
|
exp: seconds_since_epoch + OAuth::Factory.instance.parameters[:jwt_expiry_offset_sec], # expiration time
|
41
41
|
nbf: seconds_since_epoch - OAuth::Factory.instance.parameters[:jwt_accepted_offset_sec], # not before
|
@@ -46,7 +46,7 @@ module Aspera
|
|
46
46
|
Log.log.debug{"private=[#{@private_key_obj}]"}
|
47
47
|
assertion = JWT.encode(jwt_payload, @private_key_obj, 'RS256', @headers)
|
48
48
|
Log.log.debug{"assertion=[#{assertion}]"}
|
49
|
-
return create_token_call(optional_scope_client_id.merge(grant_type:
|
49
|
+
return create_token_call(optional_scope_client_id.merge(grant_type: GRANT_TYPE, assertion: assertion))
|
50
50
|
end
|
51
51
|
end
|
52
52
|
Factory.instance.register_token_creator(Jwt)
|
@@ -6,15 +6,16 @@ module Aspera
|
|
6
6
|
module OAuth
|
7
7
|
# This class is used to create a token using a JSON body and a URL
|
8
8
|
class UrlJson < Base
|
9
|
+
# @param url URL to send the JSON body
|
10
|
+
# @param json JSON body to send
|
9
11
|
def initialize(
|
10
12
|
url:,
|
11
13
|
json:,
|
12
14
|
**generic_params
|
13
15
|
)
|
14
|
-
super(**generic_params)
|
16
|
+
super(**generic_params, cache_ids: [json[:url_token]])
|
15
17
|
@body = json
|
16
18
|
@query = url
|
17
|
-
@identifiers.push(@body[:url_token])
|
18
19
|
end
|
19
20
|
|
20
21
|
def create_token
|
data/lib/aspera/oauth/web.rb
CHANGED
@@ -8,8 +8,11 @@ module Aspera
|
|
8
8
|
module OAuth
|
9
9
|
# Authentication using Web browser
|
10
10
|
class Web < Base
|
11
|
-
|
12
|
-
|
11
|
+
class << self
|
12
|
+
attr_accessor :additionnal_info
|
13
|
+
end
|
14
|
+
# @param redirect_uri url to receive the code after auth (to be exchanged for token)
|
15
|
+
# @param path_authorize path to login page on web app
|
13
16
|
def initialize(
|
14
17
|
redirect_uri:,
|
15
18
|
path_authorize: 'authorize',
|
@@ -21,18 +24,19 @@ module Aspera
|
|
21
24
|
uri = URI.parse(@redirect_uri)
|
22
25
|
Aspera.assert(%w[http https].include?(uri.scheme)){'redirect_uri scheme must be http or https'}
|
23
26
|
Aspera.assert(!uri.port.nil?){'redirect_uri must have a port'}
|
24
|
-
# TODO: we could check that host is localhost or local address
|
27
|
+
# TODO: we could check that host is localhost or local address, as we are going to listen locally
|
25
28
|
end
|
26
29
|
|
27
30
|
def create_token
|
28
|
-
|
31
|
+
# generate secure state to check later
|
32
|
+
random_state = SecureRandom.uuid
|
29
33
|
login_page_url = Rest.build_uri(
|
30
|
-
"#{@base_url}/#{@path_authorize}",
|
34
|
+
"#{@api.base_url}/#{@path_authorize}",
|
31
35
|
optional_scope_client_id.merge(response_type: 'code', redirect_uri: @redirect_uri, state: random_state))
|
32
36
|
# here, we need a human to authorize on a web page
|
33
37
|
Log.log.info{"login_page_url=#{login_page_url}".bg_red.gray}
|
34
38
|
# start a web server to receive request code
|
35
|
-
web_server = WebAuth.new(@redirect_uri)
|
39
|
+
web_server = WebAuth.new(@redirect_uri, self.class.additionnal_info)
|
36
40
|
# start browser on login page
|
37
41
|
Environment.instance.open_uri(login_page_url)
|
38
42
|
# wait for code in request
|
@@ -7,6 +7,13 @@ require 'aspera/assert'
|
|
7
7
|
module Aspera
|
8
8
|
# Persist data on file system
|
9
9
|
class PersistencyActionOnce
|
10
|
+
DELETE_DEFAULT = lambda{|d|d.empty?}
|
11
|
+
PARSE_DEFAULT = lambda {|t| JSON.parse(t)}
|
12
|
+
FORMAT_DEFAULT = lambda {|h| JSON.generate(h)}
|
13
|
+
MERGE_DEFAULT = lambda {|current, file| current.concat(file).uniq rescue current}
|
14
|
+
MANAGER_METHODS = %i[get put delete]
|
15
|
+
private_constant :DELETE_DEFAULT, :PARSE_DEFAULT, :FORMAT_DEFAULT, :MERGE_DEFAULT, :MANAGER_METHODS
|
16
|
+
|
10
17
|
# @param :manager Mandatory Database
|
11
18
|
# @param :data Mandatory object to persist, must be same object from begin to end (assume array by default)
|
12
19
|
# @param :id Mandatory identifiers
|
@@ -14,21 +21,22 @@ module Aspera
|
|
14
21
|
# @param :parse Optional parse method (default to JSON)
|
15
22
|
# @param :format Optional dump method (default to JSON)
|
16
23
|
# @param :merge Optional merge data from file to current data
|
17
|
-
def initialize(manager:, data:, id:, delete:
|
18
|
-
Aspera.assert(
|
24
|
+
def initialize(manager:, data:, id:, delete: DELETE_DEFAULT, parse: PARSE_DEFAULT, format: FORMAT_DEFAULT, merge: MERGE_DEFAULT)
|
25
|
+
Aspera.assert(MANAGER_METHODS.all?{|i|manager.respond_to?(i)}){"Manager must answer to #{MANAGER_METHODS}"}
|
19
26
|
Aspera.assert(!data.nil?)
|
20
27
|
Aspera.assert_type(id, String)
|
21
28
|
Aspera.assert(!id.empty?)
|
29
|
+
Aspera.assert_type(delete, Proc)
|
30
|
+
Aspera.assert_type(parse, Proc)
|
31
|
+
Aspera.assert_type(format, Proc)
|
32
|
+
Aspera.assert_type(merge, Proc)
|
22
33
|
@manager = manager
|
23
34
|
@persisted_object = data
|
24
35
|
@object_id = id
|
25
|
-
|
26
|
-
@
|
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}
|
36
|
+
@delete_condition = delete
|
37
|
+
@persist_format = format
|
30
38
|
value = @manager.get(@object_id)
|
31
|
-
|
39
|
+
merge.call(@persisted_object, parse.call(value)) unless value.nil?
|
32
40
|
end
|
33
41
|
|
34
42
|
def save
|