aspera-cli 4.24.2 → 4.25.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 +1070 -758
- data/CONTRIBUTING.md +130 -115
- data/README.md +961 -623
- data/lib/aspera/agent/direct.rb +14 -12
- data/lib/aspera/agent/factory.rb +9 -6
- data/lib/aspera/agent/transferd.rb +8 -8
- data/lib/aspera/api/aoc.rb +104 -67
- data/lib/aspera/api/ats.rb +1 -0
- data/lib/aspera/api/cos_node.rb +3 -2
- data/lib/aspera/api/faspex.rb +17 -10
- data/lib/aspera/api/node.rb +10 -12
- data/lib/aspera/ascmd.rb +2 -3
- data/lib/aspera/ascp/installation.rb +60 -46
- data/lib/aspera/ascp/management.rb +9 -5
- data/lib/aspera/assert.rb +28 -6
- data/lib/aspera/cli/error.rb +4 -2
- data/lib/aspera/cli/extended_value.rb +94 -62
- data/lib/aspera/cli/formatter.rb +44 -58
- data/lib/aspera/cli/main.rb +21 -14
- data/lib/aspera/cli/manager.rb +317 -250
- data/lib/aspera/cli/plugins/alee.rb +3 -3
- data/lib/aspera/cli/plugins/aoc.rb +139 -78
- data/lib/aspera/cli/plugins/ats.rb +30 -36
- data/lib/aspera/cli/plugins/base.rb +68 -55
- data/lib/aspera/cli/plugins/config.rb +90 -100
- data/lib/aspera/cli/plugins/console.rb +15 -9
- data/lib/aspera/cli/plugins/cos.rb +1 -1
- data/lib/aspera/cli/plugins/faspex.rb +39 -30
- data/lib/aspera/cli/plugins/faspex5.rb +57 -52
- data/lib/aspera/cli/plugins/faspio.rb +10 -7
- data/lib/aspera/cli/plugins/httpgw.rb +3 -2
- data/lib/aspera/cli/plugins/node.rb +140 -125
- data/lib/aspera/cli/plugins/oauth.rb +13 -12
- data/lib/aspera/cli/plugins/orchestrator.rb +116 -33
- data/lib/aspera/cli/plugins/preview.rb +28 -48
- data/lib/aspera/cli/plugins/server.rb +9 -10
- data/lib/aspera/cli/plugins/shares.rb +77 -43
- data/lib/aspera/cli/sync_actions.rb +49 -38
- data/lib/aspera/cli/transfer_agent.rb +16 -35
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/wizard.rb +8 -5
- data/lib/aspera/command_line_builder.rb +24 -21
- data/lib/aspera/coverage.rb +6 -2
- data/lib/aspera/dot_container.rb +108 -0
- data/lib/aspera/environment.rb +71 -84
- data/lib/aspera/faspex_gw.rb +1 -1
- data/lib/aspera/faspex_postproc.rb +1 -1
- data/lib/aspera/id_generator.rb +7 -10
- data/lib/aspera/keychain/factory.rb +1 -2
- data/lib/aspera/keychain/macos_security.rb +2 -2
- data/lib/aspera/log.rb +2 -1
- data/lib/aspera/markdown.rb +31 -0
- data/lib/aspera/nagios.rb +6 -5
- data/lib/aspera/oauth/base.rb +41 -64
- data/lib/aspera/oauth/factory.rb +6 -7
- data/lib/aspera/oauth/generic.rb +1 -1
- data/lib/aspera/oauth/jwt.rb +1 -1
- data/lib/aspera/oauth/url_json.rb +6 -4
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/preview/file_types.rb +24 -38
- data/lib/aspera/preview/terminal.rb +95 -29
- data/lib/aspera/preview/utils.rb +6 -5
- data/lib/aspera/products/connect.rb +3 -3
- data/lib/aspera/rest.rb +54 -39
- data/lib/aspera/rest_error_analyzer.rb +4 -4
- data/lib/aspera/ssh.rb +10 -6
- data/lib/aspera/ssl.rb +41 -0
- data/lib/aspera/sync/conf.schema.yaml +184 -36
- data/lib/aspera/sync/database.rb +2 -1
- data/lib/aspera/sync/operations.rb +128 -72
- data/lib/aspera/transfer/parameters.rb +9 -10
- data/lib/aspera/transfer/spec.rb +2 -3
- data/lib/aspera/transfer/spec.schema.yaml +52 -22
- data/lib/aspera/transfer/spec_doc.rb +20 -30
- data/lib/aspera/uri_reader.rb +18 -4
- data/lib/transferd_pb.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +34 -6
- metadata.gz.sig +0 -0
data/lib/aspera/environment.rb
CHANGED
|
@@ -5,7 +5,9 @@ require 'aspera/log'
|
|
|
5
5
|
require 'aspera/assert'
|
|
6
6
|
require 'rbconfig'
|
|
7
7
|
require 'singleton'
|
|
8
|
+
require 'open3'
|
|
8
9
|
require 'English'
|
|
10
|
+
require 'shellwords'
|
|
9
11
|
|
|
10
12
|
# cspell:words MEBI mswin bccwin
|
|
11
13
|
|
|
@@ -40,100 +42,85 @@ module Aspera
|
|
|
40
42
|
|
|
41
43
|
RB_EXT = '.rb'
|
|
42
44
|
|
|
45
|
+
PROCESS_MODES = %i[execute background capture].freeze
|
|
46
|
+
|
|
43
47
|
class << self
|
|
44
48
|
def ruby_version
|
|
45
49
|
return RbConfig::CONFIG['RUBY_PROGRAM_VERSION']
|
|
46
50
|
end
|
|
47
51
|
|
|
48
|
-
#
|
|
52
|
+
# Empty variable binding for secure eval
|
|
49
53
|
def empty_binding
|
|
50
54
|
return Kernel.binding
|
|
51
55
|
end
|
|
52
56
|
|
|
53
|
-
#
|
|
57
|
+
# Secure execution of Ruby code
|
|
58
|
+
# @param code [String] Ruby code to execute
|
|
59
|
+
# @param file [String] File name for error reporting
|
|
60
|
+
# @param line [Integer] Line number for error reporting
|
|
54
61
|
def secure_eval(code, file, line)
|
|
55
62
|
Kernel.send('lave'.reverse, code, empty_binding, file, line)
|
|
56
63
|
end
|
|
57
64
|
|
|
58
|
-
#
|
|
59
|
-
# @param
|
|
60
|
-
# @param
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
args&.map{ |a| Shellwords.shellescape(a)}
|
|
69
|
-
].compact.flatten.join(' ')
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Start process in background
|
|
73
|
-
# caller can call Process.wait on returned value
|
|
74
|
-
# @param exec [String] path to executable
|
|
75
|
-
# @param args [Array, nil] arguments for executable
|
|
76
|
-
# @param env [Hash, nil] environment variables
|
|
77
|
-
# @param options [Hash, nil] spawn options
|
|
78
|
-
# @return [String] PID of process
|
|
79
|
-
# @raise [Exception] if problem
|
|
80
|
-
def secure_spawn(exec:, args: nil, env: nil, **options)
|
|
81
|
-
Aspera.assert_type(exec, String)
|
|
82
|
-
Aspera.assert_type(args, Array, NilClass)
|
|
83
|
-
Aspera.assert_type(env, Hash, NilClass)
|
|
84
|
-
Aspera.assert_type(options, Hash, NilClass)
|
|
85
|
-
Log.log.debug{log_spawn(exec: exec, args: args, env: env)}
|
|
86
|
-
# start ascp in separate process
|
|
87
|
-
spawn_args = []
|
|
88
|
-
spawn_args.push(env) unless env.nil?
|
|
89
|
-
spawn_args.push([exec, exec])
|
|
90
|
-
spawn_args.concat(args) unless args.nil?
|
|
91
|
-
opts = {close_others: true}
|
|
92
|
-
opts.merge!(options) unless options.nil?
|
|
93
|
-
ascp_pid = Process.spawn(*spawn_args, **opts)
|
|
94
|
-
Log.log.debug{"pid: #{ascp_pid}"}
|
|
95
|
-
return ascp_pid
|
|
65
|
+
# Build argv for Process.spawn / Kernel.system (no shell)
|
|
66
|
+
# @param cmd [Array] Command and arguments
|
|
67
|
+
# @param kwargs [Hash] Additional arguments to `secure_execute`
|
|
68
|
+
def build_spawn_argv(cmd, kwargs)
|
|
69
|
+
env = kwargs.delete(:env)
|
|
70
|
+
argv = []
|
|
71
|
+
argv << env if env
|
|
72
|
+
argv << [cmd.first, cmd.first] # no shell, preserve argv[0]
|
|
73
|
+
argv.concat(cmd.drop(1))
|
|
74
|
+
argv
|
|
96
75
|
end
|
|
97
76
|
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
77
|
+
# Execute a process securely (no shell)
|
|
78
|
+
# mode:
|
|
79
|
+
# :execute -> Kernel.system, return nil
|
|
80
|
+
# :background -> Process.spawn, return pid
|
|
81
|
+
# :capture -> Open3.capture3, return stdout
|
|
82
|
+
# @param cmd [Array] Command and arguments
|
|
83
|
+
# @param env [Hash, nil] Environment variables
|
|
84
|
+
# @param mode [Symbol] Execution mode (see above)
|
|
85
|
+
# @param kwargs [Hash] Additional arguments to underlying method, includes:
|
|
86
|
+
# :exception [Boolean] for :capture mode, raise error if process fails
|
|
87
|
+
# :close_others [Boolean] for :background mode
|
|
88
|
+
# :env [Hash] for :execute mode
|
|
89
|
+
def secure_execute(*cmd, mode: :execute, **kwargs)
|
|
90
|
+
cmd = cmd.map(&:to_s)
|
|
91
|
+
Aspera.assert(cmd.size.positive?, type: ArgumentError){'executable must be present'}
|
|
92
|
+
Aspera.assert_values(mode, PROCESS_MODES, type: ArgumentError){'mode'}
|
|
93
|
+
Log.log.debug do
|
|
94
|
+
parts = [mode.to_s, 'command:']
|
|
95
|
+
kwargs[:env]&.each{ |k, v| parts << "#{k}=#{Shellwords.shellescape(v.to_s)}"}
|
|
96
|
+
cmd.each{ |a| parts << Shellwords.shellescape(a)}
|
|
97
|
+
parts.join(' ')
|
|
98
|
+
end
|
|
99
|
+
case mode
|
|
100
|
+
when :execute
|
|
101
|
+
# https://docs.ruby-lang.org/en/master/Kernel.html#method-i-system
|
|
102
|
+
# https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options
|
|
103
|
+
kwargs[:exception] = true unless kwargs.key?(:exception)
|
|
104
|
+
Kernel.system(*build_spawn_argv(cmd, kwargs), **kwargs)
|
|
105
|
+
when :background
|
|
106
|
+
# https://docs.ruby-lang.org/en/master/Process.html#method-c-spawn
|
|
107
|
+
# https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options
|
|
108
|
+
kwargs[:close_others] = true unless kwargs.key?(:close_others)
|
|
109
|
+
pid = Process.spawn(*build_spawn_argv(cmd, kwargs), **kwargs)
|
|
110
|
+
Log.dump(:pid, pid)
|
|
111
|
+
pid
|
|
112
|
+
when :capture
|
|
113
|
+
# https://docs.ruby-lang.org/en/master/Open3.html#method-c-capture3
|
|
114
|
+
# https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Execution+Options
|
|
115
|
+
argv = [kwargs.delete(:env)].compact + cmd
|
|
116
|
+
exception = kwargs.delete(:exception){true}
|
|
117
|
+
stdout, stderr, status = Open3.capture3(*argv, **kwargs)
|
|
118
|
+
Log.log.debug{"status=#{status}, stderr=#{stderr}"}
|
|
119
|
+
Log.log.trace1{"stdout=#{stdout}"}
|
|
120
|
+
raise "Process failed: #{status.exitstatus} (#{stderr})" if exception && !status.success?
|
|
121
|
+
stdout
|
|
122
|
+
else Aspera.error_unreachable_line
|
|
123
|
+
end
|
|
137
124
|
end
|
|
138
125
|
|
|
139
126
|
# Write content to a file, with restricted access
|
|
@@ -266,9 +253,9 @@ module Aspera
|
|
|
266
253
|
# @param uri [String] the URI to open
|
|
267
254
|
def open_uri_graphical(uri)
|
|
268
255
|
case @os
|
|
269
|
-
when Environment::OS_MACOS then return self.class.secure_execute(
|
|
270
|
-
when Environment::OS_WINDOWS then return self.class.secure_execute(
|
|
271
|
-
when Environment::OS_LINUX then return self.class.secure_execute(
|
|
256
|
+
when Environment::OS_MACOS then return self.class.secure_execute('open', uri.to_s)
|
|
257
|
+
when Environment::OS_WINDOWS then return self.class.secure_execute('start', 'explorer', %Q{"#{uri}"})
|
|
258
|
+
when Environment::OS_LINUX then return self.class.secure_execute('xdg-open', uri.to_s)
|
|
272
259
|
else Assert.error_unexpected_value(os){'no graphical open method'}
|
|
273
260
|
end
|
|
274
261
|
end
|
|
@@ -276,9 +263,9 @@ module Aspera
|
|
|
276
263
|
# open a file in an editor
|
|
277
264
|
def open_editor(file_path)
|
|
278
265
|
if ENV.key?('EDITOR')
|
|
279
|
-
self.class.secure_execute(
|
|
266
|
+
self.class.secure_execute(ENV['EDITOR'], file_path.to_s)
|
|
280
267
|
elsif @os.eql?(Environment::OS_WINDOWS)
|
|
281
|
-
self.class.secure_execute(
|
|
268
|
+
self.class.secure_execute('notepad.exe', %Q{"#{file_path}"})
|
|
282
269
|
else
|
|
283
270
|
open_uri_graphical(file_path.to_s)
|
|
284
271
|
end
|
data/lib/aspera/faspex_gw.rb
CHANGED
|
@@ -55,7 +55,7 @@ module Aspera
|
|
|
55
55
|
content_type: Rest::MIME_JSON,
|
|
56
56
|
body: {paths: [{'destination'=>'/'}]},
|
|
57
57
|
headers: {'Accept' => Rest::MIME_JSON}
|
|
58
|
-
)
|
|
58
|
+
)
|
|
59
59
|
transfer_spec.delete('authentication')
|
|
60
60
|
# but we place it in a Faspex package creation response
|
|
61
61
|
return {
|
|
@@ -50,7 +50,7 @@ module Aspera
|
|
|
50
50
|
Log.dump(:webhook_parameters, webhook_parameters)
|
|
51
51
|
# env expects only strings
|
|
52
52
|
environment = webhook_parameters.each_with_object({}){ |(k, v), h| h[k] = v.to_s}
|
|
53
|
-
post_proc_pid = Environment.
|
|
53
|
+
post_proc_pid = Environment.secure_execute(script_path, mode: :background, env: environment)
|
|
54
54
|
Timeout.timeout(@parameters[:timeout_seconds]) do
|
|
55
55
|
# "wait" for process to avoid zombie
|
|
56
56
|
Process.wait(post_proc_pid)
|
data/lib/aspera/id_generator.rb
CHANGED
|
@@ -9,19 +9,16 @@ module Aspera
|
|
|
9
9
|
class << self
|
|
10
10
|
# Generate an ID from a list of object IDs
|
|
11
11
|
# The generated ID is safe as file name
|
|
12
|
-
# @param
|
|
12
|
+
# @param ids [Array] the object IDs (can be nested, will be flattened, and nils removed)
|
|
13
13
|
# @return [String] the generated ID
|
|
14
|
-
def from_list(
|
|
14
|
+
def from_list(*ids)
|
|
15
15
|
safe_char = Environment.instance.safe_filename_character
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
end.join(safe_char)
|
|
21
|
-
end
|
|
22
|
-
Aspera.assert_type(object_id, String)
|
|
16
|
+
# compact: remove nils
|
|
17
|
+
id = ids.flatten.compact.map do |i|
|
|
18
|
+
i.is_a?(String) && i.start_with?('https://') ? URI.parse(i).host : i.to_s
|
|
19
|
+
end.join(safe_char)
|
|
23
20
|
# keep dot for extension only (nicer)
|
|
24
|
-
return Environment.instance.sanitized_filename(
|
|
21
|
+
return Environment.instance.sanitized_filename(id.gsub('.', safe_char)).downcase
|
|
25
22
|
end
|
|
26
23
|
end
|
|
27
24
|
end
|
|
@@ -12,8 +12,7 @@ module Aspera
|
|
|
12
12
|
# @param folder [String] folder to store the vault (if needed)
|
|
13
13
|
# @param password [String] password to open the vault
|
|
14
14
|
def create(info, name, folder, password)
|
|
15
|
-
Aspera.
|
|
16
|
-
Aspera.assert(info.values.all?(String)){'vault info shall have only string values'}
|
|
15
|
+
Aspera.assert_hash_all(info, Symbol, String){'vault info shall have only string values'}
|
|
17
16
|
info = info.symbolize_keys
|
|
18
17
|
vault_type = info.delete(:type)
|
|
19
18
|
Aspera.assert_values(vault_type, LIST.map(&:to_s)){'vault.type'}
|
|
@@ -49,7 +49,7 @@ module Aspera
|
|
|
49
49
|
options[:path] = uri.path unless ['', '/'].include?(uri.path)
|
|
50
50
|
options[:port] = uri.port unless uri.port.eql?(443) && !url.include?(':443/')
|
|
51
51
|
end
|
|
52
|
-
command_args = [command]
|
|
52
|
+
command_args = [SECURITY_UTILITY, command]
|
|
53
53
|
options&.each do |k, v|
|
|
54
54
|
Aspera.assert(supported.key?(k)){"unknown option: #{k}"}
|
|
55
55
|
next if v.nil?
|
|
@@ -57,7 +57,7 @@ module Aspera
|
|
|
57
57
|
command_args.push(v.shellescape) unless v.empty?
|
|
58
58
|
end
|
|
59
59
|
command_args.push(last_opt) unless last_opt.nil?
|
|
60
|
-
return Environment.
|
|
60
|
+
return Environment.secure_execute(*command_args, mode: :capture)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def key_chains(output)
|
data/lib/aspera/log.rb
CHANGED
|
@@ -81,8 +81,9 @@ module Aspera
|
|
|
81
81
|
# @param object [Hash, nil] Data to dump
|
|
82
82
|
# @param level [Symbol] Debug level
|
|
83
83
|
# @param block [Proc, nil] Give computed object
|
|
84
|
-
def dump(name, object = nil, level: :debug)
|
|
84
|
+
def dump(name, object = nil, level: :debug, &block)
|
|
85
85
|
return unless instance.logger.send(:"#{level}?")
|
|
86
|
+
Aspera.assert(object.nil? || block.nil?){'Use either object, or block, not both'}
|
|
86
87
|
object = yield if block_given?
|
|
87
88
|
instance.logger.send(level, obj_dump(name, object))
|
|
88
89
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aspera
|
|
4
|
+
# Formatting for Markdown
|
|
5
|
+
class Markdown
|
|
6
|
+
# Matches: **bold**, `code`, or an HTML entity (&, ©, 💩)
|
|
7
|
+
FORMATS = /(?:\*\*(?<bold>[^*]+?)\*\*)|(?:`(?<code>[^`]+)`)|&(?<entity>(?:[A-Za-z][A-Za-z0-9]{1,31}|#\d{1,7}|#x[0-9A-Fa-f]{1,6}));/m
|
|
8
|
+
HTML_BREAK = '<br/>'
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# Generate markdown from the provided 2D table
|
|
12
|
+
def table(table)
|
|
13
|
+
# get max width of each columns
|
|
14
|
+
col_widths = table.transpose.map do |col|
|
|
15
|
+
[col.flat_map{ |c| c.to_s.delete('`').split(HTML_BREAK).map(&:size)}.max, 80].min
|
|
16
|
+
end
|
|
17
|
+
headings = table.shift
|
|
18
|
+
table.unshift(col_widths.map{ |col_width| '-' * col_width})
|
|
19
|
+
table.unshift(headings)
|
|
20
|
+
lines = table.map{ |line| "| #{line.map{ |i| i.to_s.gsub('\\', '\\\\').gsub('|', '\|')}.join(' | ')} |\n"}
|
|
21
|
+
lines[1] = lines[1].tr(' ', '-')
|
|
22
|
+
return lines.join.chomp
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Generate markdown list from the provided list
|
|
26
|
+
def list(items)
|
|
27
|
+
items.map{ |i| "- #{i}"}.join("\n")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/aspera/nagios.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Aspera
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
class << self
|
|
24
|
-
#
|
|
24
|
+
# Process results of a analysis and display status and exit with code
|
|
25
25
|
def process(data)
|
|
26
26
|
Aspera.assert_type(data, Array)
|
|
27
27
|
Aspera.assert(!data.empty?){'data is empty'}
|
|
@@ -54,7 +54,7 @@ module Aspera
|
|
|
54
54
|
@data = []
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
#
|
|
57
|
+
# Compare remote time with local time
|
|
58
58
|
def check_time_offset(remote_date, component)
|
|
59
59
|
# check date if specified : 2015-10-13T07:32:01Z
|
|
60
60
|
remote_time = Time.parse(remote_date)
|
|
@@ -76,10 +76,11 @@ module Aspera
|
|
|
76
76
|
# TODO: check on database if latest version
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
#
|
|
80
|
-
|
|
79
|
+
# Readable status list
|
|
80
|
+
# @return [Array] of Hash
|
|
81
|
+
def status_list
|
|
81
82
|
Aspera.assert(!@data.empty?){'missing result'}
|
|
82
|
-
|
|
83
|
+
@data.map{ |i| {'status' => LEVELS[i[:code]].to_s, 'component' => i[:comp], 'message' => i[:msg]}}
|
|
83
84
|
end
|
|
84
85
|
end
|
|
85
86
|
end
|
data/lib/aspera/oauth/base.rb
CHANGED
|
@@ -14,83 +14,58 @@ module Aspera
|
|
|
14
14
|
# Bearer Token Usage: https://tools.ietf.org/html/rfc6750
|
|
15
15
|
class Base
|
|
16
16
|
Aspera.require_method!(:create_token)
|
|
17
|
-
# @param
|
|
18
|
-
# @param
|
|
19
|
-
# @param
|
|
20
|
-
# @param
|
|
21
|
-
# @param
|
|
22
|
-
# @param
|
|
23
|
-
# @param token_field [String] Field in result that contains the token
|
|
24
|
-
# @param cache_ids [Array, nil] List of unique identifiers for cache id generation
|
|
17
|
+
# @param params [Hash] Parameters for token creation (client_id, client_secret, scope, etc...)
|
|
18
|
+
# @param use_query [Boolean] Provide parameters in query instead of body
|
|
19
|
+
# @param path_token [String] API end point to create a token from base URL
|
|
20
|
+
# @param token_field [String] Field in result that contains the token
|
|
21
|
+
# @param cache_ids [Array] List of unique identifiers for cache id generation
|
|
22
|
+
# @param **rest_params [Hash] Parameters for REST
|
|
25
23
|
def initialize(
|
|
26
|
-
|
|
27
|
-
client_secret: nil,
|
|
28
|
-
scope: nil,
|
|
24
|
+
params: {},
|
|
29
25
|
use_query: false,
|
|
30
26
|
path_token: 'token',
|
|
31
27
|
token_field: Factory::TOKEN_FIELD,
|
|
32
|
-
cache_ids:
|
|
28
|
+
cache_ids: [],
|
|
33
29
|
**rest_params
|
|
34
30
|
)
|
|
31
|
+
Aspera.assert_type(params, Hash)
|
|
32
|
+
Aspera.assert_type(cache_ids, Array)
|
|
35
33
|
# This is the OAuth API
|
|
36
34
|
@api = Rest.new(**rest_params)
|
|
37
|
-
@
|
|
38
|
-
@token_cache_id = nil
|
|
35
|
+
@params = params.dup.freeze
|
|
39
36
|
@path_token = path_token
|
|
40
37
|
@token_field = token_field
|
|
41
|
-
@client_id = client_id
|
|
42
|
-
@client_secret = client_secret
|
|
43
38
|
@use_query = use_query
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# TODO: this shall be done in class, using cache_ids
|
|
47
|
-
@base_cache_ids.push(@api.auth_params[:username]) if @api.auth_params.key?(:username)
|
|
48
|
-
@base_cache_ids.compact!
|
|
49
|
-
@base_cache_ids.freeze
|
|
50
|
-
self.scope = scope
|
|
39
|
+
# TODO: :username and :scope shall be done in class, using cache_ids
|
|
40
|
+
@token_cache_id = Factory.cache_id(@api.base_url, self.class, cache_ids, rest_params[:username], @params[:scope])
|
|
51
41
|
end
|
|
52
42
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
attr_reader :scope, :api, :path_token, :client_id
|
|
43
|
+
# The OAuth API Object
|
|
44
|
+
attr_reader :api
|
|
45
|
+
# Sub path to generate token
|
|
46
|
+
attr_reader :path_token
|
|
47
|
+
# Parameters to generate token
|
|
48
|
+
attr_reader :params
|
|
61
49
|
|
|
62
|
-
#
|
|
50
|
+
# Helper method to create token as per RFC
|
|
51
|
+
# @return [HTTPResponse]
|
|
52
|
+
# @raise RestError if not 2XX code
|
|
63
53
|
def create_token_call(creation_params)
|
|
64
54
|
Log.log.debug{'Generating a new token'.bg_green}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
query: creation_params
|
|
68
|
-
}
|
|
69
|
-
else
|
|
70
|
-
{
|
|
71
|
-
content_type: Rest::MIME_WWW,
|
|
72
|
-
body: creation_params
|
|
73
|
-
}
|
|
74
|
-
end
|
|
75
|
-
return @api.call(
|
|
76
|
-
operation: 'POST',
|
|
77
|
-
subpath: @path_token,
|
|
78
|
-
headers: {'Accept' => Rest::MIME_JSON},
|
|
79
|
-
**payload
|
|
80
|
-
)
|
|
55
|
+
return @api.create(@path_token, nil, query: creation_params, ret: :resp) if @use_query
|
|
56
|
+
return @api.create(@path_token, creation_params, content_type: Rest::MIME_WWW, ret: :resp)
|
|
81
57
|
end
|
|
82
58
|
|
|
59
|
+
# Create base parameters for token creation calls
|
|
83
60
|
# @param add_secret [Boolean] Add secret in default call parameters
|
|
84
61
|
# @return [Hash] Optional general parameters
|
|
85
|
-
def
|
|
86
|
-
call_params =
|
|
87
|
-
call_params
|
|
88
|
-
call_params[:client_id] = @client_id unless @client_id.nil?
|
|
89
|
-
call_params[:client_secret] = @client_secret unless !add_secret || @client_id.nil? || @client_secret.nil?
|
|
62
|
+
def base_params(add_secret: false)
|
|
63
|
+
call_params = @params.dup
|
|
64
|
+
call_params.delete(:client_secret) unless add_secret
|
|
90
65
|
return call_params
|
|
91
66
|
end
|
|
92
67
|
|
|
93
|
-
# @return value suitable for Authorization header
|
|
68
|
+
# @return [String] value suitable for Authorization header
|
|
94
69
|
def authorization(**kwargs)
|
|
95
70
|
return OAuth::Factory.bearer_authorization(token(**kwargs))
|
|
96
71
|
end
|
|
@@ -122,17 +97,18 @@ module Aspera
|
|
|
122
97
|
Factory.instance.persist_mgr.delete(@token_cache_id)
|
|
123
98
|
token_data = nil
|
|
124
99
|
# lets try the existing refresh token
|
|
100
|
+
# NOTE: AoC admin token has no refresh, and lives by default 1800secs
|
|
125
101
|
if !refresh_token.nil?
|
|
126
|
-
Log.log.info{"refresh=[#{refresh_token}]".bg_green}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
json_data = resp[:http].body
|
|
102
|
+
Log.log.info{"refresh token=[#{refresh_token}]".bg_green}
|
|
103
|
+
begin
|
|
104
|
+
http = create_token_call(base_params(add_secret: true).merge(grant_type: 'refresh_token', refresh_token: refresh_token))
|
|
105
|
+
# Save only if success
|
|
106
|
+
json_data = http.body
|
|
132
107
|
token_data = JSON.parse(json_data)
|
|
133
108
|
Factory.instance.persist_mgr.put(@token_cache_id, json_data)
|
|
134
|
-
|
|
135
|
-
|
|
109
|
+
rescue => e
|
|
110
|
+
# Refresh token can fail.
|
|
111
|
+
Log.log.warn{"Refresh failed: #{e}"}
|
|
136
112
|
end
|
|
137
113
|
end
|
|
138
114
|
end
|
|
@@ -140,8 +116,9 @@ module Aspera
|
|
|
140
116
|
|
|
141
117
|
# no cache, nor refresh: generate a token
|
|
142
118
|
if token_data.nil?
|
|
143
|
-
|
|
144
|
-
|
|
119
|
+
# Call the method-specific token creation
|
|
120
|
+
# which returns the result of create_token_call
|
|
121
|
+
json_data = create_token.body
|
|
145
122
|
token_data = JSON.parse(json_data)
|
|
146
123
|
Factory.instance.persist_mgr.put(@token_cache_id, json_data)
|
|
147
124
|
end
|
data/lib/aspera/oauth/factory.rb
CHANGED
|
@@ -36,14 +36,13 @@ module Aspera
|
|
|
36
36
|
return authorization[SPACE_BEARER_AUTH_SCHEME.length..-1]
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# Generate a unique cache id for a token creator
|
|
40
|
+
# @param url [String] Base URL of the OAuth server
|
|
41
|
+
# @param creator_class [Class] Class of the token creator
|
|
42
|
+
# @param params [Array] List of parameters (can be nested) to uniquely identify the token
|
|
39
43
|
# @return a unique cache identifier
|
|
40
44
|
def cache_id(url, creator_class, *params)
|
|
41
|
-
return IdGenerator.from_list(
|
|
42
|
-
PERSIST_CATEGORY_TOKEN,
|
|
43
|
-
url,
|
|
44
|
-
Factory.class_to_id(creator_class)
|
|
45
|
-
] +
|
|
46
|
-
params)
|
|
45
|
+
return IdGenerator.from_list(PERSIST_CATEGORY_TOKEN, url, Factory.class_to_id(creator_class), params)
|
|
47
46
|
end
|
|
48
47
|
|
|
49
48
|
# @return snake version of class name
|
|
@@ -156,7 +155,7 @@ module Aspera
|
|
|
156
155
|
def register_token_creator(creator_class)
|
|
157
156
|
Aspera.assert_type(creator_class, Class)
|
|
158
157
|
id = Factory.class_to_id(creator_class)
|
|
159
|
-
Log.log.debug{"registering
|
|
158
|
+
Log.log.debug{"registering creator for #{id}"}
|
|
160
159
|
@token_type_classes[id] = creator_class
|
|
161
160
|
end
|
|
162
161
|
|
data/lib/aspera/oauth/generic.rb
CHANGED
data/lib/aspera/oauth/jwt.rb
CHANGED
|
@@ -61,7 +61,7 @@ module Aspera
|
|
|
61
61
|
Log.log.debug{"private=[#{@private_key_obj}]"}
|
|
62
62
|
assertion = JWT.encode(jwt_payload, @private_key_obj, 'RS256', @headers)
|
|
63
63
|
Log.log.debug{"assertion=[#{assertion}]"}
|
|
64
|
-
return create_token_call(
|
|
64
|
+
return create_token_call(base_params.merge(grant_type: GRANT_TYPE, assertion: assertion))
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
Factory.instance.register_token_creator(Jwt)
|
|
@@ -6,8 +6,9 @@ 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
|
|
10
|
-
# @param json
|
|
9
|
+
# @param url [Hash] Query parameters to send
|
|
10
|
+
# @param json [Hash] Body parameters to send as JSON
|
|
11
|
+
# @param generic_params [Hash] Generic parameters for OAuth::Base
|
|
11
12
|
def initialize(
|
|
12
13
|
url:,
|
|
13
14
|
json:,
|
|
@@ -22,10 +23,11 @@ module Aspera
|
|
|
22
23
|
api.call(
|
|
23
24
|
operation: 'POST',
|
|
24
25
|
subpath: path_token,
|
|
25
|
-
query: @query.merge(scope: scope), # scope is here because it may change over time (node)
|
|
26
|
+
query: @query.merge(scope: params[:scope]), # scope is here because it may change over time (node)
|
|
26
27
|
content_type: Rest::MIME_JSON,
|
|
27
28
|
body: @body,
|
|
28
|
-
headers: {'Accept' => Rest::MIME_JSON}
|
|
29
|
+
headers: {'Accept' => Rest::MIME_JSON},
|
|
30
|
+
ret: :resp
|
|
29
31
|
)
|
|
30
32
|
end
|
|
31
33
|
end
|
data/lib/aspera/oauth/web.rb
CHANGED
|
@@ -32,7 +32,7 @@ module Aspera
|
|
|
32
32
|
random_state = SecureRandom.uuid
|
|
33
33
|
login_page_url = Rest.build_uri(
|
|
34
34
|
"#{api.base_url}/#{@path_authorize}",
|
|
35
|
-
|
|
35
|
+
base_params.merge(response_type: 'code', redirect_uri: @redirect_uri, state: random_state)
|
|
36
36
|
)
|
|
37
37
|
# here, we need a human to authorize on a web page
|
|
38
38
|
Log.log.info{"login_page_url=#{login_page_url}".bg_red.gray}
|
|
@@ -44,7 +44,7 @@ module Aspera
|
|
|
44
44
|
received_params = web_server.received_request
|
|
45
45
|
Aspera.assert(random_state.eql?(received_params['state'])){'wrong received state'}
|
|
46
46
|
# exchange code for token
|
|
47
|
-
return create_token_call(
|
|
47
|
+
return create_token_call(base_params(add_secret: true).merge(
|
|
48
48
|
grant_type: 'authorization_code',
|
|
49
49
|
code: received_params['code'],
|
|
50
50
|
redirect_uri: @redirect_uri
|