aspera-cli 4.17.0 → 4.18.1
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 +3 -4
- data/CHANGELOG.md +33 -0
- data/CONTRIBUTING.md +15 -1
- data/README.md +711 -432
- data/bin/ascli +5 -0
- data/bin/asession +2 -2
- data/examples/build_package.sh +28 -0
- data/lib/aspera/agent/alpha.rb +10 -8
- data/lib/aspera/agent/base.rb +9 -6
- data/lib/aspera/agent/connect.rb +7 -8
- data/lib/aspera/agent/direct.rb +56 -37
- data/lib/aspera/agent/httpgw.rb +23 -324
- data/lib/aspera/agent/node.rb +19 -20
- data/lib/aspera/agent/trsdk.rb +19 -20
- data/lib/aspera/api/aoc.rb +17 -14
- data/lib/aspera/api/cos_node.rb +4 -4
- data/lib/aspera/api/httpgw.rb +342 -0
- data/lib/aspera/api/node.rb +135 -89
- data/lib/aspera/ascmd.rb +4 -3
- data/lib/aspera/ascp/installation.rb +15 -7
- data/lib/aspera/ascp/management.rb +2 -2
- data/lib/aspera/ascp/products.rb +1 -1
- data/lib/aspera/cli/basic_auth_plugin.rb +5 -9
- data/lib/aspera/cli/extended_value.rb +35 -16
- data/lib/aspera/cli/formatter.rb +161 -70
- data/lib/aspera/cli/hints.rb +18 -0
- data/lib/aspera/cli/main.rb +32 -39
- data/lib/aspera/cli/manager.rb +151 -119
- data/lib/aspera/cli/plugin.rb +27 -21
- data/lib/aspera/cli/plugin_factory.rb +31 -20
- data/lib/aspera/cli/plugins/alee.rb +14 -2
- data/lib/aspera/cli/plugins/aoc.rb +152 -141
- data/lib/aspera/cli/plugins/ats.rb +1 -1
- data/lib/aspera/cli/plugins/config.rb +72 -65
- data/lib/aspera/cli/plugins/console.rb +8 -5
- data/lib/aspera/cli/plugins/faspex.rb +32 -23
- data/lib/aspera/cli/plugins/faspex5.rb +232 -156
- data/lib/aspera/cli/plugins/faspio.rb +85 -0
- data/lib/aspera/cli/plugins/httpgw.rb +55 -0
- data/lib/aspera/cli/plugins/node.rb +129 -64
- data/lib/aspera/cli/plugins/orchestrator.rb +33 -30
- data/lib/aspera/cli/plugins/preview.rb +7 -3
- data/lib/aspera/cli/plugins/server.rb +6 -6
- data/lib/aspera/cli/plugins/shares.rb +16 -14
- data/lib/aspera/cli/special_values.rb +13 -0
- data/lib/aspera/cli/sync_actions.rb +10 -10
- data/lib/aspera/cli/transfer_agent.rb +7 -6
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/environment.rb +70 -9
- data/lib/aspera/faspex_gw.rb +5 -4
- data/lib/aspera/faspex_postproc.rb +2 -2
- data/lib/aspera/log.rb +6 -3
- data/lib/aspera/node_simulator.rb +2 -2
- data/lib/aspera/oauth/base.rb +31 -19
- data/lib/aspera/oauth/factory.rb +12 -13
- data/lib/aspera/oauth/generic.rb +1 -0
- data/lib/aspera/oauth/jwt.rb +18 -15
- data/lib/aspera/oauth/url_json.rb +8 -6
- data/lib/aspera/oauth/web.rb +2 -2
- data/lib/aspera/persistency_folder.rb +2 -2
- data/lib/aspera/preview/generator.rb +3 -3
- data/lib/aspera/preview/options.rb +3 -3
- data/lib/aspera/preview/terminal.rb +4 -4
- data/lib/aspera/preview/utils.rb +3 -3
- data/lib/aspera/proxy_auto_config.rb +5 -1
- data/lib/aspera/rest.rb +105 -88
- data/lib/aspera/rest_call_error.rb +1 -1
- data/lib/aspera/rest_error_analyzer.rb +2 -2
- data/lib/aspera/rest_errors_aspera.rb +1 -1
- data/lib/aspera/resumer.rb +1 -1
- data/lib/aspera/secret_hider.rb +2 -4
- data/lib/aspera/ssh.rb +1 -1
- data/lib/aspera/transfer/parameters.rb +39 -36
- data/lib/aspera/transfer/spec.rb +2 -0
- data/lib/aspera/transfer/sync.rb +2 -1
- data/lib/aspera/transfer/uri.rb +1 -1
- data/lib/aspera/uri_reader.rb +5 -4
- data/lib/aspera/web_auth.rb +1 -1
- data/lib/aspera/web_server_simple.rb +4 -3
- data.tar.gz.sig +0 -0
- metadata +7 -4
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/plugins/bss.rb +0 -71
- data/lib/aspera/open_application.rb +0 -71
|
@@ -7,7 +7,7 @@ module Aspera
|
|
|
7
7
|
module Plugins
|
|
8
8
|
# Plugin for Aspera Shares v1
|
|
9
9
|
class Shares < Cli::BasicAuthPlugin
|
|
10
|
-
|
|
10
|
+
NODE_API_PREFIX = 'node_api'
|
|
11
11
|
class << self
|
|
12
12
|
def detect(address_or_url)
|
|
13
13
|
address_or_url = "https://#{address_or_url}" unless address_or_url.match?(%r{^[a-z]{1,6}://})
|
|
@@ -16,7 +16,7 @@ module Aspera
|
|
|
16
16
|
begin
|
|
17
17
|
# shall fail: shares requires auth, but we check error message
|
|
18
18
|
# TODO: use ping instead ?
|
|
19
|
-
api.read("#{
|
|
19
|
+
api.read("#{NODE_API_PREFIX}/app")
|
|
20
20
|
rescue RestCallError => e
|
|
21
21
|
if e.response.code.to_s.eql?('401') && e.response.body.eql?('{"error":{"user_message":"API user authentication failed"}}')
|
|
22
22
|
found = true
|
|
@@ -64,22 +64,24 @@ module Aspera
|
|
|
64
64
|
when :health
|
|
65
65
|
nagios = Nagios.new
|
|
66
66
|
begin
|
|
67
|
-
Rest
|
|
68
|
-
.new(base_url: "#{options.get_option(:url, mandatory: true)}/#{
|
|
67
|
+
res = Rest
|
|
68
|
+
.new(base_url: "#{options.get_option(:url, mandatory: true)}/#{NODE_API_PREFIX}")
|
|
69
69
|
.call(
|
|
70
70
|
operation: 'GET',
|
|
71
71
|
subpath: 'ping',
|
|
72
|
-
headers: {'content-type': 'application/json'}
|
|
73
|
-
|
|
72
|
+
headers: {'content-type': 'application/json'})
|
|
73
|
+
raise 'Shares not detected' unless res[:http].body.eql?(' ')
|
|
74
74
|
nagios.add_ok('shares api', 'accessible')
|
|
75
75
|
rescue StandardError => e
|
|
76
|
-
nagios.add_critical('
|
|
76
|
+
nagios.add_critical('API', e.to_s)
|
|
77
77
|
end
|
|
78
78
|
return nagios.result
|
|
79
79
|
when :repository, :files
|
|
80
|
-
api_shares_node = basic_auth_api(
|
|
80
|
+
api_shares_node = basic_auth_api(NODE_API_PREFIX)
|
|
81
81
|
repo_command = options.get_next_command(Node::COMMANDS_SHARES)
|
|
82
|
-
return Node
|
|
82
|
+
return Node
|
|
83
|
+
.new(**init_params, api: api_shares_node)
|
|
84
|
+
.execute_action(repo_command)
|
|
83
85
|
when :admin
|
|
84
86
|
api_shares_admin = basic_auth_api('api/v1')
|
|
85
87
|
admin_command = options.get_next_command(%i[node share transfer_settings user group].freeze)
|
|
@@ -150,8 +152,8 @@ module Aspera
|
|
|
150
152
|
end
|
|
151
153
|
end
|
|
152
154
|
end
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -29,9 +29,9 @@ module Aspera
|
|
|
29
29
|
SIMPLE_ARGUMENTS_SYNC.each do |arg, check|
|
|
30
30
|
value = options.get_next_argument(
|
|
31
31
|
arg,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
mandatory: false,
|
|
33
|
+
validation: check.is_a?(Class) ? check : nil,
|
|
34
|
+
accept_list: check.is_a?(Class) ? nil : check)
|
|
35
35
|
break if value.nil?
|
|
36
36
|
simple_session_args[arg] = value.to_s
|
|
37
37
|
end
|
|
@@ -57,12 +57,12 @@ module Aspera
|
|
|
57
57
|
command2 = options.get_next_command([:status])
|
|
58
58
|
case command2
|
|
59
59
|
when :status
|
|
60
|
-
sync_session_name = options.get_next_argument('name of sync session', mandatory: false,
|
|
60
|
+
sync_session_name = options.get_next_argument('name of sync session', mandatory: false, validation: String)
|
|
61
61
|
async_params = options.get_option(:sync_info, mandatory: true)
|
|
62
62
|
return {type: :single_object, data: Transfer::Sync.admin_status(async_params, sync_session_name)}
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -26,6 +26,7 @@ module Aspera
|
|
|
26
26
|
|
|
27
27
|
<%=ts.to_yaml%>
|
|
28
28
|
END_OF_TEMPLATE
|
|
29
|
+
CP4I_REMOTE_HOST_LB = 'N/A'
|
|
29
30
|
# % (formatting bug in eclipse)
|
|
30
31
|
private_constant :FILE_LIST_FROM_ARGS,
|
|
31
32
|
:FILE_LIST_FROM_TRANSFER_SPEC,
|
|
@@ -101,8 +102,6 @@ module Aspera
|
|
|
101
102
|
def agent_instance
|
|
102
103
|
return @agent unless @agent.nil?
|
|
103
104
|
agent_type = @opt_mgr.get_option(:transfer, mandatory: true)
|
|
104
|
-
# agent plugin is loaded on demand to avoid loading unnecessary dependencies
|
|
105
|
-
require "aspera/agent/#{agent_type}"
|
|
106
105
|
# set keys as symbols
|
|
107
106
|
agent_options = @opt_mgr.get_option(:transfer_info).symbolize_keys
|
|
108
107
|
# special cases
|
|
@@ -126,8 +125,7 @@ module Aspera
|
|
|
126
125
|
end
|
|
127
126
|
agent_options[:progress] = @config.progress_bar
|
|
128
127
|
# get agent instance
|
|
129
|
-
|
|
130
|
-
self.agent_instance = new_agent
|
|
128
|
+
self.agent_instance = Agent::Base.factory_create(agent_type, agent_options)
|
|
131
129
|
Log.log.debug{"transfer agent is a #{@agent.class}"}
|
|
132
130
|
return @agent
|
|
133
131
|
end
|
|
@@ -177,7 +175,7 @@ module Aspera
|
|
|
177
175
|
when nil, FILE_LIST_FROM_ARGS
|
|
178
176
|
Log.log.debug('getting file list as parameters')
|
|
179
177
|
# get remaining arguments
|
|
180
|
-
file_list = @opt_mgr.get_next_argument('source file list',
|
|
178
|
+
file_list = @opt_mgr.get_next_argument('source file list', multiple: true)
|
|
181
179
|
raise Cli::BadArgument, 'specify at least one file on command line or use ' \
|
|
182
180
|
"--sources=#{FILE_LIST_FROM_TRANSFER_SPEC} to use transfer spec" if !file_list.is_a?(Array) || file_list.empty?
|
|
183
181
|
when FILE_LIST_FROM_TRANSFER_SPEC
|
|
@@ -218,6 +216,9 @@ module Aspera
|
|
|
218
216
|
def start(transfer_spec, rest_token: nil)
|
|
219
217
|
# check parameters
|
|
220
218
|
Aspera.assert_type(transfer_spec, Hash){'transfer_spec'}
|
|
219
|
+
if transfer_spec['remote_host'].eql?(CP4I_REMOTE_HOST_LB)
|
|
220
|
+
raise "Wrong remote host: #{CP4I_REMOTE_HOST_LB}"
|
|
221
|
+
end
|
|
221
222
|
# process :src option
|
|
222
223
|
case transfer_spec['direction']
|
|
223
224
|
when Transfer::Spec::DIRECTION_RECEIVE
|
|
@@ -243,7 +244,7 @@ module Aspera
|
|
|
243
244
|
updated_ts(transfer_spec)
|
|
244
245
|
# if TS from app has content_protection (e.g. F5), that means content is protected: ask password if not provided
|
|
245
246
|
if transfer_spec['content_protection'].eql?('decrypt') && !transfer_spec.key?('content_protection_password')
|
|
246
|
-
transfer_spec['content_protection_password'] = @opt_mgr.prompt_user_input('content protection password', true)
|
|
247
|
+
transfer_spec['content_protection_password'] = @opt_mgr.prompt_user_input('content protection password', sensitive: true)
|
|
247
248
|
end
|
|
248
249
|
# create transfer agent
|
|
249
250
|
agent_instance.start_transfer(transfer_spec, token_regenerator: rest_token)
|
data/lib/aspera/cli/version.rb
CHANGED
data/lib/aspera/environment.rb
CHANGED
|
@@ -4,17 +4,21 @@
|
|
|
4
4
|
require 'aspera/log'
|
|
5
5
|
require 'aspera/assert'
|
|
6
6
|
require 'rbconfig'
|
|
7
|
+
require 'singleton'
|
|
7
8
|
|
|
8
9
|
# cspell:words MEBI mswin bccwin
|
|
9
10
|
|
|
10
11
|
module Aspera
|
|
11
12
|
# detect OS, architecture, and specific stuff
|
|
12
13
|
class Environment
|
|
14
|
+
include Singleton
|
|
15
|
+
USER_INTERFACES = %i[text graphical].freeze
|
|
16
|
+
|
|
13
17
|
OS_WINDOWS = :windows
|
|
14
|
-
|
|
18
|
+
OS_MACOS = :osx
|
|
15
19
|
OS_LINUX = :linux
|
|
16
20
|
OS_AIX = :aix
|
|
17
|
-
OS_LIST = [OS_WINDOWS,
|
|
21
|
+
OS_LIST = [OS_WINDOWS, OS_MACOS, OS_LINUX, OS_AIX].freeze
|
|
18
22
|
CPU_X86_64 = :x86_64
|
|
19
23
|
CPU_ARM64 = :arm64
|
|
20
24
|
CPU_PPC64 = :ppc64
|
|
@@ -27,6 +31,7 @@ module Aspera
|
|
|
27
31
|
BYTES_PER_MEBIBIT = MEBI / BITS_PER_BYTE
|
|
28
32
|
|
|
29
33
|
class << self
|
|
34
|
+
@terminal_supports_unicode = nil
|
|
30
35
|
def ruby_version
|
|
31
36
|
return RbConfig::CONFIG['RUBY_PROGRAM_VERSION']
|
|
32
37
|
end
|
|
@@ -36,7 +41,7 @@ module Aspera
|
|
|
36
41
|
when /mswin/, /msys/, /mingw/, /cygwin/, /bccwin/, /wince/, /emc/
|
|
37
42
|
return OS_WINDOWS
|
|
38
43
|
when /darwin/, /mac os/
|
|
39
|
-
return
|
|
44
|
+
return OS_MACOS
|
|
40
45
|
when /linux/
|
|
41
46
|
return OS_LINUX
|
|
42
47
|
when /aix/
|
|
@@ -121,10 +126,66 @@ module Aspera
|
|
|
121
126
|
end
|
|
122
127
|
|
|
123
128
|
# @return true if we can display Unicode characters
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
|
|
136
|
+
def default_gui_mode
|
|
137
|
+
# assume not remotely connected on macos and windows
|
|
138
|
+
return :graphical if [Environment::OS_WINDOWS, Environment::OS_MACOS].include?(Environment.os)
|
|
139
|
+
# unix family
|
|
140
|
+
return :graphical if ENV.key?('DISPLAY') && !ENV['DISPLAY'].empty?
|
|
141
|
+
return :text
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# command must be non blocking
|
|
145
|
+
def open_uri_graphical(uri)
|
|
146
|
+
case Environment.os
|
|
147
|
+
when Environment::OS_MACOS then return system('open', uri.to_s)
|
|
148
|
+
when Environment::OS_WINDOWS then return system('start', 'explorer', %Q{"#{uri}"})
|
|
149
|
+
when Environment::OS_LINUX then return system('xdg-open', uri.to_s)
|
|
150
|
+
else
|
|
151
|
+
raise "no graphical open method for #{Environment.os}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def open_editor(file_path)
|
|
156
|
+
if ENV.key?('EDITOR')
|
|
157
|
+
system(ENV['EDITOR'], file_path.to_s)
|
|
158
|
+
elsif Environment.os.eql?(Environment::OS_WINDOWS)
|
|
159
|
+
system('notepad.exe', %Q{"#{file_path}"})
|
|
160
|
+
else
|
|
161
|
+
open_uri_graphical(file_path.to_s)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
attr_accessor :url_method
|
|
166
|
+
|
|
167
|
+
def initialize
|
|
168
|
+
@url_method = self.class.default_gui_mode
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Allows a user to open a Url
|
|
172
|
+
# if method is "text", then URL is displayed on terminal
|
|
173
|
+
# if method is "graphical", then the URL will be opened with the default browser.
|
|
174
|
+
# this is non blocking
|
|
175
|
+
def open_uri(the_url)
|
|
176
|
+
case @url_method
|
|
177
|
+
when :graphical
|
|
178
|
+
self.class.open_uri_graphical(the_url)
|
|
179
|
+
when :text
|
|
180
|
+
case the_url.to_s
|
|
181
|
+
when /^http/
|
|
182
|
+
puts "USER ACTION: please enter this url in a browser:\n#{the_url.to_s.red}\n"
|
|
183
|
+
else
|
|
184
|
+
puts "USER ACTION: open this:\n#{the_url.to_s.red}\n"
|
|
185
|
+
end
|
|
186
|
+
else
|
|
187
|
+
raise StandardError, "unsupported url open method: #{@url_method}"
|
|
127
188
|
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
data/lib/aspera/faspex_gw.rb
CHANGED
|
@@ -50,8 +50,9 @@ module Aspera
|
|
|
50
50
|
operation: 'POST',
|
|
51
51
|
subpath: "packages/#{package['id']}/transfer_spec/upload",
|
|
52
52
|
headers: {'Accept' => 'application/json'},
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
query: {transfer_type: Cli::Plugins::Faspex5::TRANSFER_CONNECT},
|
|
54
|
+
body: {paths: [{'destination'=>'/'}]},
|
|
55
|
+
body_type: :json
|
|
55
56
|
)[:data]
|
|
56
57
|
transfer_spec.delete('authentication')
|
|
57
58
|
# but we place it in a Faspex package creation response
|
|
@@ -94,5 +95,5 @@ module Aspera
|
|
|
94
95
|
response.body = {error: 'Unsupported endpoint'}.to_json
|
|
95
96
|
end
|
|
96
97
|
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/aspera/log.rb
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require 'aspera/colors'
|
|
4
4
|
require 'aspera/secret_hider'
|
|
5
|
-
require 'aspera/environment'
|
|
6
|
-
require 'aspera/assert'
|
|
7
5
|
require 'logger'
|
|
8
6
|
require 'pp'
|
|
9
7
|
require 'json'
|
|
10
8
|
require 'singleton'
|
|
11
9
|
|
|
10
|
+
old_verbose = $VERBOSE
|
|
11
|
+
$VERBOSE = nil
|
|
12
|
+
|
|
12
13
|
# extend Ruby logger with trace levels
|
|
13
14
|
class Logger
|
|
14
15
|
TRACE_MAX = 2
|
|
@@ -44,6 +45,8 @@ class Logger
|
|
|
44
45
|
Logger::Severity.constants.each { |severity| make_methods(severity) }
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
$VERBOSE = old_verbose
|
|
49
|
+
|
|
47
50
|
module Aspera
|
|
48
51
|
# Singleton object for logging
|
|
49
52
|
class Log
|
|
@@ -84,7 +87,7 @@ module Aspera
|
|
|
84
87
|
ensure
|
|
85
88
|
$stderr = real_stderr
|
|
86
89
|
end
|
|
87
|
-
end
|
|
90
|
+
end
|
|
88
91
|
|
|
89
92
|
attr_reader :logger_type, :logger
|
|
90
93
|
attr_writer :program_name
|
data/lib/aspera/oauth/base.rb
CHANGED
|
@@ -9,9 +9,7 @@ require 'date'
|
|
|
9
9
|
module Aspera
|
|
10
10
|
module OAuth
|
|
11
11
|
# Implement OAuth 2 for the REST client and generate a bearer token
|
|
12
|
-
#
|
|
13
|
-
# bearer tokens are kept in memory and also in a file cache for later re-use
|
|
14
|
-
# if a token is expired (api returns 4xx), call again get_authorization(refresh: true)
|
|
12
|
+
# bearer tokens are cached in memory and in a file cache for later re-use
|
|
15
13
|
# https://tools.ietf.org/html/rfc6749
|
|
16
14
|
class Base
|
|
17
15
|
# scope can be modified after creation
|
|
@@ -31,6 +29,7 @@ module Aspera
|
|
|
31
29
|
client_id: nil,
|
|
32
30
|
client_secret: nil,
|
|
33
31
|
scope: nil,
|
|
32
|
+
use_query: false,
|
|
34
33
|
path_token: 'token', # default endpoint for /token to generate token
|
|
35
34
|
token_field: 'access_token' # field with token in result of call to path_token
|
|
36
35
|
)
|
|
@@ -42,6 +41,7 @@ module Aspera
|
|
|
42
41
|
@client_id = client_id
|
|
43
42
|
@client_secret = client_secret
|
|
44
43
|
@scope = scope
|
|
44
|
+
@use_query = use_query
|
|
45
45
|
@identifiers = []
|
|
46
46
|
@identifiers.push(auth[:username]) if auth.is_a?(Hash) && auth.key?(:username)
|
|
47
47
|
# this is the OAuth API
|
|
@@ -52,13 +52,22 @@ module Aspera
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# helper method to create token as per RFC
|
|
55
|
-
def create_token_call(
|
|
55
|
+
def create_token_call(creation_params)
|
|
56
56
|
Log.log.debug{'Generating a new token'.bg_green}
|
|
57
|
+
payload = {
|
|
58
|
+
body: creation_params,
|
|
59
|
+
body_type: :www
|
|
60
|
+
}
|
|
61
|
+
if @use_query
|
|
62
|
+
payload[:query] = creation_params
|
|
63
|
+
payload[:body] = {}
|
|
64
|
+
end
|
|
57
65
|
return @api.call(
|
|
58
|
-
operation:
|
|
59
|
-
subpath:
|
|
60
|
-
headers:
|
|
61
|
-
|
|
66
|
+
operation: 'POST',
|
|
67
|
+
subpath: @path_token,
|
|
68
|
+
headers: {'Accept' => 'application/json'},
|
|
69
|
+
**payload
|
|
70
|
+
)
|
|
62
71
|
end
|
|
63
72
|
|
|
64
73
|
# @return Hash with optional general parameters
|
|
@@ -70,24 +79,27 @@ module Aspera
|
|
|
70
79
|
return call_params
|
|
71
80
|
end
|
|
72
81
|
|
|
73
|
-
# OAuth v2 token
|
|
74
|
-
#
|
|
75
|
-
|
|
82
|
+
# get an OAuth v2 token (generated, cached, refreshed)
|
|
83
|
+
# call token() to get a token.
|
|
84
|
+
# if a token is expired (api returns 4xx), call again token(refresh: true)
|
|
85
|
+
# @param cache set to false to disable cache
|
|
86
|
+
# @param refresh set to true to force refresh or re-generation (if previous failed)
|
|
87
|
+
def token(cache: true, refresh: false)
|
|
76
88
|
# generate token unique identifier for persistency (memory/disk cache)
|
|
77
89
|
token_id = IdGenerator.from_list(Factory.id(
|
|
78
90
|
@base_url,
|
|
79
|
-
|
|
91
|
+
Factory.class_to_id(self.class),
|
|
80
92
|
@identifiers,
|
|
81
93
|
@scope
|
|
82
94
|
))
|
|
83
95
|
|
|
84
96
|
# get token_data from cache (or nil), token_data is what is returned by /token
|
|
85
|
-
token_data = Factory.instance.persist_mgr.get(token_id) if
|
|
97
|
+
token_data = Factory.instance.persist_mgr.get(token_id) if cache
|
|
86
98
|
token_data = JSON.parse(token_data) unless token_data.nil?
|
|
87
99
|
# Optional optimization: check if node token is expired based on decoded content then force refresh if close enough
|
|
88
100
|
# might help in case the transfer agent cannot refresh himself
|
|
89
101
|
# `direct` agent is equipped with refresh code
|
|
90
|
-
if !
|
|
102
|
+
if !refresh && !token_data.nil?
|
|
91
103
|
decoded_token = OAuth::Factory.instance.decode_token(token_data[@token_field])
|
|
92
104
|
Log.log.debug{Log.dump('decoded_token', decoded_token)} unless decoded_token.nil?
|
|
93
105
|
if decoded_token.is_a?(Hash)
|
|
@@ -96,13 +108,13 @@ module Aspera
|
|
|
96
108
|
elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
|
|
97
109
|
end
|
|
98
110
|
# force refresh if we see a token too close from expiration
|
|
99
|
-
|
|
100
|
-
Log.log.debug{"Expiration: #{expires_at_sec} / #{
|
|
111
|
+
refresh = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < OAuth::Factory.instance.parameters[:token_expiration_guard_sec]
|
|
112
|
+
Log.log.debug{"Expiration: #{expires_at_sec} / #{refresh}"}
|
|
101
113
|
end
|
|
102
114
|
end
|
|
103
115
|
|
|
104
116
|
# an API was already called, but failed, we need to regenerate or refresh
|
|
105
|
-
if
|
|
117
|
+
if refresh
|
|
106
118
|
if token_data.is_a?(Hash) && token_data.key?('refresh_token') && !token_data['refresh_token'].eql?('not_supported')
|
|
107
119
|
# save possible refresh token, before deleting the cache
|
|
108
120
|
refresh_token = token_data['refresh_token']
|
|
@@ -133,11 +145,11 @@ module Aspera
|
|
|
133
145
|
json_data = resp[:http].body
|
|
134
146
|
token_data = JSON.parse(json_data)
|
|
135
147
|
Factory.instance.persist_mgr.put(token_id, json_data)
|
|
136
|
-
end
|
|
148
|
+
end
|
|
137
149
|
Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"}
|
|
138
150
|
# ok we shall have a token here
|
|
139
151
|
return OAuth::Factory.bearer_build(token_data[@token_field])
|
|
140
152
|
end
|
|
141
|
-
end
|
|
153
|
+
end
|
|
142
154
|
end
|
|
143
155
|
end
|
data/lib/aspera/oauth/factory.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'aspera/assert'
|
|
|
5
5
|
require 'base64'
|
|
6
6
|
module Aspera
|
|
7
7
|
module OAuth
|
|
8
|
+
# Factory to create tokens and manage their cache
|
|
8
9
|
class Factory
|
|
9
10
|
include Singleton
|
|
10
11
|
# a prefix for persistency of tokens (simplify garbage collect)
|
|
@@ -16,7 +17,11 @@ module Aspera
|
|
|
16
17
|
|
|
17
18
|
class << self
|
|
18
19
|
def bearer_build(token)
|
|
19
|
-
return BEARER_PREFIX
|
|
20
|
+
return "#{BEARER_PREFIX}#{token}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def bearer?(token)
|
|
24
|
+
return token.start_with?(BEARER_PREFIX)
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
def bearer_extract(token)
|
|
@@ -24,14 +29,11 @@ module Aspera
|
|
|
24
29
|
return token[BEARER_PREFIX.length..-1]
|
|
25
30
|
end
|
|
26
31
|
|
|
27
|
-
def bearer?(token)
|
|
28
|
-
return token.start_with?(BEARER_PREFIX)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
32
|
def id(*params)
|
|
32
33
|
return [PERSIST_CATEGORY_TOKEN, *params].flatten
|
|
33
34
|
end
|
|
34
35
|
|
|
36
|
+
# snake version of class name is the identifier
|
|
35
37
|
def class_to_id(creator_class)
|
|
36
38
|
return creator_class.name.split('::').last.capital_to_snake.to_sym
|
|
37
39
|
end
|
|
@@ -45,11 +47,8 @@ module Aspera
|
|
|
45
47
|
# token creation methods
|
|
46
48
|
@token_type_classes = {}
|
|
47
49
|
@decoders = []
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
jwt_accepted_offset_sec: 300,
|
|
51
|
-
# one hour validity (TODO: configurable?)
|
|
52
|
-
jwt_expiry_offset_sec: 3600,
|
|
50
|
+
# default parameters, others can be added by handlers
|
|
51
|
+
@parameters = {
|
|
53
52
|
# tokens older than 30 minutes will be discarded from cache
|
|
54
53
|
token_cache_expiry_sec: 1800,
|
|
55
54
|
# tokens valid for less than this duration will be regenerated
|
|
@@ -59,12 +58,12 @@ module Aspera
|
|
|
59
58
|
|
|
60
59
|
public
|
|
61
60
|
|
|
62
|
-
attr_reader :
|
|
61
|
+
attr_reader :parameters
|
|
63
62
|
|
|
64
63
|
def persist_mgr=(manager)
|
|
65
64
|
@persist = manager
|
|
66
65
|
# cleanup expired tokens
|
|
67
|
-
@persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @
|
|
66
|
+
@persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @parameters[:token_cache_expiry_sec])
|
|
68
67
|
end
|
|
69
68
|
|
|
70
69
|
def persist_mgr
|
|
@@ -104,7 +103,7 @@ module Aspera
|
|
|
104
103
|
# @param id_create called to generate unique id for token, for cache
|
|
105
104
|
def register_token_creator(creator_class)
|
|
106
105
|
Aspera.assert_type(creator_class, Class)
|
|
107
|
-
id =
|
|
106
|
+
id = Factory.class_to_id(creator_class)
|
|
108
107
|
Log.log.debug{"registering token creator #{id}"}
|
|
109
108
|
@token_type_classes[id] = creator_class
|
|
110
109
|
end
|
data/lib/aspera/oauth/generic.rb
CHANGED
data/lib/aspera/oauth/jwt.rb
CHANGED
|
@@ -5,41 +5,44 @@ require 'aspera/assert'
|
|
|
5
5
|
require 'securerandom'
|
|
6
6
|
module Aspera
|
|
7
7
|
module OAuth
|
|
8
|
+
# remove 5 minutes to account for time offset between client and server (TODO: configurable?)
|
|
9
|
+
Factory.instance.parameters[:jwt_accepted_offset_sec] = 300
|
|
10
|
+
# one hour validity (TODO: configurable?)
|
|
11
|
+
Factory.instance.parameters[:jwt_expiry_offset_sec] = 3600
|
|
8
12
|
# Authentication using private key
|
|
13
|
+
# https://tools.ietf.org/html/rfc7523
|
|
14
|
+
# https://tools.ietf.org/html/rfc7519
|
|
9
15
|
class Jwt < Base
|
|
10
|
-
# @param
|
|
11
|
-
# @param
|
|
12
|
-
# @param
|
|
16
|
+
# @param private_key_obj private key object
|
|
17
|
+
# @param payload payload to be included in the JWT
|
|
18
|
+
# @param headers headers to be included in the JWT
|
|
13
19
|
def initialize(
|
|
14
|
-
payload:,
|
|
15
20
|
private_key_obj:,
|
|
21
|
+
payload:,
|
|
16
22
|
headers: {},
|
|
17
23
|
**base_params
|
|
18
24
|
)
|
|
19
|
-
Aspera.assert_type(payload, Hash){'payload'}
|
|
20
25
|
Aspera.assert_type(private_key_obj, OpenSSL::PKey::RSA){'private_key_obj'}
|
|
26
|
+
Aspera.assert_type(payload, Hash){'payload'}
|
|
21
27
|
Aspera.assert_type(headers, Hash){'headers'}
|
|
22
28
|
super(**base_params)
|
|
23
29
|
@private_key_obj = private_key_obj
|
|
24
|
-
@
|
|
30
|
+
@additional_payload = payload
|
|
25
31
|
@headers = headers
|
|
26
|
-
@identifiers.push(@
|
|
32
|
+
@identifiers.push(@additional_payload[:sub])
|
|
27
33
|
end
|
|
28
34
|
|
|
29
35
|
def create_token
|
|
30
|
-
# https://tools.ietf.org/html/rfc7523
|
|
31
|
-
# https://tools.ietf.org/html/rfc7519
|
|
32
36
|
require 'jwt'
|
|
33
37
|
seconds_since_epoch = Time.new.to_i
|
|
34
38
|
Log.log.info{"seconds=#{seconds_since_epoch}"}
|
|
35
|
-
Aspera.assert(@payload.is_a?(Hash)){'missing JWT payload'}
|
|
36
39
|
jwt_payload = {
|
|
37
|
-
exp: seconds_since_epoch + OAuth::Factory.instance.
|
|
38
|
-
nbf: seconds_since_epoch - OAuth::Factory.instance.
|
|
39
|
-
iat: seconds_since_epoch - OAuth::Factory.instance.
|
|
40
|
+
exp: seconds_since_epoch + OAuth::Factory.instance.parameters[:jwt_expiry_offset_sec], # expiration time
|
|
41
|
+
nbf: seconds_since_epoch - OAuth::Factory.instance.parameters[:jwt_accepted_offset_sec], # not before
|
|
42
|
+
iat: seconds_since_epoch - OAuth::Factory.instance.parameters[:jwt_accepted_offset_sec] + 1, # issued at
|
|
40
43
|
jti: SecureRandom.uuid # JWT id
|
|
41
|
-
}.merge(@
|
|
42
|
-
Log.log.debug{
|
|
44
|
+
}.merge(@additional_payload)
|
|
45
|
+
Log.log.debug{Log.dump(:jwt_payload, jwt_payload)}
|
|
43
46
|
Log.log.debug{"private=[#{@private_key_obj}]"}
|
|
44
47
|
assertion = JWT.encode(jwt_payload, @private_key_obj, 'RS256', @headers)
|
|
45
48
|
Log.log.debug{"assertion=[#{assertion}]"}
|
|
@@ -4,16 +4,17 @@ require 'aspera/oauth/base'
|
|
|
4
4
|
|
|
5
5
|
module Aspera
|
|
6
6
|
module OAuth
|
|
7
|
+
# This class is used to create a token using a JSON body and a URL
|
|
7
8
|
class UrlJson < Base
|
|
8
9
|
def initialize(
|
|
9
|
-
json:,
|
|
10
10
|
url:,
|
|
11
|
+
json:,
|
|
11
12
|
**generic_params
|
|
12
13
|
)
|
|
13
14
|
super(**generic_params)
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@identifiers.push(@
|
|
15
|
+
@body = json
|
|
16
|
+
@query = url
|
|
17
|
+
@identifiers.push(@body[:url_token])
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def create_token
|
|
@@ -21,8 +22,9 @@ module Aspera
|
|
|
21
22
|
operation: 'POST',
|
|
22
23
|
subpath: @path_token,
|
|
23
24
|
headers: {'Accept' => 'application/json'},
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
query: @query.merge(scope: @scope), # scope is here because it may change over time (node)
|
|
26
|
+
body: @body,
|
|
27
|
+
body_type: :json
|
|
26
28
|
)
|
|
27
29
|
end
|
|
28
30
|
end
|