aspera-cli 4.2.2 → 4.3.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
- data/README.md +104 -36
- data/docs/README.erb.md +107 -41
- data/docs/test_env.conf +1 -0
- data/lib/aspera/aoc.rb +20 -19
- data/lib/aspera/cli/plugins/aoc.rb +25 -14
- data/lib/aspera/cli/plugins/config.rb +2 -1
- data/lib/aspera/cli/plugins/faspex.rb +3 -2
- data/lib/aspera/cli/plugins/faspex5.rb +3 -2
- data/lib/aspera/cli/plugins/node.rb +3 -2
- data/lib/aspera/cli/plugins/preview.rb +56 -36
- data/lib/aspera/cli/transfer_agent.rb +21 -13
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/command_line_builder.rb +0 -1
- data/lib/aspera/cos_node.rb +4 -3
- data/lib/aspera/fasp/aoc.rb +1 -1
- data/lib/aspera/fasp/local.rb +31 -22
- data/lib/aspera/fasp/node.rb +23 -1
- data/lib/aspera/id_generator.rb +22 -0
- data/lib/aspera/node.rb +2 -4
- data/lib/aspera/oauth.rb +112 -96
- data/lib/aspera/persistency_action_once.rb +11 -7
- data/lib/aspera/persistency_folder.rb +6 -26
- data/lib/aspera/rest.rb +1 -1
- data/lib/aspera/timer_limiter.rb +22 -0
- metadata +4 -2
data/lib/aspera/fasp/local.rb
CHANGED
@@ -21,11 +21,12 @@ module Aspera
|
|
21
21
|
ACCESS_KEY_TRANSFER_USER='xfer'
|
22
22
|
# executes a local "ascp", connects mgt port, equivalent of "Fasp Manager"
|
23
23
|
class Local < Manager
|
24
|
-
# options for initialize
|
24
|
+
# options for initialize (same as values in option transfer_info)
|
25
25
|
DEFAULT_OPTIONS = {
|
26
26
|
:spawn_timeout_sec => 3,
|
27
27
|
:spawn_delay_sec => 2,
|
28
28
|
:wss => false,
|
29
|
+
:multi_incr_udp => true,
|
29
30
|
:resume => {}
|
30
31
|
}
|
31
32
|
DEFAULT_UDP_PORT=33001
|
@@ -64,21 +65,27 @@ module Aspera
|
|
64
65
|
transfer_spec['EX_ssh_key_paths'] = Installation.instance.bypass_keys
|
65
66
|
end
|
66
67
|
|
67
|
-
#
|
68
|
-
#
|
69
|
-
|
70
|
-
multi_session_number=0
|
68
|
+
# Compute this before using transfer spec because it potentially modifies the transfer spec
|
69
|
+
# (even if the var is not used in single session)
|
70
|
+
multi_session_info=nil
|
71
71
|
if transfer_spec.has_key?('multi_session')
|
72
|
-
|
73
|
-
|
72
|
+
multi_session_info={
|
73
|
+
count: transfer_spec['multi_session'].to_i,
|
74
|
+
}
|
75
|
+
# Managed by multi-session, so delete from transfer spec
|
76
|
+
transfer_spec.delete('multi_session')
|
77
|
+
if multi_session_info[:count] < 0
|
74
78
|
Log.log.error("multi_session(#{transfer_spec['multi_session']}) shall be integer >= 0")
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
if
|
81
|
-
|
79
|
+
multi_session_info = nil
|
80
|
+
elsif multi_session_info[:count].eql?(0)
|
81
|
+
Log.log.debug("multi_session count is zero: no multisession")
|
82
|
+
multi_session_info = nil
|
83
|
+
else # multi_session_info[:count] > 0
|
84
|
+
# if option not true: keep default udp port for all sessions
|
85
|
+
if @options[:multi_incr_udp]
|
86
|
+
# override if specified, else use default value
|
87
|
+
multi_session_info[:udp_base]=transfer_spec.has_key?('fasp_port') ? transfer_spec['fasp_port'] : DEFAULT_UDP_PORT
|
88
|
+
# delete from original transfer spec, as we will increment values
|
82
89
|
transfer_spec.delete('fasp_port')
|
83
90
|
end
|
84
91
|
end
|
@@ -111,22 +118,23 @@ module Aspera
|
|
111
118
|
:options => job_options # [Hash]
|
112
119
|
}
|
113
120
|
|
114
|
-
if
|
121
|
+
if multi_session_info.nil?
|
115
122
|
Log.log.debug('Starting single session thread')
|
116
123
|
# single session for transfer : simple
|
117
124
|
session[:thread] = Thread.new(session) {|s|transfer_thread_entry(s)}
|
118
125
|
xfer_job[:sessions].push(session)
|
119
126
|
else
|
120
127
|
Log.log.debug('Starting multi session threads')
|
121
|
-
1.upto(
|
128
|
+
1.upto(multi_session_info[:count]) do |i|
|
129
|
+
# do not delay the first session
|
122
130
|
sleep(@options[:spawn_delay_sec]) unless i.eql?(1)
|
123
131
|
# do deep copy (each thread has its own copy because it is modified here below and in thread)
|
124
132
|
this_session=session.clone()
|
125
133
|
this_session[:env_args]=this_session[:env_args].clone()
|
126
134
|
this_session[:env_args][:args]=this_session[:env_args][:args].clone()
|
127
|
-
this_session[:env_args][:args].unshift("-C#{i}:#{
|
128
|
-
#
|
129
|
-
this_session[:env_args][:args].unshift('-O',"#{
|
135
|
+
this_session[:env_args][:args].unshift("-C#{i}:#{multi_session_info[:count]}")
|
136
|
+
# option: increment (default as per ascp manual) or not (cluster on other side ?)
|
137
|
+
this_session[:env_args][:args].unshift('-O',"#{multi_session_info[:udp_base]+i-1}") if @options[:multi_incr_udp]
|
130
138
|
this_session[:thread] = Thread.new(this_session) {|s|transfer_thread_entry(s)}
|
131
139
|
xfer_job[:sessions].push(this_session)
|
132
140
|
end
|
@@ -260,6 +268,7 @@ module Aspera
|
|
260
268
|
Log.log.error('need to regenerate token'.red)
|
261
269
|
if session[:options].is_a?(Hash) and session[:options].has_key?(:regenerate_token)
|
262
270
|
# regenerate token here, expired, or error on it
|
271
|
+
# Note: in multi-session, each session will have a different one.
|
263
272
|
env_args[:env]['ASPERA_SCP_TOKEN']=session[:options][:regenerate_token].call(true)
|
264
273
|
end
|
265
274
|
end
|
@@ -323,7 +332,7 @@ module Aspera
|
|
323
332
|
|
324
333
|
private
|
325
334
|
|
326
|
-
# @param options : keys(symbol):
|
335
|
+
# @param options : keys(symbol): see DEFAULT_OPTIONS
|
327
336
|
def initialize(options=nil)
|
328
337
|
super()
|
329
338
|
# by default no interactive progress bar
|
@@ -332,7 +341,7 @@ module Aspera
|
|
332
341
|
@jobs={}
|
333
342
|
# mutex protects global data accessed by threads
|
334
343
|
@mutex=Mutex.new
|
335
|
-
#
|
344
|
+
# set default options and override if specified
|
336
345
|
@options=DEFAULT_OPTIONS.clone
|
337
346
|
if !options.nil?
|
338
347
|
raise "expecting Hash (or nil), but have #{options.class}" unless options.is_a?(Hash)
|
@@ -340,7 +349,7 @@ module Aspera
|
|
340
349
|
if DEFAULT_OPTIONS.has_key?(k)
|
341
350
|
@options[k]=v
|
342
351
|
else
|
343
|
-
raise "
|
352
|
+
raise "Unknown local agent parameter: #{k}, expect one of #{DEFAULT_OPTIONS.keys.map{|i|i.to_s}.join(",")}"
|
344
353
|
end
|
345
354
|
end
|
346
355
|
end
|
data/lib/aspera/fasp/node.rb
CHANGED
@@ -7,9 +7,12 @@ module Aspera
|
|
7
7
|
# this singleton class is used by the CLI to provide a common interface to start a transfer
|
8
8
|
# before using it, the use must set the `node_api` member.
|
9
9
|
class Node < Manager
|
10
|
-
|
10
|
+
# option include: root_id if the node is an access key
|
11
|
+
attr_writer :options
|
12
|
+
def initialize(node_api,options={})
|
11
13
|
super()
|
12
14
|
@node_api=node_api
|
15
|
+
@options=options
|
13
16
|
# TODO: currently only supports one transfer. This is bad shortcut. but ok for CLI.
|
14
17
|
@transfer_id=nil
|
15
18
|
end
|
@@ -32,6 +35,25 @@ module Aspera
|
|
32
35
|
|
33
36
|
# generic method
|
34
37
|
def start_transfer(transfer_spec,options=nil)
|
38
|
+
# add root id if access key
|
39
|
+
if @options.has_key?(:root_id)
|
40
|
+
case transfer_spec['direction']
|
41
|
+
when 'send';transfer_spec['source_root_id']=@options[:root_id]
|
42
|
+
when 'receive';transfer_spec['destination_root_id']=@options[:root_id]
|
43
|
+
else raise "unexpected direction in ts: #{transfer_spec['direction']}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
# manage special additional parameter
|
47
|
+
if transfer_spec.has_key?('EX_ssh_key_paths') and transfer_spec['EX_ssh_key_paths'].is_a?(Array) and !transfer_spec['EX_ssh_key_paths'].empty?
|
48
|
+
# not standard, so place standard field
|
49
|
+
if transfer_spec.has_key?('ssh_private_key')
|
50
|
+
Log.log.warn('Both ssh_private_key and EX_ssh_key_paths are present, using ssh_private_key')
|
51
|
+
else
|
52
|
+
Log.log.warn('EX_ssh_key_paths has multiple keys, using first one only') unless transfer_spec['EX_ssh_key_paths'].length.eql?(1)
|
53
|
+
transfer_spec['ssh_private_key']=File.read(transfer_spec['EX_ssh_key_paths'].first)
|
54
|
+
transfer_spec.delete('EX_ssh_key_paths')
|
55
|
+
end
|
56
|
+
end
|
35
57
|
if transfer_spec['tags'].is_a?(Hash) and transfer_spec['tags']['aspera'].is_a?(Hash)
|
36
58
|
transfer_spec['tags']['aspera']['xfer_retry']||=150
|
37
59
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Aspera
|
4
|
+
class IdGenerator
|
5
|
+
ID_SEPARATOR='_'
|
6
|
+
WINDOWS_PROTECTED_CHAR=%r{[/:"<>\\\*\?]}
|
7
|
+
PROTECTED_CHAR_REPLACE='_'
|
8
|
+
private_constant :ID_SEPARATOR,:PROTECTED_CHAR_REPLACE,:WINDOWS_PROTECTED_CHAR
|
9
|
+
def self.from_list(object_id)
|
10
|
+
if object_id.is_a?(Array)
|
11
|
+
object_id=object_id.select{|i|!i.nil?}.map do |i|
|
12
|
+
(i.is_a?(String) and i.start_with?('https://')) ? URI.parse(i).host : i.to_s
|
13
|
+
end.join(ID_SEPARATOR)
|
14
|
+
end
|
15
|
+
raise "id must be a String" unless object_id.is_a?(String)
|
16
|
+
return object_id.
|
17
|
+
gsub(WINDOWS_PROTECTED_CHAR,PROTECTED_CHAR_REPLACE). # remove windows forbidden chars
|
18
|
+
gsub('.',PROTECTED_CHAR_REPLACE). # keep dot for extension only (nicer)
|
19
|
+
downcase
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/aspera/node.rb
CHANGED
@@ -10,10 +10,8 @@ module Aspera
|
|
10
10
|
ACCESS_LEVELS=['delete','list','mkdir','preview','read','rename','write']
|
11
11
|
MATCH_EXEC_PREFIX='exec:'
|
12
12
|
|
13
|
-
#
|
14
|
-
|
15
|
-
return JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition('==SIGNATURE==').first)
|
16
|
-
end
|
13
|
+
# register node special token decoder
|
14
|
+
Oauth.register_decoder(lambda{|token|JSON.parse(Zlib::Inflate.inflate(Base64.decode64(token)).partition('==SIGNATURE==').first)})
|
17
15
|
|
18
16
|
# for access keys: provide expression to match entry in folder
|
19
17
|
# if no prefix: regex
|
data/lib/aspera/oauth.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'aspera/open_application'
|
2
2
|
require 'aspera/web_auth'
|
3
|
+
require 'aspera/id_generator'
|
3
4
|
require 'base64'
|
4
5
|
require 'date'
|
5
6
|
require 'socket'
|
@@ -9,8 +10,14 @@ module Aspera
|
|
9
10
|
# Implement OAuth 2 for the REST client and generate a bearer token
|
10
11
|
# call get_authorization() to get a token.
|
11
12
|
# bearer tokens are kept in memory and also in a file cache for later re-use
|
12
|
-
# if a token is expired (api returns 4xx), call again get_authorization({:
|
13
|
+
# if a token is expired (api returns 4xx), call again get_authorization({refresh: true})
|
13
14
|
class Oauth
|
15
|
+
# used for code exchange
|
16
|
+
DEFAULT_PATH_AUTHORIZE='authorize'
|
17
|
+
# to generate token
|
18
|
+
DEFAULT_PATH_TOKEN='token'
|
19
|
+
# field with token in result
|
20
|
+
DEFAULT_TOKEN_FIELD='access_token'
|
14
21
|
private
|
15
22
|
# remove 5 minutes to account for time offset (TODO: configurable?)
|
16
23
|
JWT_NOTBEFORE_OFFSET_SEC=300
|
@@ -20,7 +27,9 @@ module Aspera
|
|
20
27
|
TOKEN_CACHE_EXPIRY_SEC=1800
|
21
28
|
# a prefix for persistency of tokens (garbage collect)
|
22
29
|
PERSIST_CATEGORY_TOKEN='token'
|
23
|
-
|
30
|
+
ONE_HOUR_AS_DAY_FRACTION=Rational(1,24)
|
31
|
+
|
32
|
+
private_constant :JWT_NOTBEFORE_OFFSET_SEC,:JWT_EXPIRY_OFFSET_SEC,:PERSIST_CATEGORY_TOKEN,:TOKEN_CACHE_EXPIRY_SEC,:ONE_HOUR_AS_DAY_FRACTION
|
24
33
|
class << self
|
25
34
|
# OAuth methods supported
|
26
35
|
def auth_types
|
@@ -45,8 +54,25 @@ module Aspera
|
|
45
54
|
def flush_tokens
|
46
55
|
persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,nil)
|
47
56
|
end
|
57
|
+
|
58
|
+
def register_decoder(method)
|
59
|
+
@decoders||=[]
|
60
|
+
@decoders.push(method)
|
61
|
+
end
|
62
|
+
|
63
|
+
def decode_token(token)
|
64
|
+
Log.log.debug(">>>> #{token} : #{@decoders.length}")
|
65
|
+
@decoders.each do |decoder|
|
66
|
+
result=decoder.call(token) rescue nil
|
67
|
+
return result unless result.nil?
|
68
|
+
end
|
69
|
+
return nil
|
70
|
+
end
|
48
71
|
end
|
49
72
|
|
73
|
+
# seems to be quite standard token encoding (RFC?)
|
74
|
+
self.register_decoder lambda { |token| parts=token.split('.'); raise "not aoc token" unless parts.length.eql?(3); JSON.parse(Base64.decode64(parts[1]))}
|
75
|
+
|
50
76
|
# for supported parameters, look in the code for @params
|
51
77
|
# parameters are provided all with oauth_ prefix :
|
52
78
|
# :base_url
|
@@ -56,8 +82,8 @@ module Aspera
|
|
56
82
|
# :jwt_audience
|
57
83
|
# :jwt_private_key_obj
|
58
84
|
# :jwt_subject
|
59
|
-
# :path_authorize (default:
|
60
|
-
# :path_token (default:
|
85
|
+
# :path_authorize (default: DEFAULT_PATH_AUTHORIZE)
|
86
|
+
# :path_token (default: DEFAULT_PATH_TOKEN)
|
61
87
|
# :scope (optional)
|
62
88
|
# :grant (one of returned by self.auth_types)
|
63
89
|
# :url_token
|
@@ -65,21 +91,21 @@ module Aspera
|
|
65
91
|
# :user_pass
|
66
92
|
# :token_type
|
67
93
|
def initialize(auth_params)
|
68
|
-
Log.log.debug
|
94
|
+
Log.log.debug("auth=#{auth_params}")
|
69
95
|
@params=auth_params.clone
|
70
96
|
# default values
|
71
97
|
# name of field to take as token from result of call to /token
|
72
|
-
@params[:token_field]||=
|
98
|
+
@params[:token_field]||=DEFAULT_TOKEN_FIELD
|
73
99
|
# default endpoint for /token
|
74
|
-
@params[:path_token]||=
|
100
|
+
@params[:path_token]||=DEFAULT_PATH_TOKEN
|
75
101
|
# default endpoint for /authorize
|
76
|
-
@params[:path_authorize]||=
|
77
|
-
rest_params={:
|
102
|
+
@params[:path_authorize]||=DEFAULT_PATH_AUTHORIZE
|
103
|
+
rest_params={base_url: @params[:base_url]}
|
78
104
|
if @params.has_key?(:client_id)
|
79
|
-
rest_params.merge!({:
|
80
|
-
:
|
81
|
-
:
|
82
|
-
:
|
105
|
+
rest_params.merge!({auth: {
|
106
|
+
type: :basic,
|
107
|
+
username: @params[:client_id],
|
108
|
+
password: @params[:client_secret]
|
83
109
|
}})
|
84
110
|
end
|
85
111
|
@token_auth_api=Rest.new(rest_params)
|
@@ -107,28 +133,20 @@ module Aspera
|
|
107
133
|
return code
|
108
134
|
end
|
109
135
|
|
110
|
-
def
|
136
|
+
def create_token(rest_params)
|
111
137
|
return @token_auth_api.call({
|
112
|
-
:
|
113
|
-
:
|
114
|
-
:
|
115
|
-
end
|
116
|
-
|
117
|
-
# shortcut for create_token_advanced
|
118
|
-
def create_token_www_body(creation_params)
|
119
|
-
return create_token_advanced({:www_body_params=>creation_params})
|
138
|
+
operation: 'POST',
|
139
|
+
subpath: @params[:path_token],
|
140
|
+
headers: {'Accept'=>'application/json'}}.merge(rest_params))
|
120
141
|
end
|
121
142
|
|
122
|
-
# @return
|
123
|
-
def
|
143
|
+
# @return unique identifier of token
|
144
|
+
def token_cache_id(api_scope)
|
124
145
|
oauth_uri=URI.parse(@params[:base_url])
|
125
|
-
parts=[PERSIST_CATEGORY_TOKEN,oauth_uri.host
|
126
|
-
|
127
|
-
parts.push(@params[
|
128
|
-
|
129
|
-
parts.push(@params[:url_token]) if @params.has_key?(:url_token)
|
130
|
-
parts.push(@params[:api_key]) if @params.has_key?(:api_key)
|
131
|
-
return parts
|
146
|
+
parts=[PERSIST_CATEGORY_TOKEN,api_scope,oauth_uri.host,oauth_uri.path]
|
147
|
+
# add some of the parameters that uniquely define the token
|
148
|
+
[:grant,:jwt_subject,:user_name,:url_token,:api_key].inject(parts){|p,i|p.push(@params[i])}
|
149
|
+
return IdGenerator.from_list(parts)
|
132
150
|
end
|
133
151
|
|
134
152
|
public
|
@@ -148,22 +166,23 @@ module Aspera
|
|
148
166
|
use_refresh_token=options[:refresh]
|
149
167
|
|
150
168
|
# generate token identifier to use with cache
|
151
|
-
|
169
|
+
token_id=token_cache_id(api_scope)
|
152
170
|
|
153
171
|
# get token_data from cache (or nil), token_data is what is returned by /token
|
154
|
-
token_data=self.class.persist_mgr.get(
|
172
|
+
token_data=self.class.persist_mgr.get(token_id)
|
155
173
|
token_data=JSON.parse(token_data) unless token_data.nil?
|
156
|
-
|
157
174
|
# Optional optimization: check if node token is expired, then force refresh
|
158
175
|
# in case the transfer agent cannot refresh himself
|
159
176
|
# else, anyway, faspmanager is equipped with refresh code
|
160
177
|
if !token_data.nil?
|
161
|
-
|
178
|
+
# TODO: use @params[:token_field] ?
|
179
|
+
decoded_node_token = self.class.decode_token(token_data['access_token'])
|
180
|
+
Log.dump('decoded_node_token',decoded_node_token) unless decoded_node_token.nil?
|
162
181
|
if decoded_node_token.is_a?(Hash) and decoded_node_token['expires_at'].is_a?(String)
|
163
|
-
Log.dump('decoded_node_token',decoded_node_token)
|
164
182
|
expires_at=DateTime.parse(decoded_node_token['expires_at'])
|
165
|
-
|
166
|
-
|
183
|
+
# Time.at(decoded_node_token['exp'])
|
184
|
+
# does it seem expired, with one hour of security
|
185
|
+
use_refresh_token=true if DateTime.now > (expires_at-ONE_HOUR_AS_DAY_FRACTION)
|
167
186
|
end
|
168
187
|
end
|
169
188
|
|
@@ -174,7 +193,7 @@ module Aspera
|
|
174
193
|
refresh_token=token_data['refresh_token']
|
175
194
|
end
|
176
195
|
# delete caches
|
177
|
-
self.class.persist_mgr.delete(
|
196
|
+
self.class.persist_mgr.delete(token_id)
|
178
197
|
token_data=nil
|
179
198
|
# lets try the existing refresh token
|
180
199
|
if !refresh_token.nil?
|
@@ -182,14 +201,14 @@ module Aspera
|
|
182
201
|
# try to refresh
|
183
202
|
# note: admin token has no refresh, and lives by default 1800secs
|
184
203
|
# Note: scope is mandatory in Files, and we can either provide basic auth, or client_Secret in data
|
185
|
-
resp=
|
186
|
-
:
|
187
|
-
:refresh_token
|
204
|
+
resp=create_token(www_body_params: p_client_id_and_scope.merge({
|
205
|
+
grant_type: 'refresh_token',
|
206
|
+
refresh_token: refresh_token}))
|
188
207
|
if resp[:http].code.start_with?('2') then
|
189
|
-
# save only if success
|
208
|
+
# save only if success
|
190
209
|
json_data=resp[:http].body
|
191
210
|
token_data=JSON.parse(json_data)
|
192
|
-
self.class.persist_mgr.put(
|
211
|
+
self.class.persist_mgr.put(token_id,json_data)
|
193
212
|
else
|
194
213
|
Log.log.debug("refresh failed: #{resp[:http].body}".bg_red)
|
195
214
|
end
|
@@ -204,19 +223,19 @@ module Aspera
|
|
204
223
|
# AoC Web based Auth
|
205
224
|
check_code=SecureRandom.uuid
|
206
225
|
auth_params=p_client_id_and_scope.merge({
|
207
|
-
:
|
208
|
-
:
|
209
|
-
:
|
226
|
+
response_type: 'code',
|
227
|
+
redirect_uri: @params[:redirect_uri],
|
228
|
+
state: check_code
|
210
229
|
})
|
211
230
|
auth_params[:client_secret]=@params[:client_secret] if @params.has_key?(:client_secret)
|
212
231
|
login_page_url=Rest.build_uri("#{@params[:base_url]}/#{@params[:path_authorize]}",auth_params)
|
213
232
|
# here, we need a human to authorize on a web page
|
214
233
|
code=goto_page_and_get_code(login_page_url,check_code)
|
215
234
|
# exchange code for token
|
216
|
-
resp=
|
217
|
-
:
|
218
|
-
:
|
219
|
-
:
|
235
|
+
resp=create_token(www_body_params: p_client_id_and_scope.merge({
|
236
|
+
grant_type: 'authorization_code',
|
237
|
+
code: code,
|
238
|
+
redirect_uri: @params[:redirect_uri]
|
220
239
|
}))
|
221
240
|
when :jwt
|
222
241
|
# https://tools.ietf.org/html/rfc7519
|
@@ -226,18 +245,18 @@ module Aspera
|
|
226
245
|
Log.log.info("seconds=#{seconds_since_epoch}")
|
227
246
|
|
228
247
|
payload = {
|
229
|
-
:
|
230
|
-
:
|
231
|
-
:
|
232
|
-
:
|
233
|
-
:
|
248
|
+
iss: @params[:client_id], # issuer
|
249
|
+
sub: @params[:jwt_subject], # subject
|
250
|
+
aud: @params[:jwt_audience], # audience
|
251
|
+
nbf: seconds_since_epoch-JWT_NOTBEFORE_OFFSET_SEC, # not before
|
252
|
+
exp: seconds_since_epoch+JWT_EXPIRY_OFFSET_SEC # expiration
|
234
253
|
}
|
235
254
|
# Hum.. compliant ? TODO: remove when Faspex5 API is clarified
|
236
255
|
if @params.has_key?(:f5_username)
|
237
256
|
payload[:jti] = SecureRandom.uuid # JWT id
|
238
257
|
payload[:iat] = seconds_since_epoch # issued at
|
239
|
-
payload.delete(:nbf)
|
240
|
-
p_scope[:redirect_uri]="https://127.0.0.1:5000/token"
|
258
|
+
payload.delete(:nbf) # not used in f5
|
259
|
+
p_scope[:redirect_uri]="https://127.0.0.1:5000/token" # used ?
|
241
260
|
p_scope[:state]=SecureRandom.uuid
|
242
261
|
p_scope[:client_id]=@params[:client_id]
|
243
262
|
@token_auth_api.params[:auth]={type: :basic,username: @params[:f5_username], password: @params[:f5_password]}
|
@@ -245,65 +264,62 @@ module Aspera
|
|
245
264
|
|
246
265
|
# non standard, only for global ids
|
247
266
|
payload.merge!(@params[:jwt_add]) if @params.has_key?(:jwt_add)
|
267
|
+
Log.log.debug("JWT payload=[#{payload}]")
|
248
268
|
|
249
269
|
rsa_private=@params[:jwt_private_key_obj] # type: OpenSSL::PKey::RSA
|
250
|
-
|
251
270
|
Log.log.debug("private=[#{rsa_private}]")
|
252
271
|
|
253
|
-
|
254
|
-
assertion = JWT.encode(payload, rsa_private, 'RS256',@params[:jwt_headers]||{})
|
255
|
-
|
272
|
+
assertion = JWT.encode(payload, rsa_private, 'RS256', @params[:jwt_headers]||{})
|
256
273
|
Log.log.debug("assertion=[#{assertion}]")
|
257
274
|
|
258
|
-
resp=
|
259
|
-
:
|
260
|
-
:
|
275
|
+
resp=create_token(www_body_params: p_scope.merge({
|
276
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
277
|
+
assertion: assertion
|
261
278
|
}))
|
262
279
|
when :url_token
|
263
280
|
# AoC Public Link
|
264
|
-
params={:
|
281
|
+
params={url_token: @params[:url_token]}
|
265
282
|
params[:password]=@params[:password] if @params.has_key?(:password)
|
266
|
-
resp=
|
267
|
-
:
|
268
|
-
:
|
269
|
-
|
270
|
-
})})
|
283
|
+
resp=create_token({
|
284
|
+
json_params: params,
|
285
|
+
url_params: p_scope.merge({grant_type: 'url_token'})
|
286
|
+
})
|
271
287
|
when :ibm_apikey
|
272
288
|
# ATS
|
273
|
-
resp=
|
274
|
-
|
275
|
-
|
276
|
-
|
289
|
+
resp=create_token(www_body_params: {
|
290
|
+
grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
|
291
|
+
response_type: 'cloud_iam',
|
292
|
+
apikey: @params[:api_key]
|
277
293
|
})
|
278
294
|
when :delegated_refresh
|
279
295
|
# COS
|
280
|
-
resp=
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
296
|
+
resp=create_token(www_body_params: {
|
297
|
+
grant_type: 'urn:ibm:params:oauth:grant-type:apikey',
|
298
|
+
response_type: 'delegated_refresh_token',
|
299
|
+
apikey: @params[:api_key],
|
300
|
+
receiver_client_ids: 'aspera_ats'
|
285
301
|
})
|
286
302
|
when :header_userpass
|
287
303
|
# used in Faspex apiv4 and shares2
|
288
|
-
resp=
|
289
|
-
|
290
|
-
|
291
|
-
:
|
292
|
-
:
|
293
|
-
:
|
294
|
-
|
304
|
+
resp=create_token(
|
305
|
+
json_params: p_client_id_and_scope.merge({grant_type: 'password'}), #:www_body_params also works
|
306
|
+
auth: {
|
307
|
+
type: :basic,
|
308
|
+
username: @params[:user_name],
|
309
|
+
password: @params[:user_pass]}
|
310
|
+
)
|
295
311
|
when :body_userpass
|
296
312
|
# legacy, not used
|
297
|
-
resp=
|
298
|
-
:
|
299
|
-
:
|
300
|
-
:
|
313
|
+
resp=create_token(www_body_params: p_client_id_and_scope.merge({
|
314
|
+
grant_type: 'password',
|
315
|
+
username: @params[:user_name],
|
316
|
+
password: @params[:user_pass]
|
301
317
|
}))
|
302
318
|
when :body_data
|
303
319
|
# used in Faspex apiv5
|
304
|
-
resp=
|
305
|
-
:
|
306
|
-
:
|
320
|
+
resp=create_token({
|
321
|
+
auth: {type: :none},
|
322
|
+
json_params: @params[:userpass_body],
|
307
323
|
})
|
308
324
|
else
|
309
325
|
raise "auth grant type unknown: #{@params[:grant]}"
|
@@ -311,9 +327,9 @@ module Aspera
|
|
311
327
|
# TODO: test return code ?
|
312
328
|
json_data=resp[:http].body
|
313
329
|
token_data=JSON.parse(json_data)
|
314
|
-
self.class.persist_mgr.put(
|
330
|
+
self.class.persist_mgr.put(token_id,json_data)
|
315
331
|
end # if ! in_cache
|
316
|
-
|
332
|
+
raise "API error: No such field in answer: #{@params[:token_field]}" unless token_data.has_key?(@params[:token_field])
|
317
333
|
# ok we shall have a token here
|
318
334
|
return 'Bearer '+token_data[@params[:token_field]]
|
319
335
|
end
|
@@ -6,7 +6,7 @@ module Aspera
|
|
6
6
|
class PersistencyActionOnce
|
7
7
|
# @param :manager Mandatory Database
|
8
8
|
# @param :data Mandatory object to persist, must be same object from begin to end (assume array by default)
|
9
|
-
# @param :
|
9
|
+
# @param :id Mandatory identifiers
|
10
10
|
# @param :delete Optional delete persistency condition
|
11
11
|
# @param :parse Optional parse method (default to JSON)
|
12
12
|
# @param :format Optional dump method (default to JSON)
|
@@ -16,27 +16,31 @@ module Aspera
|
|
16
16
|
raise "options shall be Hash" unless options.is_a?(Hash)
|
17
17
|
raise "mandatory :manager" if options[:manager].nil?
|
18
18
|
raise "mandatory :data" if options[:data].nil?
|
19
|
-
raise "mandatory :
|
20
|
-
raise "mandatory 1 element in :
|
19
|
+
raise "mandatory :id (String)" unless options[:id].is_a?(String)
|
20
|
+
raise "mandatory 1 element in :id" unless options[:id].length >= 1
|
21
21
|
@manager=options[:manager]
|
22
22
|
@persisted_object=options[:data]
|
23
|
-
@
|
23
|
+
@object_id=options[:id]
|
24
24
|
# by default , at save time, file is deleted if data is nil
|
25
25
|
@delete_condition=options[:delete] || lambda{|d|d.empty?}
|
26
26
|
@persist_format=options[:format] || lambda {|h| JSON.generate(h)}
|
27
27
|
persist_parse=options[:parse] || lambda {|t| JSON.parse(t)}
|
28
28
|
persist_merge=options[:merge] || lambda {|current,file| current.concat(file).uniq rescue current}
|
29
|
-
value=@manager.get(@
|
29
|
+
value=@manager.get(@object_id)
|
30
30
|
persist_merge.call(@persisted_object,persist_parse.call(value)) unless value.nil?
|
31
31
|
end
|
32
32
|
|
33
33
|
def save
|
34
34
|
if @delete_condition.call(@persisted_object)
|
35
|
-
@manager.delete(@
|
35
|
+
@manager.delete(@object_id)
|
36
36
|
else
|
37
|
-
@manager.put(@
|
37
|
+
@manager.put(@object_id,@persist_format.call(@persisted_object))
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
+
def data
|
42
|
+
return @persisted_object
|
43
|
+
end
|
44
|
+
|
41
45
|
end
|
42
46
|
end
|