aspera-cli 4.25.0.pre → 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 +23 -17
- data/CONTRIBUTING.md +119 -47
- data/README.md +325 -239
- 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 +33 -24
- data/lib/aspera/api/ats.rb +1 -0
- data/lib/aspera/api/faspex.rb +11 -5
- data/lib/aspera/ascmd.rb +1 -1
- data/lib/aspera/ascp/installation.rb +7 -7
- data/lib/aspera/ascp/management.rb +9 -5
- data/lib/aspera/assert.rb +3 -3
- data/lib/aspera/cli/extended_value.rb +10 -2
- data/lib/aspera/cli/formatter.rb +15 -62
- data/lib/aspera/cli/manager.rb +9 -43
- data/lib/aspera/cli/plugins/aoc.rb +71 -66
- data/lib/aspera/cli/plugins/ats.rb +30 -36
- data/lib/aspera/cli/plugins/base.rb +11 -6
- data/lib/aspera/cli/plugins/config.rb +21 -16
- data/lib/aspera/cli/plugins/console.rb +2 -1
- data/lib/aspera/cli/plugins/faspex.rb +7 -4
- data/lib/aspera/cli/plugins/faspex5.rb +12 -9
- data/lib/aspera/cli/plugins/faspio.rb +5 -2
- data/lib/aspera/cli/plugins/httpgw.rb +2 -1
- data/lib/aspera/cli/plugins/node.rb +10 -6
- data/lib/aspera/cli/plugins/oauth.rb +12 -11
- data/lib/aspera/cli/plugins/orchestrator.rb +2 -1
- data/lib/aspera/cli/plugins/preview.rb +2 -2
- data/lib/aspera/cli/plugins/server.rb +3 -2
- data/lib/aspera/cli/plugins/shares.rb +59 -20
- data/lib/aspera/cli/transfer_agent.rb +1 -2
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +5 -5
- data/lib/aspera/coverage.rb +5 -1
- data/lib/aspera/dot_container.rb +108 -0
- data/lib/aspera/environment.rb +69 -89
- data/lib/aspera/faspex_postproc.rb +1 -1
- data/lib/aspera/id_generator.rb +7 -10
- data/lib/aspera/keychain/macos_security.rb +2 -2
- data/lib/aspera/log.rb +2 -1
- data/lib/aspera/oauth/base.rb +25 -38
- data/lib/aspera/oauth/factory.rb +5 -6
- data/lib/aspera/oauth/generic.rb +1 -1
- data/lib/aspera/oauth/jwt.rb +1 -1
- data/lib/aspera/oauth/url_json.rb +4 -3
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/preview/file_types.rb +1 -1
- data/lib/aspera/preview/terminal.rb +95 -29
- data/lib/aspera/preview/utils.rb +6 -5
- data/lib/aspera/rest.rb +5 -2
- data/lib/aspera/ssh.rb +6 -5
- data/lib/aspera/sync/conf.schema.yaml +2 -2
- data/lib/aspera/sync/operations.rb +3 -3
- data/lib/aspera/transfer/parameters.rb +6 -6
- data/lib/aspera/transfer/spec.schema.yaml +4 -4
- data/lib/aspera/transfer/spec_doc.rb +11 -21
- data/lib/aspera/uri_reader.rb +17 -3
- data.tar.gz.sig +0 -0
- metadata +17 -2
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aspera/assert'
|
|
4
|
+
|
|
5
|
+
module Aspera
|
|
6
|
+
# Convert dotted-path to/from nested Hash/Array container
|
|
7
|
+
class DotContainer
|
|
8
|
+
class << self
|
|
9
|
+
# Insert extended value `value` into struct `result` at `path`
|
|
10
|
+
# @param path [String] Dotted path in container
|
|
11
|
+
# @param value [String] Last value to insert
|
|
12
|
+
# @param result [NilClass, Hash, Array] current value
|
|
13
|
+
# @return [Hash, Array]
|
|
14
|
+
def dotted_to_container(path, value, result = nil)
|
|
15
|
+
# Typed keys
|
|
16
|
+
keys = path.split(OPTION_DOTTED_SEPARATOR).map{ |k| int_or_string(k)}
|
|
17
|
+
# Create, or re-use first level container
|
|
18
|
+
current = (result ||= new_hash_or_array_from_key(keys.first))
|
|
19
|
+
# walk the path, and create sub-containers if necessary
|
|
20
|
+
keys.each_cons(2) do |k, next_k|
|
|
21
|
+
array_requires_integer_index!(current, k)
|
|
22
|
+
current = (current[k] ||= new_hash_or_array_from_key(next_k))
|
|
23
|
+
end
|
|
24
|
+
# Assign value at last index
|
|
25
|
+
array_requires_integer_index!(current, keys.last)
|
|
26
|
+
current[keys.last] = value
|
|
27
|
+
result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Convert `String` to `Integer`, or keep `String` if not `Integer`
|
|
33
|
+
def int_or_string(value)
|
|
34
|
+
Integer(value, exception: false) || value
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Assert that if `container` is an `Array`, then `index` is an `Integer`
|
|
38
|
+
# @param container [Hash, Array]
|
|
39
|
+
# @param index [String, Integer]
|
|
40
|
+
def array_requires_integer_index!(container, index)
|
|
41
|
+
Aspera.assert(container.is_a?(Hash) || index.is_a?(Integer)){'Using String index when Integer index used previously'}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Create a new `Hash` or `Array` depending on type of `key`
|
|
45
|
+
def new_hash_or_array_from_key(key)
|
|
46
|
+
key.is_a?(Integer) ? [] : {}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param [Hash,Array] Container object
|
|
51
|
+
def initialize(container)
|
|
52
|
+
Aspera.assert_type(container, Hash)
|
|
53
|
+
# tail (pop,push) contains the next element to display
|
|
54
|
+
# elements are [path, value]
|
|
55
|
+
@stack = container.empty? ? [] : [[[], container]]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Convert nested Hash/Array container to dotted-path Hash
|
|
59
|
+
# @return [Hash] Dotted-path Hash
|
|
60
|
+
def to_dotted
|
|
61
|
+
result = {}
|
|
62
|
+
until @stack.empty?
|
|
63
|
+
path, current = @stack.pop
|
|
64
|
+
to_insert = nil
|
|
65
|
+
# empty things are left intact
|
|
66
|
+
if current.respond_to?(:empty?) && current.empty?
|
|
67
|
+
to_insert = current
|
|
68
|
+
else
|
|
69
|
+
case current
|
|
70
|
+
when Hash
|
|
71
|
+
add_elements(path, current)
|
|
72
|
+
when Array
|
|
73
|
+
# Array has no nested structures -> list of Strings
|
|
74
|
+
if current.none?{ |i| i.is_a?(Array) || i.is_a?(Hash)}
|
|
75
|
+
to_insert = current.map(&:to_s)
|
|
76
|
+
# Array of Hashes with only 'name' keys -> list of Strings
|
|
77
|
+
elsif current.all?{ |i| i.is_a?(Hash) && i.keys == ['name']}
|
|
78
|
+
to_insert = current.map{ |i| i['name']}
|
|
79
|
+
# Array of Hashes with only 'name' and 'value' keys -> Hash of key/values
|
|
80
|
+
elsif current.all?{ |i| i.is_a?(Hash) && i.keys.sort == %w[name value]}
|
|
81
|
+
add_elements(path, current.each_with_object({}){ |i, h| h[i['name']] = i['value']})
|
|
82
|
+
else
|
|
83
|
+
add_elements(path, current.each_with_index.map{ |v, i| [i, v]})
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
to_insert = current
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
result[path.map(&:to_s).join(OPTION_DOTTED_SEPARATOR)] = to_insert unless to_insert.nil?
|
|
90
|
+
end
|
|
91
|
+
result
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# Add elements of enumerator to the @stack, in reverse order
|
|
97
|
+
def add_elements(path, enum)
|
|
98
|
+
enum.reverse_each do |key, value|
|
|
99
|
+
@stack.push([path + [key], value])
|
|
100
|
+
end
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# "."
|
|
105
|
+
OPTION_DOTTED_SEPARATOR = '.'
|
|
106
|
+
private_constant :OPTION_DOTTED_SEPARATOR
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/aspera/environment.rb
CHANGED
|
@@ -42,105 +42,85 @@ module Aspera
|
|
|
42
42
|
|
|
43
43
|
RB_EXT = '.rb'
|
|
44
44
|
|
|
45
|
+
PROCESS_MODES = %i[execute background capture].freeze
|
|
46
|
+
|
|
45
47
|
class << self
|
|
46
48
|
def ruby_version
|
|
47
49
|
return RbConfig::CONFIG['RUBY_PROGRAM_VERSION']
|
|
48
50
|
end
|
|
49
51
|
|
|
50
|
-
#
|
|
52
|
+
# Empty variable binding for secure eval
|
|
51
53
|
def empty_binding
|
|
52
54
|
return Kernel.binding
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
#
|
|
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
|
|
56
61
|
def secure_eval(code, file, line)
|
|
57
62
|
Kernel.send('lave'.reverse, code, empty_binding, file, line)
|
|
58
63
|
end
|
|
59
64
|
|
|
60
|
-
#
|
|
61
|
-
# @param
|
|
62
|
-
# @param
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
args&.map{ |a| Shellwords.shellescape(a)}
|
|
71
|
-
].compact.flatten.join(' ')
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Start process in background
|
|
75
|
-
# caller can call Process.wait on returned value
|
|
76
|
-
# @param exec [String] Path to executable
|
|
77
|
-
# @param args [Array, nil] Arguments for executable
|
|
78
|
-
# @param env [Hash, nil] Environment variables
|
|
79
|
-
# @param kwargs [Hash] Options for `Process.spawn`
|
|
80
|
-
# @return [String] PID of process
|
|
81
|
-
# @raise [Exception] if problem
|
|
82
|
-
def secure_spawn(exec:, args: nil, env: nil, **kwargs)
|
|
83
|
-
Aspera.assert_type(exec, String)
|
|
84
|
-
Aspera.assert_type(args, Array, NilClass)
|
|
85
|
-
Aspera.assert_type(env, Hash, NilClass)
|
|
86
|
-
Log.log.debug{log_spawn(exec: exec, args: args, env: env)}
|
|
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
|
-
kwargs[:close_others] = true unless kwargs.key?(:close_others)
|
|
92
|
-
# Start separate process in background
|
|
93
|
-
pid = Process.spawn(*spawn_args, **kwargs)
|
|
94
|
-
Log.dump(:pid, pid)
|
|
95
|
-
return 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
|
-
# @param
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
144
124
|
end
|
|
145
125
|
|
|
146
126
|
# Write content to a file, with restricted access
|
|
@@ -273,9 +253,9 @@ module Aspera
|
|
|
273
253
|
# @param uri [String] the URI to open
|
|
274
254
|
def open_uri_graphical(uri)
|
|
275
255
|
case @os
|
|
276
|
-
when Environment::OS_MACOS then return self.class.secure_execute(
|
|
277
|
-
when Environment::OS_WINDOWS then return self.class.secure_execute(
|
|
278
|
-
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)
|
|
279
259
|
else Assert.error_unexpected_value(os){'no graphical open method'}
|
|
280
260
|
end
|
|
281
261
|
end
|
|
@@ -283,9 +263,9 @@ module Aspera
|
|
|
283
263
|
# open a file in an editor
|
|
284
264
|
def open_editor(file_path)
|
|
285
265
|
if ENV.key?('EDITOR')
|
|
286
|
-
self.class.secure_execute(
|
|
266
|
+
self.class.secure_execute(ENV['EDITOR'], file_path.to_s)
|
|
287
267
|
elsif @os.eql?(Environment::OS_WINDOWS)
|
|
288
|
-
self.class.secure_execute(
|
|
268
|
+
self.class.secure_execute('notepad.exe', %Q{"#{file_path}"})
|
|
289
269
|
else
|
|
290
270
|
open_uri_graphical(file_path.to_s)
|
|
291
271
|
end
|
|
@@ -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
|
|
@@ -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
|
data/lib/aspera/oauth/base.rb
CHANGED
|
@@ -14,50 +14,38 @@ 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
|
|
63
51
|
# @return [HTTPResponse]
|
|
@@ -68,17 +56,16 @@ module Aspera
|
|
|
68
56
|
return @api.create(@path_token, creation_params, content_type: Rest::MIME_WWW, ret: :resp)
|
|
69
57
|
end
|
|
70
58
|
|
|
59
|
+
# Create base parameters for token creation calls
|
|
71
60
|
# @param add_secret [Boolean] Add secret in default call parameters
|
|
72
61
|
# @return [Hash] Optional general parameters
|
|
73
|
-
def
|
|
74
|
-
call_params =
|
|
75
|
-
call_params
|
|
76
|
-
call_params[:client_id] = @client_id unless @client_id.nil?
|
|
77
|
-
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
|
|
78
65
|
return call_params
|
|
79
66
|
end
|
|
80
67
|
|
|
81
|
-
# @return value suitable for Authorization header
|
|
68
|
+
# @return [String] value suitable for Authorization header
|
|
82
69
|
def authorization(**kwargs)
|
|
83
70
|
return OAuth::Factory.bearer_authorization(token(**kwargs))
|
|
84
71
|
end
|
|
@@ -114,7 +101,7 @@ module Aspera
|
|
|
114
101
|
if !refresh_token.nil?
|
|
115
102
|
Log.log.info{"refresh token=[#{refresh_token}]".bg_green}
|
|
116
103
|
begin
|
|
117
|
-
http = create_token_call(
|
|
104
|
+
http = create_token_call(base_params(add_secret: true).merge(grant_type: 'refresh_token', refresh_token: refresh_token))
|
|
118
105
|
# Save only if success
|
|
119
106
|
json_data = http.body
|
|
120
107
|
token_data = JSON.parse(json_data)
|
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
|
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,7 +23,7 @@ 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
29
|
headers: {'Accept' => Rest::MIME_JSON},
|
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
|
|
@@ -53,7 +53,7 @@ module Aspera
|
|
|
53
53
|
|
|
54
54
|
private_constant :SUPPORTED_MIME_TYPES
|
|
55
55
|
|
|
56
|
-
# @attr use_mimemagic [
|
|
56
|
+
# @attr use_mimemagic [Boolean] `true` to use mimemagic to determine real mime type based on file content
|
|
57
57
|
attr_accessor :use_mimemagic
|
|
58
58
|
|
|
59
59
|
def initialize
|