aspera-cli 4.4.0 → 4.7.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 +2095 -1503
- data/bin/ascli +2 -1
- data/bin/asession +4 -5
- data/docs/test_env.conf +3 -0
- data/examples/aoc.rb +4 -3
- data/examples/faspex4.rb +25 -25
- data/examples/proxy.pac +1 -1
- data/examples/transfer.rb +17 -17
- data/lib/aspera/aoc.rb +238 -185
- data/lib/aspera/ascmd.rb +93 -83
- 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 +142 -108
- 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 +18 -21
- data/lib/aspera/cli/main.rb +173 -149
- data/lib/aspera/cli/manager.rb +163 -168
- data/lib/aspera/cli/plugin.rb +43 -31
- data/lib/aspera/cli/plugins/alee.rb +6 -6
- data/lib/aspera/cli/plugins/aoc.rb +405 -370
- data/lib/aspera/cli/plugins/ats.rb +86 -79
- data/lib/aspera/cli/plugins/bss.rb +14 -16
- data/lib/aspera/cli/plugins/config.rb +580 -362
- 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 +201 -158
- data/lib/aspera/cli/plugins/faspex5.rb +80 -57
- data/lib/aspera/cli/plugins/node.rb +183 -166
- data/lib/aspera/cli/plugins/orchestrator.rb +71 -67
- 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 +35 -19
- data/lib/aspera/cli/plugins/sync.rb +20 -22
- data/lib/aspera/cli/transfer_agent.rb +76 -113
- 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/{manager.rb → agent_base.rb} +28 -25
- data/lib/aspera/fasp/{connect.rb → agent_connect.rb} +52 -43
- data/lib/aspera/fasp/{local.rb → agent_direct.rb} +58 -72
- data/lib/aspera/fasp/{http_gw.rb → agent_httpgw.rb} +37 -43
- data/lib/aspera/fasp/{node.rb → agent_node.rb} +35 -16
- data/lib/aspera/fasp/agent_trsdk.rb +104 -0
- data/lib/aspera/fasp/error.rb +2 -1
- data/lib/aspera/fasp/error_info.rb +68 -52
- data/lib/aspera/fasp/installation.rb +152 -124
- data/lib/aspera/fasp/listener.rb +1 -0
- data/lib/aspera/fasp/parameters.rb +87 -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 -89
- data/lib/aspera/hash_ext.rb +4 -3
- data/lib/aspera/id_generator.rb +8 -7
- data/lib/aspera/keychain/encrypted_hash.rb +121 -0
- data/lib/aspera/keychain/macos_security.rb +90 -0
- data/lib/aspera/log.rb +55 -37
- data/lib/aspera/nagios.rb +13 -12
- data/lib/aspera/node.rb +30 -25
- 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 +154 -135
- 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 +116 -29
- data/docs/Makefile +0 -66
- data/docs/README.erb.md +0 -3973
- data/docs/README.md +0 -13
- data/docs/diagrams.txt +0 -49
- data/docs/doc_tools.rb +0 -58
- data/lib/aspera/api_detector.rb +0 -60
- data/lib/aspera/cli/plugins/shares2.rb +0 -114
- data/lib/aspera/secrets.rb +0 -20
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}]")
|