aspera-cli 4.5.0 → 4.8.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 +1 -0
- data/README.md +1894 -1574
- data/bin/ascli +21 -1
- data/bin/asession +38 -34
- data/docs/test_env.conf +14 -3
- data/examples/aoc.rb +17 -15
- data/examples/dascli +26 -0
- data/examples/faspex4.rb +42 -35
- data/examples/proxy.pac +1 -1
- data/examples/transfer.rb +38 -37
- data/lib/aspera/aoc.rb +245 -205
- data/lib/aspera/ascmd.rb +111 -90
- data/lib/aspera/ats_api.rb +16 -14
- data/lib/aspera/cli/basic_auth_plugin.rb +19 -18
- data/lib/aspera/cli/extended_value.rb +50 -39
- data/lib/aspera/cli/formater.rb +161 -135
- data/lib/aspera/cli/info.rb +18 -0
- data/lib/aspera/cli/listener/line_dump.rb +4 -2
- data/lib/aspera/cli/listener/logger.rb +3 -1
- data/lib/aspera/cli/listener/progress.rb +20 -21
- data/lib/aspera/cli/listener/progress_multi.rb +29 -31
- data/lib/aspera/cli/main.rb +194 -183
- data/lib/aspera/cli/manager.rb +213 -206
- data/lib/aspera/cli/plugin.rb +71 -49
- data/lib/aspera/cli/plugins/alee.rb +8 -7
- data/lib/aspera/cli/plugins/aoc.rb +675 -558
- data/lib/aspera/cli/plugins/ats.rb +116 -109
- data/lib/aspera/cli/plugins/bss.rb +35 -34
- data/lib/aspera/cli/plugins/config.rb +722 -542
- data/lib/aspera/cli/plugins/console.rb +28 -22
- data/lib/aspera/cli/plugins/cos.rb +28 -37
- data/lib/aspera/cli/plugins/faspex.rb +281 -227
- data/lib/aspera/cli/plugins/faspex5.rb +129 -84
- data/lib/aspera/cli/plugins/node.rb +426 -232
- data/lib/aspera/cli/plugins/orchestrator.rb +106 -98
- data/lib/aspera/cli/plugins/preview.rb +196 -191
- data/lib/aspera/cli/plugins/server.rb +131 -126
- data/lib/aspera/cli/plugins/shares.rb +49 -36
- data/lib/aspera/cli/plugins/sync.rb +27 -28
- data/lib/aspera/cli/transfer_agent.rb +84 -79
- data/lib/aspera/cli/version.rb +3 -1
- data/lib/aspera/colors.rb +37 -28
- data/lib/aspera/command_line_builder.rb +84 -63
- data/lib/aspera/cos_node.rb +68 -34
- data/lib/aspera/data_repository.rb +4 -2
- data/lib/aspera/environment.rb +61 -46
- data/lib/aspera/fasp/agent_base.rb +36 -31
- data/lib/aspera/fasp/agent_connect.rb +44 -37
- data/lib/aspera/fasp/agent_direct.rb +101 -104
- data/lib/aspera/fasp/agent_httpgw.rb +91 -90
- data/lib/aspera/fasp/agent_node.rb +36 -33
- data/lib/aspera/fasp/agent_trsdk.rb +28 -31
- data/lib/aspera/fasp/error.rb +3 -1
- data/lib/aspera/fasp/error_info.rb +81 -54
- data/lib/aspera/fasp/installation.rb +171 -151
- data/lib/aspera/fasp/listener.rb +2 -0
- data/lib/aspera/fasp/parameters.rb +105 -111
- data/lib/aspera/fasp/parameters.yaml +305 -249
- data/lib/aspera/fasp/resume_policy.rb +20 -20
- data/lib/aspera/fasp/transfer_spec.rb +27 -0
- data/lib/aspera/fasp/uri.rb +31 -29
- data/lib/aspera/faspex_gw.rb +95 -118
- data/lib/aspera/hash_ext.rb +12 -13
- data/lib/aspera/id_generator.rb +11 -9
- data/lib/aspera/keychain/encrypted_hash.rb +73 -57
- data/lib/aspera/keychain/macos_security.rb +27 -29
- data/lib/aspera/log.rb +40 -39
- data/lib/aspera/nagios.rb +24 -22
- data/lib/aspera/node.rb +38 -30
- data/lib/aspera/oauth.rb +217 -248
- data/lib/aspera/open_application.rb +9 -7
- data/lib/aspera/persistency_action_once.rb +15 -14
- data/lib/aspera/persistency_folder.rb +15 -18
- data/lib/aspera/preview/file_types.rb +266 -270
- data/lib/aspera/preview/generator.rb +94 -92
- data/lib/aspera/preview/image_error.png +0 -0
- data/lib/aspera/preview/options.rb +20 -17
- data/lib/aspera/preview/utils.rb +99 -102
- data/lib/aspera/preview/video_error.png +0 -0
- data/lib/aspera/{proxy_auto_config.erb.js → proxy_auto_config.js} +23 -31
- data/lib/aspera/proxy_auto_config.rb +114 -21
- data/lib/aspera/rest.rb +144 -142
- data/lib/aspera/rest_call_error.rb +3 -2
- data/lib/aspera/rest_error_analyzer.rb +31 -31
- data/lib/aspera/rest_errors_aspera.rb +18 -16
- data/lib/aspera/secret_hider.rb +68 -0
- data/lib/aspera/ssh.rb +20 -16
- data/lib/aspera/sync.rb +57 -54
- data/lib/aspera/temp_file_manager.rb +20 -14
- data/lib/aspera/timer_limiter.rb +10 -8
- data/lib/aspera/uri_reader.rb +14 -15
- data/lib/aspera/web_auth.rb +85 -80
- data.tar.gz.sig +0 -0
- metadata +169 -40
- metadata.gz.sig +2 -0
- data/bin/dascli +0 -13
- data/docs/Makefile +0 -63
- data/docs/README.erb.md +0 -4221
- data/docs/README.md +0 -13
- data/docs/diagrams.txt +0 -49
- data/docs/doc_tools.rb +0 -58
- data/lib/aspera/cli/plugins/shares2.rb +0 -114
- data/lib/aspera/fasp/default.rb +0 -17
data/lib/aspera/oauth.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'aspera/open_application'
|
2
4
|
require 'aspera/web_auth'
|
3
5
|
require 'aspera/id_generator'
|
@@ -11,203 +13,278 @@ module Aspera
|
|
11
13
|
# call get_authorization() to get a token.
|
12
14
|
# bearer tokens are kept in memory and also in a file cache for later re-use
|
13
15
|
# if a token is expired (api returns 4xx), call again get_authorization({refresh: true})
|
16
|
+
# https://tools.ietf.org/html/rfc6749
|
14
17
|
class Oauth
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
DEFAULT_CREATE_PARAMS = {
|
19
|
+
path_token: 'token', # default endpoint for /token to generate token
|
20
|
+
token_field: 'access_token', # field with token in result of call to path_token
|
21
|
+
web: {path_authorize: 'authorize'} # default endpoint for /authorize, used for code exchange
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
# OAuth methods supported by default
|
25
|
+
STD_AUTH_TYPES = %i[web jwt].freeze
|
26
|
+
|
22
27
|
# remove 5 minutes to account for time offset (TODO: configurable?)
|
23
|
-
JWT_NOTBEFORE_OFFSET_SEC=300
|
28
|
+
JWT_NOTBEFORE_OFFSET_SEC = 300
|
24
29
|
# one hour validity (TODO: configurable?)
|
25
|
-
JWT_EXPIRY_OFFSET_SEC=3600
|
30
|
+
JWT_EXPIRY_OFFSET_SEC = 3600
|
26
31
|
# tokens older than 30 minutes will be discarded from cache
|
27
|
-
TOKEN_CACHE_EXPIRY_SEC=1800
|
28
|
-
#
|
29
|
-
|
30
|
-
|
32
|
+
TOKEN_CACHE_EXPIRY_SEC = 1800
|
33
|
+
# tokens valid for less than this duration will be regenerated
|
34
|
+
TOKEN_EXPIRATION_GUARD_SEC = 120
|
35
|
+
# a prefix for persistency of tokens (simplify garbage collect)
|
36
|
+
PERSIST_CATEGORY_TOKEN = 'token'
|
31
37
|
|
32
|
-
private_constant :JWT_NOTBEFORE_OFFSET_SEC,:JWT_EXPIRY_OFFSET_SEC,:PERSIST_CATEGORY_TOKEN,:
|
33
|
-
class << self
|
34
|
-
# OAuth methods supported
|
35
|
-
def auth_types
|
36
|
-
[ :body_userpass, :header_userpass, :web, :jwt, :url_token, :ibm_apikey ]
|
37
|
-
end
|
38
|
+
private_constant :JWT_NOTBEFORE_OFFSET_SEC,:JWT_EXPIRY_OFFSET_SEC,:TOKEN_CACHE_EXPIRY_SEC,:PERSIST_CATEGORY_TOKEN,:TOKEN_EXPIRATION_GUARD_SEC
|
38
39
|
|
40
|
+
# persistency manager
|
41
|
+
@persist = nil
|
42
|
+
# token creation methods
|
43
|
+
@create_handlers = {}
|
44
|
+
# token unique identifiers from oauth parameters
|
45
|
+
@id_handlers = {}
|
46
|
+
|
47
|
+
class << self
|
39
48
|
def persist_mgr=(manager)
|
40
|
-
@persist=manager
|
49
|
+
@persist = manager
|
50
|
+
# cleanup expired tokens
|
51
|
+
@persist.garbage_collect(PERSIST_CATEGORY_TOKEN,TOKEN_CACHE_EXPIRY_SEC)
|
41
52
|
end
|
42
53
|
|
43
54
|
def persist_mgr
|
44
55
|
if @persist.nil?
|
45
|
-
Log.log.
|
56
|
+
Log.log.debug('Not using persistency') # (use Aspera::Oauth.persist_mgr=Aspera::PersistencyFolder.new)
|
46
57
|
# create NULL persistency class
|
47
|
-
@persist=Class.new do
|
48
|
-
def get(
|
58
|
+
@persist = Class.new do
|
59
|
+
def get(_x);nil;end;def delete(_x);nil;end;def put(_x,_y);nil;end;def garbage_collect(_x,_y);nil;end
|
49
60
|
end.new
|
50
61
|
end
|
51
62
|
return @persist
|
52
63
|
end
|
53
64
|
|
65
|
+
# delete all existing tokens
|
54
66
|
def flush_tokens
|
55
67
|
persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,nil)
|
56
68
|
end
|
57
69
|
|
70
|
+
# register a bearer token decoder, mainly to inspect expiry date
|
58
71
|
def register_decoder(method)
|
59
|
-
@decoders||=[]
|
72
|
+
@decoders ||= []
|
60
73
|
@decoders.push(method)
|
61
74
|
end
|
62
75
|
|
76
|
+
# decode token using all registered decoders
|
63
77
|
def decode_token(token)
|
64
|
-
Log.log.debug(">>>> #{token} : #{@decoders.length}")
|
65
78
|
@decoders.each do |decoder|
|
66
|
-
result=decoder.call(token) rescue nil
|
79
|
+
result = decoder.call(token) rescue nil
|
67
80
|
return result unless result.nil?
|
68
81
|
end
|
69
82
|
return nil
|
70
83
|
end
|
71
|
-
end
|
72
84
|
|
73
|
-
|
74
|
-
|
85
|
+
# register a token creation method
|
86
|
+
# @param id creation type from field :crtype in constructor
|
87
|
+
# @param lambda_create called to create token
|
88
|
+
# @param id_create called to generate unique id for token, for cache
|
89
|
+
def register_token_creator(id, lambda_create, id_create)
|
90
|
+
raise 'ERROR: requites Symbol and 2 lambdas' unless id.is_a?(Symbol) && lambda_create.is_a?(Proc) && id_create.is_a?(Proc)
|
91
|
+
@create_handlers[id] = lambda_create
|
92
|
+
@id_handlers[id] = id_create
|
93
|
+
end
|
75
94
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
# :client_secret
|
81
|
-
# :redirect_uri
|
82
|
-
# :jwt_audience
|
83
|
-
# :jwt_private_key_obj
|
84
|
-
# :jwt_subject
|
85
|
-
# :path_authorize (default: DEFAULT_PATH_AUTHORIZE)
|
86
|
-
# :path_token (default: DEFAULT_PATH_TOKEN)
|
87
|
-
# :scope (optional)
|
88
|
-
# :grant (one of returned by self.auth_types)
|
89
|
-
# :url_token
|
90
|
-
# :user_name
|
91
|
-
# :user_pass
|
92
|
-
# :token_type
|
93
|
-
def initialize(auth_params)
|
94
|
-
Log.log.debug("auth=#{auth_params}")
|
95
|
-
@params=auth_params.clone
|
96
|
-
# default values
|
97
|
-
# name of field to take as token from result of call to /token
|
98
|
-
@params[:token_field]||=DEFAULT_TOKEN_FIELD
|
99
|
-
# default endpoint for /token
|
100
|
-
@params[:path_token]||=DEFAULT_PATH_TOKEN
|
101
|
-
# default endpoint for /authorize
|
102
|
-
@params[:path_authorize]||=DEFAULT_PATH_AUTHORIZE
|
103
|
-
rest_params={base_url: @params[:base_url]}
|
104
|
-
if @params.has_key?(:client_id)
|
105
|
-
rest_params.merge!({auth: {
|
106
|
-
type: :basic,
|
107
|
-
username: @params[:client_id],
|
108
|
-
password: @params[:client_secret]
|
109
|
-
}})
|
95
|
+
# @return one of the registered creators for the given create type
|
96
|
+
def token_creator(id)
|
97
|
+
raise "token creator type unknown: #{id}/#{id.class}" unless @create_handlers.has_key?(id)
|
98
|
+
@create_handlers[id]
|
110
99
|
end
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
raise "
|
115
|
-
|
116
|
-
# we could check that host is localhost or local address
|
100
|
+
|
101
|
+
# list of identifiers foundn in creation parameters that can be used to uniquely identify the token
|
102
|
+
def id_creator(id)
|
103
|
+
raise "id creator type unknown: #{id}/#{id.class}" unless @id_handlers.has_key?(id)
|
104
|
+
@id_handlers[id]
|
117
105
|
end
|
118
|
-
|
119
|
-
self.class.persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,TOKEN_CACHE_EXPIRY_SEC)
|
120
|
-
end
|
106
|
+
end # self
|
121
107
|
|
122
|
-
#
|
123
|
-
|
108
|
+
# JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
|
109
|
+
register_decoder lambda { |token| parts = token.split('.'); raise 'not aoc token' unless parts.length.eql?(3); JSON.parse(Base64.decode64(parts[1]))}
|
110
|
+
|
111
|
+
# generic token creation, parameters are provided in :generic
|
112
|
+
register_token_creator :generic,lambda { |oauth|
|
113
|
+
return oauth.create_token(oauth.sparams)
|
114
|
+
},lambda { |oauth|
|
115
|
+
return [
|
116
|
+
oauth.sparams[:grant_type]&.split(':')&.last,
|
117
|
+
oauth.sparams[:apikey],
|
118
|
+
oauth.sparams[:response_type]
|
119
|
+
]
|
120
|
+
}
|
121
|
+
|
122
|
+
# Authentication using Web browser
|
123
|
+
register_token_creator :web,lambda { |oauth|
|
124
|
+
verification_code = SecureRandom.uuid # used to check later
|
125
|
+
login_page_url = Rest.build_uri("#{oauth.api[:base_url]}/#{oauth.sparams[:path_authorize]}",
|
126
|
+
oauth.optional_scope_client_id.merge(response_type: 'code', redirect_uri: oauth.sparams[:redirect_uri], state: verification_code))
|
127
|
+
# here, we need a human to authorize on a web page
|
124
128
|
Log.log.info("login_page_url=#{login_page_url}".bg_red.gray)
|
125
129
|
# start a web server to receive request code
|
126
|
-
webserver=WebAuth.new(
|
130
|
+
webserver = WebAuth.new(oauth.sparams[:redirect_uri])
|
127
131
|
# start browser on login page
|
128
132
|
OpenApplication.instance.uri(login_page_url)
|
129
133
|
# wait for code in request
|
130
|
-
|
131
|
-
|
132
|
-
code
|
133
|
-
return
|
134
|
-
|
134
|
+
received_params = webserver.received_request
|
135
|
+
raise 'state does not match' unless verification_code.eql?(received_params['state'])
|
136
|
+
# exchange code for token
|
137
|
+
return oauth.create_token(oauth.optional_scope_client_id(add_secret: true).merge(
|
138
|
+
grant_type: 'authorization_code',
|
139
|
+
code: received_params['code'],
|
140
|
+
redirect_uri: oauth.sparams[:redirect_uri]))
|
141
|
+
},lambda { |_oauth|
|
142
|
+
return []
|
143
|
+
}
|
135
144
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
145
|
+
# Authentication using private key
|
146
|
+
register_token_creator :jwt,lambda { |oauth|
|
147
|
+
# https://tools.ietf.org/html/rfc7523
|
148
|
+
# https://tools.ietf.org/html/rfc7519
|
149
|
+
require 'jwt'
|
150
|
+
seconds_since_epoch = Time.new.to_i
|
151
|
+
Log.log.info("seconds=#{seconds_since_epoch}")
|
152
|
+
raise 'missing JWT payload' unless oauth.sparams[:payload].is_a?(Hash)
|
153
|
+
jwt_payload = {
|
154
|
+
exp: seconds_since_epoch + JWT_EXPIRY_OFFSET_SEC, # expiration time
|
155
|
+
nbf: seconds_since_epoch - JWT_NOTBEFORE_OFFSET_SEC, # not before
|
156
|
+
iat: seconds_since_epoch, # issued at
|
157
|
+
jti: SecureRandom.uuid # JWT id
|
158
|
+
}.merge(oauth.sparams[:payload])
|
159
|
+
Log.log.debug("JWT jwt_payload=[#{jwt_payload}]")
|
160
|
+
rsa_private = oauth.sparams[:private_key_obj] # type: OpenSSL::PKey::RSA
|
161
|
+
Log.log.debug("private=[#{rsa_private}]")
|
162
|
+
assertion = JWT.encode(jwt_payload, rsa_private, 'RS256', oauth.sparams[:headers] || {})
|
163
|
+
Log.log.debug("assertion=[#{assertion}]")
|
164
|
+
return oauth.create_token(oauth.optional_scope_client_id.merge(grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: assertion))
|
165
|
+
},lambda { |oauth|
|
166
|
+
return [oauth.sparams.dig(:payload,:sub)]
|
167
|
+
}
|
168
|
+
|
169
|
+
attr_reader :gparams, :sparams, :api
|
170
|
+
|
171
|
+
private
|
142
172
|
|
143
|
-
#
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
173
|
+
# [M]=mandatory [D]=has default value [0]=accept nil
|
174
|
+
# :base_url [M] URL of authentication API
|
175
|
+
# :auth
|
176
|
+
# :crtype [M] :generic, :web, :jwt, custom
|
177
|
+
# :client_id [0]
|
178
|
+
# :client_secret [0]
|
179
|
+
# :scope [0]
|
180
|
+
# :path_token [D] API end point to create a token
|
181
|
+
# :token_field [D] field in result that contains the token
|
182
|
+
# :jwt:private_key_obj [M] for type :jwt
|
183
|
+
# :jwt:payload [M] for type :jwt
|
184
|
+
# :jwt:headers [0] for type :jwt
|
185
|
+
# :web:redirect_uri [M] for type :web
|
186
|
+
# :web:path_authorize [D] for type :web
|
187
|
+
# :generic [M] for type :generic
|
188
|
+
def initialize(a_params)
|
189
|
+
Log.log.debug("auth=#{a_params}")
|
190
|
+
# replace default values
|
191
|
+
@gparams = DEFAULT_CREATE_PARAMS.deep_merge(a_params)
|
192
|
+
# check that type is known
|
193
|
+
self.class.token_creator(@gparams[:crtype])
|
194
|
+
# specific parameters for the creation type
|
195
|
+
@sparams=@gparams[@gparams[:crtype]]
|
196
|
+
if @gparams[:crtype].eql?(:web) && @sparams.has_key?(:redirect_uri)
|
197
|
+
uri = URI.parse(@sparams[:redirect_uri])
|
198
|
+
raise 'redirect_uri scheme must be http or https' unless %w[http https].include?(uri.scheme)
|
199
|
+
raise 'redirect_uri must have a port' if uri.port.nil?
|
200
|
+
# TODO: we could check that host is localhost or local address
|
201
|
+
end
|
202
|
+
rest_params = {
|
203
|
+
base_url: @gparams[:base_url],
|
204
|
+
redirect_max: 2
|
205
|
+
}
|
206
|
+
rest_params[:auth] = a_params[:auth] if a_params.has_key?(:auth)
|
207
|
+
@api = Rest.new(rest_params)
|
208
|
+
# if needed use from api
|
209
|
+
@gparams.delete(:base_url)
|
210
|
+
@gparams.delete(:auth)
|
211
|
+
@gparams.delete(@gparams[:crtype])
|
212
|
+
Log.dump(:gparams,@gparams)
|
213
|
+
Log.dump(:sparams,@sparams)
|
150
214
|
end
|
151
215
|
|
152
216
|
public
|
153
217
|
|
154
|
-
#
|
155
|
-
|
218
|
+
# helper method to create token as per RFC
|
219
|
+
def create_token(www_params)
|
220
|
+
return @api.call({
|
221
|
+
operation: 'POST',
|
222
|
+
subpath: @gparams[:path_token],
|
223
|
+
headers: {'Accept' => 'application/json'},
|
224
|
+
www_body_params: www_params})
|
225
|
+
end
|
156
226
|
|
157
|
-
# @
|
158
|
-
def
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
p_client_id_and_scope[:client_id] = @params[:client_id] if @params.has_key?(:client_id)
|
166
|
-
use_refresh_token=options[:refresh]
|
227
|
+
# @return Hash with optional general parameters
|
228
|
+
def optional_scope_client_id(add_secret: false)
|
229
|
+
call_params={}
|
230
|
+
call_params[:scope] = @gparams[:scope] unless @gparams[:scope].nil?
|
231
|
+
call_params[:client_id] = @gparams[:client_id] unless @gparams[:client_id].nil?
|
232
|
+
call_params[:client_secret] = @gparams[:client_secret] if add_secret && !@gparams[:client_id].nil?
|
233
|
+
return call_params
|
234
|
+
end
|
167
235
|
|
168
|
-
|
169
|
-
|
236
|
+
# Oauth v2 token generation
|
237
|
+
# @param use_refresh_token set to true to force refresh or re-generation (if previous failed)
|
238
|
+
def get_authorization(use_refresh_token: false, use_cache: true)
|
239
|
+
# generate token unique identifier for persistency (memory/disk cache)
|
240
|
+
token_id = IdGenerator.from_list([
|
241
|
+
PERSIST_CATEGORY_TOKEN,
|
242
|
+
@api.params[:base_url],
|
243
|
+
@gparams[:crtype],
|
244
|
+
self.class.id_creator(@gparams[:crtype]).call(self), # array, so we flatten later
|
245
|
+
@gparams[:scope],
|
246
|
+
@api.params.dig(%i[auth username])
|
247
|
+
].flatten)
|
170
248
|
|
171
249
|
# get token_data from cache (or nil), token_data is what is returned by /token
|
172
|
-
token_data=self.class.persist_mgr.get(token_id)
|
173
|
-
token_data=JSON.parse(token_data) unless token_data.nil?
|
174
|
-
# Optional optimization: check if node token is expired
|
175
|
-
# in case the transfer agent cannot refresh himself
|
176
|
-
#
|
177
|
-
if !token_data.nil?
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
250
|
+
token_data = self.class.persist_mgr.get(token_id) if use_cache
|
251
|
+
token_data = JSON.parse(token_data) unless token_data.nil?
|
252
|
+
# Optional optimization: check if node token is expired basd on decoded content then force refresh if close enough
|
253
|
+
# might help in case the transfer agent cannot refresh himself
|
254
|
+
# `direct` agent is equipped with refresh code
|
255
|
+
if !use_refresh_token && !token_data.nil?
|
256
|
+
decoded_token = self.class.decode_token(token_data[@gparams[:token_field]])
|
257
|
+
Log.dump('decoded_token',decoded_token) unless decoded_token.nil?
|
258
|
+
if decoded_token.is_a?(Hash)
|
259
|
+
expires_at_sec =
|
260
|
+
if decoded_token['expires_at'].is_a?(String) then DateTime.parse(decoded_token['expires_at']).to_time
|
261
|
+
elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
|
262
|
+
end
|
263
|
+
# force refresh if we see a token too close from expiration
|
264
|
+
use_refresh_token = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < TOKEN_EXPIRATION_GUARD_SEC
|
265
|
+
Log.log.debug("Expiration: #{expires_at_sec} / #{use_refresh_token}")
|
186
266
|
end
|
187
267
|
end
|
188
268
|
|
189
269
|
# an API was already called, but failed, we need to regenerate or refresh
|
190
270
|
if use_refresh_token
|
191
|
-
if token_data.is_a?(Hash)
|
271
|
+
if token_data.is_a?(Hash) && token_data.has_key?('refresh_token')
|
192
272
|
# save possible refresh token, before deleting the cache
|
193
|
-
refresh_token=token_data['refresh_token']
|
273
|
+
refresh_token = token_data['refresh_token']
|
194
274
|
end
|
195
|
-
# delete
|
275
|
+
# delete cache
|
196
276
|
self.class.persist_mgr.delete(token_id)
|
197
|
-
token_data=nil
|
277
|
+
token_data = nil
|
198
278
|
# lets try the existing refresh token
|
199
279
|
if !refresh_token.nil?
|
200
280
|
Log.log.info("refresh=[#{refresh_token}]".bg_green)
|
201
281
|
# try to refresh
|
202
|
-
# note: admin token has no refresh, and lives by default 1800secs
|
203
|
-
|
204
|
-
resp
|
205
|
-
grant_type: 'refresh_token',
|
206
|
-
refresh_token: refresh_token}))
|
207
|
-
if resp[:http].code.start_with?('2') then
|
282
|
+
# note: AoC admin token has no refresh, and lives by default 1800secs
|
283
|
+
resp = create_token(optional_scope_client_id.merge(grant_type: 'refresh_token',refresh_token: refresh_token))
|
284
|
+
if resp[:http].code.start_with?('2')
|
208
285
|
# save only if success
|
209
|
-
json_data=resp[:http].body
|
210
|
-
token_data=JSON.parse(json_data)
|
286
|
+
json_data = resp[:http].body
|
287
|
+
token_data = JSON.parse(json_data)
|
211
288
|
self.class.persist_mgr.put(token_id,json_data)
|
212
289
|
else
|
213
290
|
Log.log.debug("refresh failed: #{resp[:http].body}".bg_red)
|
@@ -215,124 +292,16 @@ module Aspera
|
|
215
292
|
end
|
216
293
|
end
|
217
294
|
|
218
|
-
# no cache
|
219
|
-
if token_data.nil?
|
220
|
-
resp=
|
221
|
-
|
222
|
-
|
223
|
-
# AoC Web based Auth
|
224
|
-
check_code=SecureRandom.uuid
|
225
|
-
auth_params=p_client_id_and_scope.merge({
|
226
|
-
response_type: 'code',
|
227
|
-
redirect_uri: @params[:redirect_uri],
|
228
|
-
state: check_code
|
229
|
-
})
|
230
|
-
auth_params[:client_secret]=@params[:client_secret] if @params.has_key?(:client_secret)
|
231
|
-
login_page_url=Rest.build_uri("#{@params[:base_url]}/#{@params[:path_authorize]}",auth_params)
|
232
|
-
# here, we need a human to authorize on a web page
|
233
|
-
code=goto_page_and_get_code(login_page_url,check_code)
|
234
|
-
# exchange code for token
|
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]
|
239
|
-
}))
|
240
|
-
when :jwt
|
241
|
-
# https://tools.ietf.org/html/rfc7519
|
242
|
-
# https://tools.ietf.org/html/rfc7523
|
243
|
-
require 'jwt'
|
244
|
-
seconds_since_epoch=Time.new.to_i
|
245
|
-
Log.log.info("seconds=#{seconds_since_epoch}")
|
246
|
-
|
247
|
-
payload = {
|
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
|
253
|
-
}
|
254
|
-
# Hum.. compliant ? TODO: remove when Faspex5 API is clarified
|
255
|
-
if @params.has_key?(:f5_username)
|
256
|
-
payload[:jti] = SecureRandom.uuid # JWT id
|
257
|
-
payload[:iat] = seconds_since_epoch # issued at
|
258
|
-
payload.delete(:nbf) # not used in f5
|
259
|
-
p_scope[:redirect_uri]="https://127.0.0.1:5000/token" # used ?
|
260
|
-
p_scope[:state]=SecureRandom.uuid
|
261
|
-
p_scope[:client_id]=@params[:client_id]
|
262
|
-
@token_auth_api.params[:auth]={type: :basic,username: @params[:f5_username], password: @params[:f5_password]}
|
263
|
-
end
|
264
|
-
|
265
|
-
# non standard, only for global ids
|
266
|
-
payload.merge!(@params[:jwt_add]) if @params.has_key?(:jwt_add)
|
267
|
-
Log.log.debug("JWT payload=[#{payload}]")
|
268
|
-
|
269
|
-
rsa_private=@params[:jwt_private_key_obj] # type: OpenSSL::PKey::RSA
|
270
|
-
Log.log.debug("private=[#{rsa_private}]")
|
271
|
-
|
272
|
-
assertion = JWT.encode(payload, rsa_private, 'RS256', @params[:jwt_headers]||{})
|
273
|
-
Log.log.debug("assertion=[#{assertion}]")
|
274
|
-
|
275
|
-
resp=create_token(www_body_params: p_scope.merge({
|
276
|
-
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
277
|
-
assertion: assertion
|
278
|
-
}))
|
279
|
-
when :url_token
|
280
|
-
# AoC Public Link
|
281
|
-
params={url_token: @params[:url_token]}
|
282
|
-
params[:password]=@params[:password] if @params.has_key?(:password)
|
283
|
-
resp=create_token({
|
284
|
-
json_params: params,
|
285
|
-
url_params: p_scope.merge({grant_type: 'url_token'})
|
286
|
-
})
|
287
|
-
when :ibm_apikey
|
288
|
-
# ATS
|
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]
|
293
|
-
})
|
294
|
-
when :delegated_refresh
|
295
|
-
# COS
|
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'
|
301
|
-
})
|
302
|
-
when :header_userpass
|
303
|
-
# used in Faspex apiv4 and shares2
|
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
|
-
)
|
311
|
-
when :body_userpass
|
312
|
-
# legacy, not used
|
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]
|
317
|
-
}))
|
318
|
-
when :body_data
|
319
|
-
# used in Faspex apiv5
|
320
|
-
resp=create_token({
|
321
|
-
auth: {type: :none},
|
322
|
-
json_params: @params[:userpass_body],
|
323
|
-
})
|
324
|
-
else
|
325
|
-
raise "auth grant type unknown: #{@params[:grant]}"
|
326
|
-
end
|
327
|
-
# TODO: test return code ?
|
328
|
-
json_data=resp[:http].body
|
329
|
-
token_data=JSON.parse(json_data)
|
295
|
+
# no cache, nor refresh: generate a token
|
296
|
+
if token_data.nil?
|
297
|
+
resp = self.class.token_creator(@gparams[:crtype]).call(self)
|
298
|
+
json_data = resp[:http].body
|
299
|
+
token_data = JSON.parse(json_data)
|
330
300
|
self.class.persist_mgr.put(token_id,json_data)
|
331
301
|
end # if ! in_cache
|
332
|
-
raise "API error: No such field in answer: #{@
|
302
|
+
raise "API error: No such field in answer: #{@gparams[:token_field]}" unless token_data.has_key?(@gparams[:token_field])
|
333
303
|
# ok we shall have a token here
|
334
|
-
return 'Bearer '+token_data[@
|
304
|
+
return 'Bearer ' + token_data[@gparams[:token_field]]
|
335
305
|
end
|
336
|
-
|
337
306
|
end # OAuth
|
338
307
|
end # Aspera
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'aspera/log'
|
2
4
|
require 'aspera/environment'
|
3
5
|
require 'rbconfig'
|
@@ -10,12 +12,12 @@ module Aspera
|
|
10
12
|
class OpenApplication
|
11
13
|
include Singleton
|
12
14
|
# User Interfaces
|
13
|
-
def self.user_interfaces; [
|
15
|
+
def self.user_interfaces; %i[text graphical]; end
|
14
16
|
|
15
17
|
def self.default_gui_mode
|
16
18
|
return :graphical if [Aspera::Environment::OS_WINDOWS,Aspera::Environment::OS_X].include?(Aspera::Environment.os)
|
17
19
|
# unix family
|
18
|
-
return :graphical if ENV.has_key?(
|
20
|
+
return :graphical if ENV.has_key?('DISPLAY') && !ENV['DISPLAY'].empty?
|
19
21
|
return :text
|
20
22
|
end
|
21
23
|
|
@@ -25,9 +27,9 @@ module Aspera
|
|
25
27
|
when Aspera::Environment::OS_X
|
26
28
|
return system('open',uri.to_s)
|
27
29
|
when Aspera::Environment::OS_WINDOWS
|
28
|
-
return system('start explorer "'+uri.to_s+'"')
|
30
|
+
return system('start explorer "' + uri.to_s + '"')
|
29
31
|
when Aspera::Environment::OS_LINUX
|
30
|
-
return system("xdg-open '#{uri
|
32
|
+
return system("xdg-open '#{uri}'")
|
31
33
|
else
|
32
34
|
raise "no graphical open method for #{Aspera::Environment.os}"
|
33
35
|
end
|
@@ -36,7 +38,7 @@ module Aspera
|
|
36
38
|
attr_accessor :url_method
|
37
39
|
|
38
40
|
def initialize
|
39
|
-
@url_method=self.class.default_gui_mode
|
41
|
+
@url_method = self.class.default_gui_mode
|
40
42
|
end
|
41
43
|
|
42
44
|
# this is non blocking
|
@@ -47,9 +49,9 @@ module Aspera
|
|
47
49
|
when :text
|
48
50
|
case the_url.to_s
|
49
51
|
when /^http/
|
50
|
-
puts "USER ACTION: please enter this url in a browser:\n"+the_url.to_s.red
|
52
|
+
puts "USER ACTION: please enter this url in a browser:\n" + the_url.to_s.red + "\n"
|
51
53
|
else
|
52
|
-
puts "USER ACTION: open this:\n"+the_url.to_s.red
|
54
|
+
puts "USER ACTION: open this:\n" + the_url.to_s.red + "\n"
|
53
55
|
end
|
54
56
|
else
|
55
57
|
raise StandardError,"unsupported url open method: #{@url_method}"
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require 'aspera/log'
|
3
5
|
|
@@ -13,20 +15,20 @@ module Aspera
|
|
13
15
|
# @param :merge Optional merge data from file to current data
|
14
16
|
def initialize(options)
|
15
17
|
Log.log.debug("persistency: #{options}")
|
16
|
-
raise
|
17
|
-
raise
|
18
|
-
raise
|
19
|
-
raise
|
20
|
-
raise
|
21
|
-
@manager=options[:manager]
|
22
|
-
@persisted_object=options[:data]
|
23
|
-
@object_id=options[:id]
|
18
|
+
raise 'options shall be Hash' unless options.is_a?(Hash)
|
19
|
+
raise 'mandatory :manager' if options[:manager].nil?
|
20
|
+
raise 'mandatory :data' if options[:data].nil?
|
21
|
+
raise 'mandatory :id (String)' unless options[:id].is_a?(String)
|
22
|
+
raise 'mandatory 1 element in :id' unless options[:id].length >= 1
|
23
|
+
@manager = options[:manager]
|
24
|
+
@persisted_object = options[:data]
|
25
|
+
@object_id = options[:id]
|
24
26
|
# by default , at save time, file is deleted if data is nil
|
25
|
-
@delete_condition=options[:delete] || lambda{|d|d.empty?}
|
26
|
-
@persist_format=options[:format] || lambda {|h| JSON.generate(h)}
|
27
|
-
persist_parse=options[:parse] || lambda {|t| JSON.parse(t)}
|
28
|
-
persist_merge=options[:merge] || lambda {|current,file| current.concat(file).uniq rescue current}
|
29
|
-
value
|
27
|
+
@delete_condition = options[:delete] || lambda{|d|d.empty?}
|
28
|
+
@persist_format = options[:format] || lambda {|h| JSON.generate(h)}
|
29
|
+
persist_parse = options[:parse] || lambda {|t| JSON.parse(t)}
|
30
|
+
persist_merge = options[:merge] || lambda {|current,file| current.concat(file).uniq rescue current}
|
31
|
+
value = @manager.get(@object_id)
|
30
32
|
persist_merge.call(@persisted_object,persist_parse.call(value)) unless value.nil?
|
31
33
|
end
|
32
34
|
|
@@ -41,6 +43,5 @@ module Aspera
|
|
41
43
|
def data
|
42
44
|
return @persisted_object
|
43
45
|
end
|
44
|
-
|
45
46
|
end
|
46
47
|
end
|