aspera-cli 4.6.0 → 4.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +427 -300
- data/bin/ascli +2 -1
- data/bin/asession +1 -0
- data/docs/test_env.conf +2 -0
- data/examples/aoc.rb +4 -3
- data/examples/faspex4.rb +21 -19
- data/examples/proxy.pac +1 -1
- data/examples/transfer.rb +15 -15
- data/lib/aspera/aoc.rb +135 -124
- data/lib/aspera/ascmd.rb +85 -75
- data/lib/aspera/ats_api.rb +11 -10
- data/lib/aspera/cli/basic_auth_plugin.rb +13 -14
- data/lib/aspera/cli/extended_value.rb +42 -33
- data/lib/aspera/cli/formater.rb +138 -111
- data/lib/aspera/cli/info.rb +17 -0
- data/lib/aspera/cli/listener/line_dump.rb +3 -2
- data/lib/aspera/cli/listener/logger.rb +2 -1
- data/lib/aspera/cli/listener/progress.rb +16 -18
- data/lib/aspera/cli/listener/progress_multi.rb +13 -16
- data/lib/aspera/cli/main.rb +122 -130
- data/lib/aspera/cli/manager.rb +146 -154
- data/lib/aspera/cli/plugin.rb +38 -34
- data/lib/aspera/cli/plugins/alee.rb +6 -6
- data/lib/aspera/cli/plugins/aoc.rb +273 -276
- data/lib/aspera/cli/plugins/ats.rb +82 -76
- data/lib/aspera/cli/plugins/bss.rb +14 -16
- data/lib/aspera/cli/plugins/config.rb +350 -306
- data/lib/aspera/cli/plugins/console.rb +23 -19
- data/lib/aspera/cli/plugins/cos.rb +18 -18
- data/lib/aspera/cli/plugins/faspex.rb +180 -159
- data/lib/aspera/cli/plugins/faspex5.rb +64 -54
- data/lib/aspera/cli/plugins/node.rb +147 -140
- data/lib/aspera/cli/plugins/orchestrator.rb +68 -66
- data/lib/aspera/cli/plugins/preview.rb +92 -96
- data/lib/aspera/cli/plugins/server.rb +79 -75
- data/lib/aspera/cli/plugins/shares.rb +23 -24
- data/lib/aspera/cli/plugins/sync.rb +20 -22
- data/lib/aspera/cli/transfer_agent.rb +40 -39
- data/lib/aspera/cli/version.rb +2 -1
- data/lib/aspera/colors.rb +35 -27
- data/lib/aspera/command_line_builder.rb +48 -34
- data/lib/aspera/cos_node.rb +29 -21
- data/lib/aspera/data_repository.rb +3 -2
- data/lib/aspera/environment.rb +50 -45
- data/lib/aspera/fasp/agent_base.rb +22 -20
- data/lib/aspera/fasp/agent_connect.rb +13 -11
- data/lib/aspera/fasp/agent_direct.rb +48 -59
- data/lib/aspera/fasp/agent_httpgw.rb +33 -39
- data/lib/aspera/fasp/agent_node.rb +15 -13
- data/lib/aspera/fasp/agent_trsdk.rb +12 -14
- data/lib/aspera/fasp/error.rb +2 -1
- data/lib/aspera/fasp/error_info.rb +68 -52
- data/lib/aspera/fasp/installation.rb +106 -94
- data/lib/aspera/fasp/listener.rb +1 -0
- data/lib/aspera/fasp/parameters.rb +83 -92
- data/lib/aspera/fasp/parameters.yaml +305 -249
- data/lib/aspera/fasp/resume_policy.rb +11 -14
- data/lib/aspera/fasp/transfer_spec.rb +26 -0
- data/lib/aspera/fasp/uri.rb +22 -21
- data/lib/aspera/faspex_gw.rb +55 -90
- data/lib/aspera/hash_ext.rb +4 -3
- data/lib/aspera/id_generator.rb +8 -7
- data/lib/aspera/keychain/encrypted_hash.rb +17 -16
- data/lib/aspera/keychain/macos_security.rb +6 -10
- data/lib/aspera/log.rb +25 -20
- data/lib/aspera/nagios.rb +13 -12
- data/lib/aspera/node.rb +30 -22
- data/lib/aspera/oauth.rb +175 -226
- data/lib/aspera/open_application.rb +4 -3
- data/lib/aspera/persistency_action_once.rb +6 -6
- data/lib/aspera/persistency_folder.rb +5 -9
- data/lib/aspera/preview/file_types.rb +6 -5
- data/lib/aspera/preview/generator.rb +25 -24
- data/lib/aspera/preview/options.rb +16 -14
- data/lib/aspera/preview/utils.rb +98 -98
- data/lib/aspera/{proxy_auto_config.erb.js → proxy_auto_config.js} +23 -31
- data/lib/aspera/proxy_auto_config.rb +111 -20
- data/lib/aspera/rest.rb +115 -113
- data/lib/aspera/rest_call_error.rb +2 -2
- data/lib/aspera/rest_error_analyzer.rb +23 -25
- data/lib/aspera/rest_errors_aspera.rb +15 -14
- data/lib/aspera/ssh.rb +12 -10
- data/lib/aspera/sync.rb +42 -41
- data/lib/aspera/temp_file_manager.rb +18 -14
- data/lib/aspera/timer_limiter.rb +2 -1
- data/lib/aspera/uri_reader.rb +7 -5
- data/lib/aspera/web_auth.rb +79 -76
- metadata +64 -21
- data/docs/Makefile +0 -65
- data/docs/README.erb.md +0 -4424
- 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,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'aspera/open_application'
|
2
3
|
require 'aspera/web_auth'
|
3
4
|
require 'aspera/id_generator'
|
@@ -11,14 +12,17 @@ module Aspera
|
|
11
12
|
# call get_authorization() to get a token.
|
12
13
|
# bearer tokens are kept in memory and also in a file cache for later re-use
|
13
14
|
# if a token is expired (api returns 4xx), call again get_authorization({refresh: true})
|
15
|
+
# https://tools.ietf.org/html/rfc6749
|
14
16
|
class Oauth
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
DEFAULT_CREATE_PARAMS={
|
18
|
+
token_field: 'access_token', # field with token in result
|
19
|
+
path_token: 'token', # default endpoint for /token to generate token
|
20
|
+
web: {path_authorize: 'authorize'} # default endpoint for /authorize, used for code exchange
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
# OAuth methods supported by default
|
24
|
+
STD_AUTH_TYPES=[:web, :jwt].freeze
|
25
|
+
|
22
26
|
# remove 5 minutes to account for time offset (TODO: configurable?)
|
23
27
|
JWT_NOTBEFORE_OFFSET_SEC=300
|
24
28
|
# one hour validity (TODO: configurable?)
|
@@ -27,184 +31,237 @@ module Aspera
|
|
27
31
|
TOKEN_CACHE_EXPIRY_SEC=1800
|
28
32
|
# a prefix for persistency of tokens (garbage collect)
|
29
33
|
PERSIST_CATEGORY_TOKEN='token'
|
30
|
-
|
34
|
+
TOKEN_EXPIRATION_GUARD_SEC=120
|
31
35
|
|
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
|
36
|
+
private_constant :JWT_NOTBEFORE_OFFSET_SEC,:JWT_EXPIRY_OFFSET_SEC,:TOKEN_CACHE_EXPIRY_SEC,:PERSIST_CATEGORY_TOKEN,:TOKEN_EXPIRATION_GUARD_SEC
|
38
37
|
|
38
|
+
# persistency manager
|
39
|
+
@persist=nil
|
40
|
+
# token creation methods
|
41
|
+
@handlers={}
|
42
|
+
# token unique identifiers from oauth parameters
|
43
|
+
@id_elements=[
|
44
|
+
[:crtype],
|
45
|
+
[:generic,:grant_type],
|
46
|
+
[:jwt,:payload,:sub],
|
47
|
+
[:auth,:username],
|
48
|
+
[:aoc_pub_link,:json,:url_token],
|
49
|
+
[:generic,:apikey],
|
50
|
+
[:scope],
|
51
|
+
[:generic,:response_type]
|
52
|
+
]
|
53
|
+
|
54
|
+
class << self
|
39
55
|
def persist_mgr=(manager)
|
40
56
|
@persist=manager
|
57
|
+
# cleanup expired tokens
|
58
|
+
@persist.garbage_collect(PERSIST_CATEGORY_TOKEN,TOKEN_CACHE_EXPIRY_SEC)
|
41
59
|
end
|
42
60
|
|
43
61
|
def persist_mgr
|
44
62
|
if @persist.nil?
|
45
|
-
Log.log.
|
63
|
+
Log.log.debug('Not using persistency') # (use Aspera::Oauth.persist_mgr=Aspera::PersistencyFolder.new)
|
46
64
|
# create NULL persistency class
|
47
65
|
@persist=Class.new do
|
48
|
-
def get(
|
66
|
+
def get(_x);nil;end;def delete(_x);nil;end;def put(_x,_y);nil;end;def garbage_collect(_x,_y);nil;end # rubocop:disable Layout/EmptyLineBetweenDefs
|
49
67
|
end.new
|
50
68
|
end
|
51
69
|
return @persist
|
52
70
|
end
|
53
71
|
|
72
|
+
# delete all existing tokens
|
54
73
|
def flush_tokens
|
55
74
|
persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN,nil)
|
56
75
|
end
|
57
76
|
|
77
|
+
# register a bearer token decoder, mainly to inspect expiry date
|
58
78
|
def register_decoder(method)
|
59
79
|
@decoders||=[]
|
60
80
|
@decoders.push(method)
|
61
81
|
end
|
62
82
|
|
83
|
+
# decode token using all registered decoders
|
63
84
|
def decode_token(token)
|
64
|
-
Log.log.debug(">>>> #{token} : #{@decoders.length}")
|
65
85
|
@decoders.each do |decoder|
|
66
86
|
result=decoder.call(token) rescue nil
|
67
87
|
return result unless result.nil?
|
68
88
|
end
|
69
89
|
return nil
|
70
90
|
end
|
71
|
-
end
|
72
91
|
|
73
|
-
|
74
|
-
|
92
|
+
# register a token creation method, specify using field :crtype in constructor
|
93
|
+
def register_token_creator(id, method)
|
94
|
+
raise 'error' unless id.is_a?(Symbol) && method.is_a?(Proc)
|
95
|
+
@handlers[id]=method
|
96
|
+
end
|
75
97
|
|
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
|
-
}})
|
98
|
+
# @return one of the registered creators for the given create type
|
99
|
+
def token_creator(id)
|
100
|
+
raise "token create type unknown: #{id}/#{id.class}" unless @handlers.has_key?(id)
|
101
|
+
@handlers[id]
|
110
102
|
end
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
raise "redirect_uri must have a port" if uri.port.nil?
|
116
|
-
# we could check that host is localhost or local address
|
103
|
+
|
104
|
+
# list of identifiers foundn in creation parameters that can be used to uniquely identify the token
|
105
|
+
def id_elements
|
106
|
+
return @id_elements
|
117
107
|
end
|
118
|
-
|
119
|
-
|
120
|
-
|
108
|
+
end # self
|
109
|
+
|
110
|
+
# JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
|
111
|
+
register_decoder lambda { |token| parts=token.split('.'); raise 'not aoc token' unless parts.length.eql?(3); JSON.parse(Base64.decode64(parts[1]))}
|
121
112
|
|
122
|
-
#
|
123
|
-
|
113
|
+
# generic token creation, parameters are provided in :generic
|
114
|
+
register_token_creator :generic,lambda { |oauth|
|
115
|
+
return oauth.create_token(oauth.params[:generic])
|
116
|
+
}
|
117
|
+
|
118
|
+
# Authentication using Web browser
|
119
|
+
register_token_creator :web,lambda { |oauth|
|
120
|
+
callback_verif=SecureRandom.uuid # used to check later
|
121
|
+
login_page_url=Rest.build_uri("#{oauth.params[:base_url]}/#{oauth.params[:web][:path_authorize]}",optional_scope_client_id({response_type: 'code', redirect_uri: oauth.params[:web][:redirect_uri], state: callback_verif}))
|
122
|
+
# here, we need a human to authorize on a web page
|
124
123
|
Log.log.info("login_page_url=#{login_page_url}".bg_red.gray)
|
125
124
|
# start a web server to receive request code
|
126
|
-
webserver=WebAuth.new(
|
125
|
+
webserver=WebAuth.new(oauth.params[:web][:redirect_uri])
|
127
126
|
# start browser on login page
|
128
127
|
OpenApplication.instance.uri(login_page_url)
|
129
128
|
# wait for code in request
|
130
|
-
|
131
|
-
|
132
|
-
code
|
133
|
-
return code
|
134
|
-
|
129
|
+
received_params=webserver.received_request
|
130
|
+
raise 'state does not match' if !callback_verif.eql?(received_params['state'])
|
131
|
+
# exchange code for token
|
132
|
+
return oauth.create_token(oauth.optional_scope_client_id({grant_type: 'authorization_code', code: received_params['code'], redirect_uri: oauth.params[:web][:redirect_uri]},add_secret: true))
|
133
|
+
}
|
135
134
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
135
|
+
# Authentication using private key
|
136
|
+
register_token_creator :jwt,lambda { |oauth|
|
137
|
+
# https://tools.ietf.org/html/rfc7523
|
138
|
+
# https://tools.ietf.org/html/rfc7519
|
139
|
+
require 'jwt'
|
140
|
+
seconds_since_epoch=Time.new.to_i
|
141
|
+
Log.log.info("seconds=#{seconds_since_epoch}")
|
142
|
+
raise 'missing JWT payload' unless oauth.params[:jwt][:payload].is_a?(Hash)
|
143
|
+
jwt_payload = {
|
144
|
+
exp: seconds_since_epoch+JWT_EXPIRY_OFFSET_SEC, # expiration time
|
145
|
+
nbf: seconds_since_epoch-JWT_NOTBEFORE_OFFSET_SEC, # not before
|
146
|
+
iat: seconds_since_epoch, # issued at
|
147
|
+
jti: SecureRandom.uuid # JWT id
|
148
|
+
}.merge(oauth.params[:jwt][:payload])
|
149
|
+
Log.log.debug("JWT jwt_payload=[#{jwt_payload}]")
|
150
|
+
rsa_private=oauth.params[:jwt][:private_key_obj] # type: OpenSSL::PKey::RSA
|
151
|
+
Log.log.debug("private=[#{rsa_private}]")
|
152
|
+
assertion = JWT.encode(jwt_payload, rsa_private, 'RS256', oauth.params[:jwt][:headers]||{})
|
153
|
+
Log.log.debug("assertion=[#{assertion}]")
|
154
|
+
return oauth.create_token(oauth.optional_scope_client_id({grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: assertion}))
|
155
|
+
}
|
156
|
+
|
157
|
+
attr_reader :params, :token_auth_api
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
# [M]=mandatory [D]=has default value [0]=accept nil
|
162
|
+
# :base_url [M] URL of authentication API
|
163
|
+
# :auth
|
164
|
+
# :crtype [M] :generic, :web, :jwt, custom
|
165
|
+
# :client_id [0]
|
166
|
+
# :client_secret [0]
|
167
|
+
# :scope [0]
|
168
|
+
# :path_token [D] API end point to create a token
|
169
|
+
# :token_field [D] field in result that contains the token
|
170
|
+
# :jwt:private_key_obj [M] for type :jwt
|
171
|
+
# :jwt:payload [M] for type :jwt
|
172
|
+
# :jwt:headers [0] for type :jwt
|
173
|
+
# :web:redirect_uri [M] for type :web
|
174
|
+
# :web:path_authorize [D] for type :web
|
175
|
+
# :generic [M] for type :generic
|
176
|
+
def initialize(a_params)
|
177
|
+
Log.log.debug("auth=#{a_params}")
|
178
|
+
# replace default values
|
179
|
+
@params=DEFAULT_CREATE_PARAMS.clone.deep_merge(a_params)
|
180
|
+
if @params.has_key?(:redirect_uri)
|
181
|
+
uri=URI.parse(@params[:web][:redirect_uri])
|
182
|
+
raise 'redirect_uri scheme must be http or https' unless ['http','https'].include?(uri.scheme)
|
183
|
+
raise 'redirect_uri must have a port' if uri.port.nil?
|
184
|
+
# TODO: we could check that host is localhost or local address
|
185
|
+
end
|
186
|
+
rest_params={base_url: @params[:base_url]}
|
187
|
+
rest_params[:auth]=a_params[:auth] if a_params.has_key?(:auth)
|
188
|
+
@token_auth_api=Rest.new(rest_params)
|
141
189
|
end
|
142
190
|
|
143
191
|
# @return unique identifier of token
|
144
|
-
|
145
|
-
|
146
|
-
parts=[PERSIST_CATEGORY_TOKEN
|
147
|
-
# add some of the parameters that uniquely
|
148
|
-
|
192
|
+
# TODO: external handlers shall provide unique identifiers
|
193
|
+
def token_cache_id
|
194
|
+
parts=[PERSIST_CATEGORY_TOKEN,@params[:base_url]]
|
195
|
+
# add some of the parameters that uniquely identify the token
|
196
|
+
self.class.id_elements.each do |p|
|
197
|
+
identifier=@params.dig(*p)
|
198
|
+
identifier=identifier.split(':').last if identifier.is_a?(String) && p.last.eql?(:grant_type)
|
199
|
+
parts.push(identifier) unless identifier.nil?
|
200
|
+
end
|
149
201
|
return IdGenerator.from_list(parts)
|
150
202
|
end
|
151
203
|
|
152
204
|
public
|
153
205
|
|
154
|
-
#
|
155
|
-
|
206
|
+
# helper method to create token as per RFC
|
207
|
+
def create_token(www_params)
|
208
|
+
return @token_auth_api.call({
|
209
|
+
operation: 'POST',
|
210
|
+
subpath: @params[:path_token],
|
211
|
+
headers: {'Accept'=>'application/json'},
|
212
|
+
www_body_params: www_params})
|
213
|
+
end
|
156
214
|
|
157
|
-
#
|
158
|
-
def
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
p_client_id_and_scope=p_scope.clone
|
165
|
-
p_client_id_and_scope[:client_id] = @params[:client_id] if @params.has_key?(:client_id)
|
166
|
-
use_refresh_token=options[:refresh]
|
215
|
+
# helper method
|
216
|
+
def optional_scope_client_id(call_params, add_secret: false)
|
217
|
+
call_params[:scope] = @params[:scope] unless @params[:scope].nil?
|
218
|
+
call_params[:client_id] = @params[:client_id] unless @params[:client_id].nil?
|
219
|
+
call_params[:client_secret] = @params[:client_secret] if add_secret && !@params[:client_id].nil?
|
220
|
+
return call_params
|
221
|
+
end
|
167
222
|
|
168
|
-
|
169
|
-
|
223
|
+
# Oauth v2 token generation
|
224
|
+
# @param use_refresh_token set to true to force refresh or re-generation (if previous failed)
|
225
|
+
def get_authorization(use_refresh_token: false)
|
226
|
+
# generate token unique identifier for persistency (memory/disk cache)
|
227
|
+
token_id=token_cache_id
|
170
228
|
|
171
229
|
# get token_data from cache (or nil), token_data is what is returned by /token
|
172
230
|
token_data=self.class.persist_mgr.get(token_id)
|
173
231
|
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
|
-
expires_at
|
183
|
-
|
184
|
-
|
185
|
-
|
232
|
+
# Optional optimization: check if node token is expired basd on decoded content then force refresh if close enough
|
233
|
+
# might help in case the transfer agent cannot refresh himself
|
234
|
+
# `direct` agent is equipped with refresh code
|
235
|
+
if !use_refresh_token && !token_data.nil?
|
236
|
+
decoded_token = self.class.decode_token(token_data[@params[:token_field]])
|
237
|
+
Log.dump('decoded_token',decoded_token) unless decoded_token.nil?
|
238
|
+
if decoded_token.is_a?(Hash)
|
239
|
+
expires_at_sec=
|
240
|
+
if decoded_token['expires_at'].is_a?(String) then DateTime.parse(decoded_token['expires_at']).to_time
|
241
|
+
elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
|
242
|
+
end
|
243
|
+
# force refresh if we see a token too close from expiration
|
244
|
+
use_refresh_token=true if expires_at_sec.is_a?(Time) && (expires_at_sec-Time.now) < TOKEN_EXPIRATION_GUARD_SEC
|
245
|
+
Log.log.debug("Expiration: #{expires_at_sec} / #{use_refresh_token}")
|
186
246
|
end
|
187
247
|
end
|
188
248
|
|
189
249
|
# an API was already called, but failed, we need to regenerate or refresh
|
190
250
|
if use_refresh_token
|
191
|
-
if token_data.is_a?(Hash)
|
251
|
+
if token_data.is_a?(Hash) && token_data.has_key?('refresh_token')
|
192
252
|
# save possible refresh token, before deleting the cache
|
193
253
|
refresh_token=token_data['refresh_token']
|
194
254
|
end
|
195
|
-
# delete
|
255
|
+
# delete cache
|
196
256
|
self.class.persist_mgr.delete(token_id)
|
197
257
|
token_data=nil
|
198
258
|
# lets try the existing refresh token
|
199
259
|
if !refresh_token.nil?
|
200
260
|
Log.log.info("refresh=[#{refresh_token}]".bg_green)
|
201
261
|
# 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
|
262
|
+
# note: AoC admin token has no refresh, and lives by default 1800secs
|
263
|
+
resp=create_token(optional_scope_client_id({grant_type: 'refresh_token',refresh_token: refresh_token}))
|
264
|
+
if resp[:http].code.start_with?('2')
|
208
265
|
# save only if success
|
209
266
|
json_data=resp[:http].body
|
210
267
|
token_data=JSON.parse(json_data)
|
@@ -215,116 +272,9 @@ module Aspera
|
|
215
272
|
end
|
216
273
|
end
|
217
274
|
|
218
|
-
# no cache
|
219
|
-
if token_data.nil?
|
220
|
-
resp=
|
221
|
-
case @params[:grant]
|
222
|
-
when :web
|
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 ?
|
275
|
+
# no cache, nor refresh: generate a token
|
276
|
+
if token_data.nil?
|
277
|
+
resp=self.class.token_creator(@params[:crtype]).call(self)
|
328
278
|
json_data=resp[:http].body
|
329
279
|
token_data=JSON.parse(json_data)
|
330
280
|
self.class.persist_mgr.put(token_id,json_data)
|
@@ -333,6 +283,5 @@ module Aspera
|
|
333
283
|
# ok we shall have a token here
|
334
284
|
return 'Bearer '+token_data[@params[:token_field]]
|
335
285
|
end
|
336
|
-
|
337
286
|
end # OAuth
|
338
287
|
end # Aspera
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'aspera/log'
|
2
3
|
require 'aspera/environment'
|
3
4
|
require 'rbconfig'
|
@@ -10,12 +11,12 @@ module Aspera
|
|
10
11
|
class OpenApplication
|
11
12
|
include Singleton
|
12
13
|
# User Interfaces
|
13
|
-
def self.user_interfaces; [
|
14
|
+
def self.user_interfaces; [:text, :graphical]; end
|
14
15
|
|
15
16
|
def self.default_gui_mode
|
16
17
|
return :graphical if [Aspera::Environment::OS_WINDOWS,Aspera::Environment::OS_X].include?(Aspera::Environment.os)
|
17
18
|
# unix family
|
18
|
-
return :graphical if ENV.has_key?(
|
19
|
+
return :graphical if ENV.has_key?('DISPLAY') && !ENV['DISPLAY'].empty?
|
19
20
|
return :text
|
20
21
|
end
|
21
22
|
|
@@ -27,7 +28,7 @@ module Aspera
|
|
27
28
|
when Aspera::Environment::OS_WINDOWS
|
28
29
|
return system('start explorer "'+uri.to_s+'"')
|
29
30
|
when Aspera::Environment::OS_LINUX
|
30
|
-
return system("xdg-open '#{uri
|
31
|
+
return system("xdg-open '#{uri}'")
|
31
32
|
else
|
32
33
|
raise "no graphical open method for #{Aspera::Environment.os}"
|
33
34
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'json'
|
2
3
|
require 'aspera/log'
|
3
4
|
|
@@ -13,11 +14,11 @@ module Aspera
|
|
13
14
|
# @param :merge Optional merge data from file to current data
|
14
15
|
def initialize(options)
|
15
16
|
Log.log.debug("persistency: #{options}")
|
16
|
-
raise
|
17
|
-
raise
|
18
|
-
raise
|
19
|
-
raise
|
20
|
-
raise
|
17
|
+
raise 'options shall be Hash' unless options.is_a?(Hash)
|
18
|
+
raise 'mandatory :manager' if options[:manager].nil?
|
19
|
+
raise 'mandatory :data' if options[:data].nil?
|
20
|
+
raise 'mandatory :id (String)' unless options[:id].is_a?(String)
|
21
|
+
raise 'mandatory 1 element in :id' unless options[:id].length >= 1
|
21
22
|
@manager=options[:manager]
|
22
23
|
@persisted_object=options[:data]
|
23
24
|
@object_id=options[:id]
|
@@ -41,6 +42,5 @@ module Aspera
|
|
41
42
|
def data
|
42
43
|
return @persisted_object
|
43
44
|
end
|
44
|
-
|
45
45
|
end
|
46
46
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'fileutils'
|
2
3
|
require 'aspera/log'
|
3
4
|
|
@@ -10,10 +11,6 @@ module Aspera
|
|
10
11
|
private_constant :FILE_SUFFIX
|
11
12
|
def initialize(folder)
|
12
13
|
@cache={}
|
13
|
-
set_folder(folder)
|
14
|
-
end
|
15
|
-
|
16
|
-
def set_folder(folder)
|
17
14
|
@folder=folder
|
18
15
|
Log.log.debug("persistency folder: #{@folder}")
|
19
16
|
end
|
@@ -22,12 +19,12 @@ module Aspera
|
|
22
19
|
def get(object_id)
|
23
20
|
Log.log.debug("persistency get: #{object_id}")
|
24
21
|
if @cache.has_key?(object_id)
|
25
|
-
Log.log.debug(
|
22
|
+
Log.log.debug('got from memory cache')
|
26
23
|
else
|
27
24
|
persist_filepath=id_to_filepath(object_id)
|
28
25
|
Log.log.debug("persistency = #{persist_filepath}")
|
29
26
|
if File.exist?(persist_filepath)
|
30
|
-
Log.log.debug(
|
27
|
+
Log.log.debug('got from file cache')
|
31
28
|
@cache[object_id]=File.read(persist_filepath)
|
32
29
|
end
|
33
30
|
end
|
@@ -35,7 +32,7 @@ module Aspera
|
|
35
32
|
end
|
36
33
|
|
37
34
|
def put(object_id,value)
|
38
|
-
raise
|
35
|
+
raise 'value: only String supported' unless value.is_a?(String)
|
39
36
|
persist_filepath=id_to_filepath(object_id)
|
40
37
|
Log.log.debug("persistency saving: #{persist_filepath}")
|
41
38
|
File.write(persist_filepath,value)
|
@@ -66,11 +63,10 @@ module Aspera
|
|
66
63
|
|
67
64
|
# @param object_id String or Array
|
68
65
|
def id_to_filepath(object_id)
|
69
|
-
raise
|
66
|
+
raise 'object_id: only String supported' unless object_id.is_a?(String)
|
70
67
|
FileUtils.mkdir_p(@folder)
|
71
68
|
return File.join(@folder,"#{object_id}#{FILE_SUFFIX}")
|
72
69
|
#.gsub(/[^a-z]+/,FILE_FIELD_SEPARATOR)
|
73
70
|
end
|
74
|
-
|
75
71
|
end # PersistencyFolder
|
76
72
|
end # Aspera
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'aspera/log'
|
2
3
|
require 'singleton'
|
3
4
|
|
@@ -305,15 +306,15 @@ module Aspera
|
|
305
306
|
def conversion_type(filepath,mimetype)
|
306
307
|
Log.log.debug("conversion_type(#{filepath},m=#{mimetype},t=#{@use_mimemagic})")
|
307
308
|
# 1- get type from provided mime type, using local mapping
|
308
|
-
conv_type=SUPPORTED_MIME_TYPES[mimetype] if !
|
309
|
+
conv_type=SUPPORTED_MIME_TYPES[mimetype] if !mimetype.nil?
|
309
310
|
# 2- else, from computed mime type (if available)
|
310
|
-
if conv_type.nil?
|
311
|
+
if conv_type.nil? && @use_mimemagic
|
311
312
|
detected_mime=mime_from_file(filepath)
|
312
|
-
if !
|
313
|
+
if !detected_mime.nil?
|
313
314
|
conv_type=SUPPORTED_MIME_TYPES[detected_mime]
|
314
|
-
if !
|
315
|
+
if !mimetype.nil?
|
315
316
|
if mimetype.eql?(detected_mime)
|
316
|
-
Log.log.debug(
|
317
|
+
Log.log.debug('matching mime type per magic number')
|
317
318
|
else
|
318
319
|
# note: detected can be nil
|
319
320
|
Log.log.debug("non matching mime types: node=[#{mimetype}], magic=[#{detected_mime}]")
|