aspera-cli 4.16.0 → 4.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +50 -19
  4. data/CONTRIBUTING.md +3 -1
  5. data/README.md +965 -793
  6. data/bin/asession +29 -21
  7. data/lib/aspera/{fasp/agent_alpha.rb → agent/alpha.rb} +26 -25
  8. data/lib/aspera/{fasp/agent_base.rb → agent/base.rb} +15 -12
  9. data/lib/aspera/{fasp/agent_connect.rb → agent/connect.rb} +13 -11
  10. data/lib/aspera/{fasp/agent_direct.rb → agent/direct.rb} +49 -53
  11. data/lib/aspera/{fasp/agent_httpgw.rb → agent/httpgw.rb} +20 -19
  12. data/lib/aspera/{fasp/agent_node.rb → agent/node.rb} +20 -33
  13. data/lib/aspera/{fasp/agent_trsdk.rb → agent/trsdk.rb} +11 -11
  14. data/lib/aspera/api/aoc.rb +586 -0
  15. data/lib/aspera/api/ats.rb +46 -0
  16. data/lib/aspera/api/cos_node.rb +95 -0
  17. data/lib/aspera/api/node.rb +344 -0
  18. data/lib/aspera/ascmd.rb +46 -10
  19. data/lib/aspera/{fasp → ascp}/installation.rb +5 -5
  20. data/lib/aspera/{fasp → ascp}/management.rb +3 -8
  21. data/lib/aspera/{fasp → ascp}/products.rb +1 -1
  22. data/lib/aspera/assert.rb +30 -30
  23. data/lib/aspera/cli/basic_auth_plugin.rb +11 -10
  24. data/lib/aspera/cli/extended_value.rb +1 -1
  25. data/lib/aspera/cli/formatter.rb +13 -13
  26. data/lib/aspera/cli/hints.rb +5 -5
  27. data/lib/aspera/cli/main.rb +35 -28
  28. data/lib/aspera/cli/manager.rb +25 -24
  29. data/lib/aspera/cli/plugin.rb +22 -15
  30. data/lib/aspera/cli/plugin_factory.rb +61 -0
  31. data/lib/aspera/cli/plugins/alee.rb +7 -7
  32. data/lib/aspera/cli/plugins/aoc.rb +83 -77
  33. data/lib/aspera/cli/plugins/ats.rb +32 -33
  34. data/lib/aspera/cli/plugins/bss.rb +3 -4
  35. data/lib/aspera/cli/plugins/config.rb +169 -186
  36. data/lib/aspera/cli/plugins/console.rb +8 -6
  37. data/lib/aspera/cli/plugins/cos.rb +19 -18
  38. data/lib/aspera/cli/plugins/faspex.rb +61 -54
  39. data/lib/aspera/cli/plugins/faspex5.rb +150 -103
  40. data/lib/aspera/cli/plugins/node.rb +68 -73
  41. data/lib/aspera/cli/plugins/orchestrator.rb +34 -44
  42. data/lib/aspera/cli/plugins/preview.rb +31 -31
  43. data/lib/aspera/cli/plugins/server.rb +31 -33
  44. data/lib/aspera/cli/plugins/shares.rb +13 -11
  45. data/lib/aspera/cli/sync_actions.rb +8 -8
  46. data/lib/aspera/cli/transfer_agent.rb +32 -19
  47. data/lib/aspera/cli/transfer_progress.rb +1 -1
  48. data/lib/aspera/cli/version.rb +1 -1
  49. data/lib/aspera/colors.rb +5 -0
  50. data/lib/aspera/command_line_builder.rb +14 -14
  51. data/lib/aspera/coverage.rb +1 -2
  52. data/lib/aspera/data_repository.rb +1 -1
  53. data/lib/aspera/environment.rb +2 -3
  54. data/lib/aspera/faspex_gw.rb +5 -6
  55. data/lib/aspera/faspex_postproc.rb +1 -1
  56. data/lib/aspera/id_generator.rb +2 -2
  57. data/lib/aspera/json_rpc.rb +5 -5
  58. data/lib/aspera/keychain/encrypted_hash.rb +6 -6
  59. data/lib/aspera/keychain/macos_security.rb +27 -22
  60. data/lib/aspera/log.rb +2 -2
  61. data/lib/aspera/nagios.rb +3 -3
  62. data/lib/aspera/node_simulator.rb +5 -6
  63. data/lib/aspera/oauth/base.rb +143 -0
  64. data/lib/aspera/oauth/factory.rb +124 -0
  65. data/lib/aspera/oauth/generic.rb +34 -0
  66. data/lib/aspera/oauth/jwt.rb +51 -0
  67. data/lib/aspera/oauth/url_json.rb +31 -0
  68. data/lib/aspera/oauth/web.rb +50 -0
  69. data/lib/aspera/oauth.rb +5 -331
  70. data/lib/aspera/open_application.rb +7 -7
  71. data/lib/aspera/persistency_action_once.rb +4 -4
  72. data/lib/aspera/persistency_folder.rb +2 -2
  73. data/lib/aspera/preview/generator.rb +5 -5
  74. data/lib/aspera/preview/terminal.rb +3 -2
  75. data/lib/aspera/preview/utils.rb +3 -3
  76. data/lib/aspera/proxy_auto_config.rb +4 -4
  77. data/lib/aspera/rest.rb +175 -144
  78. data/lib/aspera/rest_errors_aspera.rb +3 -3
  79. data/lib/aspera/resumer.rb +77 -0
  80. data/lib/aspera/ssh.rb +6 -1
  81. data/lib/aspera/{fasp → transfer}/error.rb +3 -3
  82. data/lib/aspera/{fasp → transfer}/error_info.rb +1 -1
  83. data/lib/aspera/{fasp → transfer}/faux_file.rb +1 -1
  84. data/lib/aspera/{fasp → transfer}/parameters.rb +58 -89
  85. data/lib/aspera/{fasp/transfer_spec.rb → transfer/spec.rb} +18 -16
  86. data/lib/aspera/{fasp/parameters.yaml → transfer/spec.yaml} +4 -99
  87. data/lib/aspera/{fasp → transfer}/sync.rb +32 -32
  88. data/lib/aspera/{fasp → transfer}/uri.rb +9 -8
  89. data/lib/aspera/web_server_simple.rb +11 -3
  90. data.tar.gz.sig +0 -0
  91. metadata +36 -63
  92. metadata.gz.sig +0 -0
  93. data/lib/aspera/aoc.rb +0 -601
  94. data/lib/aspera/ats_api.rb +0 -47
  95. data/lib/aspera/cos_node.rb +0 -94
  96. data/lib/aspera/fasp/resume_policy.rb +0 -79
  97. data/lib/aspera/node.rb +0 -339
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'aspera/assert'
5
+ require 'base64'
6
+ module Aspera
7
+ module OAuth
8
+ class Factory
9
+ include Singleton
10
+ # a prefix for persistency of tokens (simplify garbage collect)
11
+ PERSIST_CATEGORY_TOKEN = 'token'
12
+ # prefix for bearer token when in header
13
+ BEARER_PREFIX = 'Bearer '
14
+
15
+ private_constant :PERSIST_CATEGORY_TOKEN, :BEARER_PREFIX
16
+
17
+ class << self
18
+ def bearer_build(token)
19
+ return BEARER_PREFIX + token
20
+ end
21
+
22
+ def bearer_extract(token)
23
+ Aspera.assert(bearer?(token)){'not a bearer token, wrong prefix'}
24
+ return token[BEARER_PREFIX.length..-1]
25
+ end
26
+
27
+ def bearer?(token)
28
+ return token.start_with?(BEARER_PREFIX)
29
+ end
30
+
31
+ def id(*params)
32
+ return [PERSIST_CATEGORY_TOKEN, *params].flatten
33
+ end
34
+
35
+ def class_to_id(creator_class)
36
+ return creator_class.name.split('::').last.capital_to_snake.to_sym
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def initialize
43
+ # persistency manager
44
+ @persist = nil
45
+ # token creation methods
46
+ @token_type_classes = {}
47
+ @decoders = []
48
+ @globals = {
49
+ # remove 5 minutes to account for time offset between client and server (TODO: configurable?)
50
+ jwt_accepted_offset_sec: 300,
51
+ # one hour validity (TODO: configurable?)
52
+ jwt_expiry_offset_sec: 3600,
53
+ # tokens older than 30 minutes will be discarded from cache
54
+ token_cache_expiry_sec: 1800,
55
+ # tokens valid for less than this duration will be regenerated
56
+ token_expiration_guard_sec: 120
57
+ }
58
+ end
59
+
60
+ public
61
+
62
+ attr_reader :globals
63
+
64
+ def persist_mgr=(manager)
65
+ @persist = manager
66
+ # cleanup expired tokens
67
+ @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @globals[:token_cache_expiry_sec])
68
+ end
69
+
70
+ def persist_mgr
71
+ if @persist.nil?
72
+ # use OAuth::Factory.instance.persist_mgr=PersistencyFolder.new)
73
+ Log.log.debug('Not using persistency')
74
+ # create NULL persistency class
75
+ @persist = Class.new do
76
+ 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, Style/Semicolon, Layout/LineLength
77
+ end.new
78
+ end
79
+ return @persist
80
+ end
81
+
82
+ # delete all existing tokens
83
+ def flush_tokens
84
+ persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN, nil)
85
+ end
86
+
87
+ # register a bearer token decoder, mainly to inspect expiry date
88
+ def register_decoder(method)
89
+ @decoders.push(method)
90
+ end
91
+
92
+ # decode token using all registered decoders
93
+ def decode_token(token)
94
+ @decoders.each do |decoder|
95
+ result = decoder.call(token) rescue nil
96
+ return result unless result.nil?
97
+ end
98
+ return nil
99
+ end
100
+
101
+ # register a token creation method
102
+ # @param id creation type from field :grant_method in constructor
103
+ # @param lambda_create called to create token
104
+ # @param id_create called to generate unique id for token, for cache
105
+ def register_token_creator(creator_class)
106
+ Aspera.assert_type(creator_class, Class)
107
+ id = self.class.class_to_id(creator_class)
108
+ Log.log.debug{"registering token creator #{id}"}
109
+ @token_type_classes[id] = creator_class
110
+ end
111
+
112
+ # @return one of the registered creators for the given create type
113
+ def create(**parameters)
114
+ Aspera.assert_type(parameters, Hash)
115
+ id = parameters[:grant_method]
116
+ Aspera.assert(@token_type_classes.key?(id)){"token grant method unknown: '#{id}'"}
117
+ create_parameters = parameters.reject { |k, _v| k.eql?(:grant_method) }
118
+ @token_type_classes[id].new(**create_parameters)
119
+ end
120
+ end
121
+ # JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
122
+ Factory.instance.register_decoder(lambda { |token| parts = token.split('.'); Aspera.assert(parts.length.eql?(3)){'not aoc token'}; JSON.parse(Base64.decode64(parts[1]))}) # rubocop:disable Style/Semicolon, Layout/LineLength
123
+ end
124
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/oauth/base'
4
+
5
+ module Aspera
6
+ module OAuth
7
+ class Generic < Base
8
+ def initialize(
9
+ grant_type:,
10
+ response_type: nil,
11
+ apikey: nil,
12
+ receiver_client_ids: nil,
13
+ **base_params
14
+ )
15
+ super(**base_params)
16
+ @create_params = {
17
+ grant_type: grant_type
18
+ }
19
+ @create_params[:response_type] = response_type if response_type
20
+ @create_params[:apikey] = apikey if apikey
21
+ @create_params[:receiver_client_ids] = receiver_client_ids if receiver_client_ids
22
+ @identifiers.push(
23
+ @create_params[:grant_type]&.split(':')&.last,
24
+ @create_params[:apikey],
25
+ @create_params[:response_type])
26
+ end
27
+
28
+ def create_token
29
+ return create_token_call(optional_scope_client_id.merge(@create_params))
30
+ end
31
+ end
32
+ Factory.instance.register_token_creator(Generic)
33
+ end
34
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/oauth/base'
4
+ require 'aspera/assert'
5
+ require 'securerandom'
6
+ module Aspera
7
+ module OAuth
8
+ # Authentication using private key
9
+ class Jwt < Base
10
+ # @param g_o:private_key_obj [M] for type :jwt
11
+ # @param g_o:payload [M] for type :jwt
12
+ # @param g_o:headers [0] for type :jwt
13
+ def initialize(
14
+ payload:,
15
+ private_key_obj:,
16
+ headers: {},
17
+ **base_params
18
+ )
19
+ Aspera.assert_type(payload, Hash){'payload'}
20
+ Aspera.assert_type(private_key_obj, OpenSSL::PKey::RSA){'private_key_obj'}
21
+ Aspera.assert_type(headers, Hash){'headers'}
22
+ super(**base_params)
23
+ @private_key_obj = private_key_obj
24
+ @payload = payload
25
+ @headers = headers
26
+ @identifiers.push(@payload[:sub])
27
+ end
28
+
29
+ def create_token
30
+ # https://tools.ietf.org/html/rfc7523
31
+ # https://tools.ietf.org/html/rfc7519
32
+ require 'jwt'
33
+ seconds_since_epoch = Time.new.to_i
34
+ Log.log.info{"seconds=#{seconds_since_epoch}"}
35
+ Aspera.assert(@payload.is_a?(Hash)){'missing JWT payload'}
36
+ jwt_payload = {
37
+ exp: seconds_since_epoch + OAuth::Factory.instance.globals[:jwt_expiry_offset_sec], # expiration time
38
+ nbf: seconds_since_epoch - OAuth::Factory.instance.globals[:jwt_accepted_offset_sec], # not before
39
+ iat: seconds_since_epoch - OAuth::Factory.instance.globals[:jwt_accepted_offset_sec] + 1, # issued at
40
+ jti: SecureRandom.uuid # JWT id
41
+ }.merge(@payload)
42
+ Log.log.debug{"JWT jwt_payload=[#{jwt_payload}]"}
43
+ Log.log.debug{"private=[#{@private_key_obj}]"}
44
+ assertion = JWT.encode(jwt_payload, @private_key_obj, 'RS256', @headers)
45
+ Log.log.debug{"assertion=[#{assertion}]"}
46
+ return create_token_call(optional_scope_client_id.merge(grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: assertion))
47
+ end
48
+ end
49
+ Factory.instance.register_token_creator(Jwt)
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/oauth/base'
4
+
5
+ module Aspera
6
+ module OAuth
7
+ class UrlJson < Base
8
+ def initialize(
9
+ json:,
10
+ url:,
11
+ **generic_params
12
+ )
13
+ super(**generic_params)
14
+ @json_params = json
15
+ @url_params = url
16
+ @identifiers.push(@json_params[:url_token])
17
+ end
18
+
19
+ def create_token
20
+ @api.call(
21
+ operation: 'POST',
22
+ subpath: @path_token,
23
+ headers: {'Accept' => 'application/json'},
24
+ json_params: @json_params,
25
+ url_params: @url_params.merge(scope: @scope) # scope is here because it may change over time (node)
26
+ )
27
+ end
28
+ end
29
+ Factory.instance.register_token_creator(UrlJson)
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/oauth/base'
4
+ require 'aspera/open_application'
5
+ require 'aspera/web_auth'
6
+ require 'aspera/assert'
7
+ module Aspera
8
+ module OAuth
9
+ # Authentication using Web browser
10
+ class Web < Base
11
+ # @param g_o:redirect_uri [M] for type :web
12
+ # @param g_o:path_authorize [D] for type :web
13
+ def initialize(
14
+ redirect_uri:,
15
+ path_authorize: 'authorize',
16
+ **base_params
17
+ )
18
+ super(**base_params)
19
+ @redirect_uri = redirect_uri
20
+ @path_authorize = path_authorize
21
+ uri = URI.parse(@redirect_uri)
22
+ Aspera.assert(%w[http https].include?(uri.scheme)){'redirect_uri scheme must be http or https'}
23
+ Aspera.assert(!uri.port.nil?){'redirect_uri must have a port'}
24
+ # TODO: we could check that host is localhost or local address
25
+ end
26
+
27
+ def create_token
28
+ random_state = SecureRandom.uuid # used to check later
29
+ login_page_url = Rest.build_uri(
30
+ "#{@base_url}/#{@path_authorize}",
31
+ optional_scope_client_id.merge(response_type: 'code', redirect_uri: @redirect_uri, state: random_state))
32
+ # here, we need a human to authorize on a web page
33
+ Log.log.info{"login_page_url=#{login_page_url}".bg_red.gray}
34
+ # start a web server to receive request code
35
+ web_server = WebAuth.new(@redirect_uri)
36
+ # start browser on login page
37
+ OpenApplication.instance.uri(login_page_url)
38
+ # wait for code in request
39
+ received_params = web_server.received_request
40
+ Aspera.assert(random_state.eql?(received_params['state'])){'wrong received state'}
41
+ # exchange code for token
42
+ return create_token_call(optional_scope_client_id(add_secret: true).merge(
43
+ grant_type: 'authorization_code',
44
+ code: received_params['code'],
45
+ redirect_uri: @redirect_uri))
46
+ end
47
+ end
48
+ Factory.instance.register_token_creator(Web)
49
+ end
50
+ end
data/lib/aspera/oauth.rb CHANGED
@@ -1,333 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'aspera/open_application'
4
- require 'aspera/web_auth'
5
- require 'aspera/id_generator'
6
- require 'aspera/log'
7
- require 'aspera/assert'
8
- require 'base64'
9
- require 'date'
10
- require 'socket'
11
- require 'securerandom'
12
-
13
- module Aspera
14
- # Implement OAuth 2 for the REST client and generate a bearer token
15
- # call get_authorization() to get a token.
16
- # bearer tokens are kept in memory and also in a file cache for later re-use
17
- # if a token is expired (api returns 4xx), call again get_authorization({refresh: true})
18
- # https://tools.ietf.org/html/rfc6749
19
- class Oauth
20
- DEFAULT_CREATE_PARAMS = {
21
- path_token: 'token', # default endpoint for /token to generate token
22
- token_field: 'access_token', # field with token in result of call to path_token
23
- web: {path_authorize: 'authorize'} # default endpoint for /authorize, used for code exchange
24
- }.freeze
25
-
26
- # OAuth methods supported by default
27
- STD_AUTH_TYPES = %i[web jwt].freeze
28
-
29
- @@globals = { # rubocop:disable Style/ClassVars
30
- # remove 5 minutes to account for time offset between client and server (TODO: configurable?)
31
- jwt_accepted_offset_sec: 300,
32
- # one hour validity (TODO: configurable?)
33
- jwt_expiry_offset_sec: 3600,
34
- # tokens older than 30 minutes will be discarded from cache
35
- token_cache_expiry_sec: 1800,
36
- # tokens valid for less than this duration will be regenerated
37
- token_expiration_guard_sec: 120
38
- }
39
-
40
- # a prefix for persistency of tokens (simplify garbage collect)
41
- PERSIST_CATEGORY_TOKEN = 'token'
42
- # prefix for bearer token when in header
43
- BEARER_PREFIX = 'Bearer '
44
-
45
- private_constant :PERSIST_CATEGORY_TOKEN, :BEARER_PREFIX
46
-
47
- # persistency manager
48
- @persist = nil
49
- # token creation methods
50
- @create_handlers = {}
51
- # token unique identifiers from oauth parameters
52
- @id_handlers = {}
53
-
54
- class << self
55
- def bearer_build(token)
56
- return BEARER_PREFIX + token
57
- end
58
-
59
- def bearer_extract(token)
60
- assert(bearer?(token)){'not a bearer token, wrong prefix'}
61
- return token[BEARER_PREFIX.length..-1]
62
- end
63
-
64
- def bearer?(token)
65
- return token.start_with?(BEARER_PREFIX)
66
- end
67
-
68
- def persist_mgr=(manager)
69
- @persist = manager
70
- # cleanup expired tokens
71
- @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @@globals[:token_cache_expiry_sec])
72
- end
73
-
74
- def persist_mgr
75
- if @persist.nil?
76
- Log.log.debug('Not using persistency') # (use Aspera::Oauth.persist_mgr=Aspera::PersistencyFolder.new)
77
- # create NULL persistency class
78
- @persist = Class.new do
79
- 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, Style/Semicolon, Layout/LineLength
80
- end.new
81
- end
82
- return @persist
83
- end
84
-
85
- # delete all existing tokens
86
- def flush_tokens
87
- persist_mgr.garbage_collect(PERSIST_CATEGORY_TOKEN, nil)
88
- end
89
-
90
- # register a bearer token decoder, mainly to inspect expiry date
91
- def register_decoder(method)
92
- @decoders ||= []
93
- @decoders.push(method)
94
- end
95
-
96
- # decode token using all registered decoders
97
- def decode_token(token)
98
- @decoders.each do |decoder|
99
- result = decoder.call(token) rescue nil
100
- return result unless result.nil?
101
- end
102
- return nil
103
- end
104
-
105
- # register a token creation method
106
- # @param id creation type from field :grant_method in constructor
107
- # @param lambda_create called to create token
108
- # @param id_create called to generate unique id for token, for cache
109
- def register_token_creator(id, lambda_create, id_create)
110
- Log.log.debug{"registering token creator #{id}"}
111
- assert_type(id, Symbol)
112
- assert_type(lambda_create, Proc)
113
- assert_type(id_create, Proc)
114
- @create_handlers[id] = lambda_create
115
- @id_handlers[id] = id_create
116
- end
117
-
118
- # @return one of the registered creators for the given create type
119
- def token_creator(id)
120
- assert(@create_handlers.key?(id)){"token grant method unknown: '#{id}' (#{id.class})"}
121
- @create_handlers[id]
122
- end
123
-
124
- # list of identifiers found in creation parameters that can be used to uniquely identify the token
125
- def id_creator(id)
126
- assert(@id_handlers.key?(id)){"id creator type unknown: #{id}/#{id.class}"}
127
- @id_handlers[id]
128
- end
129
- end # self
130
-
131
- # JSON Web Signature (JWS) compact serialization: https://datatracker.ietf.org/doc/html/rfc7515
132
- register_decoder lambda { |token| parts = token.split('.'); assert(parts.length.eql?(3)){'not aoc token'}; JSON.parse(Base64.decode64(parts[1]))} # rubocop:disable Style/Semicolon, Layout/LineLength
133
-
134
- # generic token creation, parameters are provided in :generic
135
- register_token_creator :generic, lambda { |oauth|
136
- return oauth.create_token(oauth.specific_parameters)
137
- }, lambda { |oauth|
138
- return [
139
- oauth.specific_parameters[:grant_type]&.split(':')&.last,
140
- oauth.specific_parameters[:apikey],
141
- oauth.specific_parameters[:response_type]
142
- ]
143
- }
144
-
145
- # Authentication using Web browser
146
- register_token_creator :web, lambda { |oauth|
147
- random_state = SecureRandom.uuid # used to check later
148
- login_page_url = Rest.build_uri(
149
- "#{oauth.api.params[:base_url]}/#{oauth.specific_parameters[:path_authorize]}",
150
- oauth.optional_scope_client_id.merge(response_type: 'code', redirect_uri: oauth.specific_parameters[:redirect_uri], state: random_state))
151
- # here, we need a human to authorize on a web page
152
- Log.log.info{"login_page_url=#{login_page_url}".bg_red.gray}
153
- # start a web server to receive request code
154
- web_server = WebAuth.new(oauth.specific_parameters[:redirect_uri])
155
- # start browser on login page
156
- OpenApplication.instance.uri(login_page_url)
157
- # wait for code in request
158
- received_params = web_server.received_request
159
- assert(random_state.eql?(received_params['state'])){'wrong received state'}
160
- # exchange code for token
161
- return oauth.create_token(oauth.optional_scope_client_id(add_secret: true).merge(
162
- grant_type: 'authorization_code',
163
- code: received_params['code'],
164
- redirect_uri: oauth.specific_parameters[:redirect_uri]))
165
- }, lambda { |_oauth|
166
- return []
167
- }
168
-
169
- # Authentication using private key
170
- register_token_creator :jwt, lambda { |oauth|
171
- # https://tools.ietf.org/html/rfc7523
172
- # https://tools.ietf.org/html/rfc7519
173
- require 'jwt'
174
- seconds_since_epoch = Time.new.to_i
175
- Log.log.info{"seconds=#{seconds_since_epoch}"}
176
- assert(oauth.specific_parameters[:payload].is_a?(Hash)){'missing JWT payload'}
177
- jwt_payload = {
178
- exp: seconds_since_epoch + @@globals[:jwt_expiry_offset_sec], # expiration time
179
- nbf: seconds_since_epoch - @@globals[:jwt_accepted_offset_sec], # not before
180
- iat: seconds_since_epoch - @@globals[:jwt_accepted_offset_sec] + 1, # issued at (we tell a little in the past so that server always accepts)
181
- jti: SecureRandom.uuid # JWT id
182
- }.merge(oauth.specific_parameters[:payload])
183
- Log.log.debug{"JWT jwt_payload=[#{jwt_payload}]"}
184
- rsa_private = oauth.specific_parameters[:private_key_obj] # type: OpenSSL::PKey::RSA
185
- Log.log.debug{"private=[#{rsa_private}]"}
186
- assertion = JWT.encode(jwt_payload, rsa_private, 'RS256', oauth.specific_parameters[:headers] || {})
187
- Log.log.debug{"assertion=[#{assertion}]"}
188
- return oauth.create_token(oauth.optional_scope_client_id.merge(grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: assertion))
189
- }, lambda { |oauth|
190
- return [oauth.specific_parameters.dig(:payload, :sub)]
191
- }
192
-
193
- attr_reader :generic_parameters, :specific_parameters, :api
194
-
195
- private
196
-
197
- # [M]=mandatory [D]=has default value [0]=accept nil
198
- # :base_url [M] URL of authentication API
199
- # :auth
200
- # :grant_method [M] :generic, :web, :jwt, [custom types]
201
- # :client_id [0]
202
- # :client_secret [0]
203
- # :scope [0]
204
- # :path_token [D] API end point to create a token
205
- # :token_field [D] field in result that contains the token
206
- # :jwt:private_key_obj [M] for type :jwt
207
- # :jwt:payload [M] for type :jwt
208
- # :jwt:headers [0] for type :jwt
209
- # :web:redirect_uri [M] for type :web
210
- # :web:path_authorize [D] for type :web
211
- # :generic [M] for type :generic
212
- def initialize(a_params)
213
- Log.log.debug{"auth=#{a_params}"}
214
- # set default values if not set in parameters common to all types
215
- @generic_parameters = DEFAULT_CREATE_PARAMS.deep_merge(a_params)
216
- # check that type is known
217
- self.class.token_creator(@generic_parameters[:grant_method])
218
- # specific parameters for the creation type
219
- @specific_parameters = @generic_parameters[@generic_parameters[:grant_method]]
220
- if @generic_parameters[:grant_method].eql?(:web) && @specific_parameters.key?(:redirect_uri)
221
- uri = URI.parse(@specific_parameters[:redirect_uri])
222
- assert(%w[http https].include?(uri.scheme)){'redirect_uri scheme must be http or https'}
223
- assert(!uri.port.nil?){'redirect_uri must have a port'}
224
- # TODO: we could check that host is localhost or local address
225
- end
226
- rest_params = {
227
- base_url: @generic_parameters[:base_url],
228
- redirect_max: 2
229
- }
230
- rest_params[:auth] = a_params[:auth] if a_params.key?(:auth)
231
- # this is the OAuth API
232
- @api = Rest.new(rest_params)
233
- # if those are needed use from @api
234
- @generic_parameters.delete(:base_url)
235
- @generic_parameters.delete(:auth)
236
- @generic_parameters.delete(@generic_parameters[:grant_method])
237
- Log.log.debug{Log.dump(:generic_parameters, @generic_parameters)}
238
- Log.log.debug{Log.dump(:specific_parameters, @specific_parameters)}
239
- end
240
-
241
- public
242
-
243
- # helper method to create token as per RFC
244
- def create_token(www_params)
245
- Log.log.debug{'Generating a new token'.bg_green}
246
- return @api.call({
247
- operation: 'POST',
248
- subpath: @generic_parameters[:path_token],
249
- headers: {'Accept' => 'application/json'},
250
- www_body_params: www_params})
251
- end
252
-
253
- # @return Hash with optional general parameters
254
- def optional_scope_client_id(add_secret: false)
255
- call_params = {}
256
- call_params[:scope] = @generic_parameters[:scope] unless @generic_parameters[:scope].nil?
257
- call_params[:client_id] = @generic_parameters[:client_id] unless @generic_parameters[:client_id].nil?
258
- call_params[:client_secret] = @generic_parameters[:client_secret] if add_secret && !@generic_parameters[:client_id].nil?
259
- return call_params
260
- end
261
-
262
- # Oauth v2 token generation
263
- # @param use_refresh_token set to true to force refresh or re-generation (if previous failed)
264
- def get_authorization(use_refresh_token: false, use_cache: true)
265
- # generate token unique identifier for persistency (memory/disk cache)
266
- token_id = IdGenerator.from_list([
267
- PERSIST_CATEGORY_TOKEN,
268
- @api.params[:base_url],
269
- @generic_parameters[:grant_method],
270
- self.class.id_creator(@generic_parameters[:grant_method]).call(self), # array, so we flatten later
271
- @generic_parameters[:scope],
272
- @api.params.dig(*%i[auth username])
273
- ].flatten)
274
-
275
- # get token_data from cache (or nil), token_data is what is returned by /token
276
- token_data = self.class.persist_mgr.get(token_id) if use_cache
277
- token_data = JSON.parse(token_data) unless token_data.nil?
278
- # Optional optimization: check if node token is expired based on decoded content then force refresh if close enough
279
- # might help in case the transfer agent cannot refresh himself
280
- # `direct` agent is equipped with refresh code
281
- if !use_refresh_token && !token_data.nil?
282
- decoded_token = self.class.decode_token(token_data[@generic_parameters[:token_field]])
283
- Log.log.debug{Log.dump('decoded_token', decoded_token)} unless decoded_token.nil?
284
- if decoded_token.is_a?(Hash)
285
- expires_at_sec =
286
- if decoded_token['expires_at'].is_a?(String) then DateTime.parse(decoded_token['expires_at']).to_time
287
- elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
288
- end
289
- # force refresh if we see a token too close from expiration
290
- use_refresh_token = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < @@globals[:token_expiration_guard_sec]
291
- Log.log.debug{"Expiration: #{expires_at_sec} / #{use_refresh_token}"}
292
- end
293
- end
294
-
295
- # an API was already called, but failed, we need to regenerate or refresh
296
- if use_refresh_token
297
- if token_data.is_a?(Hash) && token_data.key?('refresh_token') && !token_data['refresh_token'].eql?('not_supported')
298
- # save possible refresh token, before deleting the cache
299
- refresh_token = token_data['refresh_token']
300
- end
301
- # delete cache
302
- self.class.persist_mgr.delete(token_id)
303
- token_data = nil
304
- # lets try the existing refresh token
305
- if !refresh_token.nil?
306
- Log.log.info{"refresh=[#{refresh_token}]".bg_green}
307
- # try to refresh
308
- # note: AoC admin token has no refresh, and lives by default 1800secs
309
- resp = create_token(optional_scope_client_id.merge(grant_type: 'refresh_token', refresh_token: refresh_token))
310
- if resp[:http].code.start_with?('2')
311
- # save only if success
312
- json_data = resp[:http].body
313
- token_data = JSON.parse(json_data)
314
- self.class.persist_mgr.put(token_id, json_data)
315
- else
316
- Log.log.debug{"refresh failed: #{resp[:http].body}".bg_red}
317
- end
318
- end
319
- end
320
-
321
- # no cache, nor refresh: generate a token
322
- if token_data.nil?
323
- resp = self.class.token_creator(@generic_parameters[:grant_method]).call(self)
324
- json_data = resp[:http].body
325
- token_data = JSON.parse(json_data)
326
- self.class.persist_mgr.put(token_id, json_data)
327
- end # if ! in_cache
328
- assert(token_data.key?(@generic_parameters[:token_field])){"API error: No such field in answer: #{@generic_parameters[:token_field]}"}
329
- # ok we shall have a token here
330
- return self.class.bearer_build(token_data[@generic_parameters[:token_field]])
331
- end
332
- end # OAuth
333
- end # Aspera
3
+ require 'aspera/oauth/factory'
4
+ require 'aspera/oauth/generic'
5
+ require 'aspera/oauth/jwt'
6
+ require 'aspera/oauth/web'
7
+ require 'aspera/oauth/url_json'
@@ -17,7 +17,7 @@ module Aspera
17
17
  def user_interfaces; USER_INTERFACES; end
18
18
 
19
19
  def default_gui_mode
20
- return :graphical if [Aspera::Environment::OS_WINDOWS, Aspera::Environment::OS_X].include?(Aspera::Environment.os)
20
+ return :graphical if [Environment::OS_WINDOWS, Environment::OS_X].include?(Environment.os)
21
21
  # unix family
22
22
  return :graphical if ENV.key?('DISPLAY') && !ENV['DISPLAY'].empty?
23
23
  return :text
@@ -25,19 +25,19 @@ module Aspera
25
25
 
26
26
  # command must be non blocking
27
27
  def uri_graphical(uri)
28
- case Aspera::Environment.os
29
- when Aspera::Environment::OS_X then return system('open', uri.to_s)
30
- when Aspera::Environment::OS_WINDOWS then return system('start', 'explorer', %Q{"#{uri}"})
31
- when Aspera::Environment::OS_LINUX then return system('xdg-open', uri.to_s)
28
+ case Environment.os
29
+ when Environment::OS_X then return system('open', uri.to_s)
30
+ when Environment::OS_WINDOWS then return system('start', 'explorer', %Q{"#{uri}"})
31
+ when Environment::OS_LINUX then return system('xdg-open', uri.to_s)
32
32
  else
33
- raise "no graphical open method for #{Aspera::Environment.os}"
33
+ raise "no graphical open method for #{Environment.os}"
34
34
  end
35
35
  end
36
36
 
37
37
  def editor(file_path)
38
38
  if ENV.key?('EDITOR')
39
39
  system(ENV['EDITOR'], file_path.to_s)
40
- elsif Aspera::Environment.os.eql?(Aspera::Environment::OS_WINDOWS)
40
+ elsif Environment.os.eql?(Environment::OS_WINDOWS)
41
41
  system('notepad.exe', %Q{"#{file_path}"})
42
42
  else
43
43
  uri_graphical(file_path.to_s)
@@ -15,10 +15,10 @@ module Aspera
15
15
  # @param :format Optional dump method (default to JSON)
16
16
  # @param :merge Optional merge data from file to current data
17
17
  def initialize(manager:, data:, id:, delete: nil, parse: nil, format: nil, merge: nil)
18
- assert(!manager.nil?)
19
- assert(!data.nil?)
20
- assert_type(id, String)
21
- assert(!id.empty?)
18
+ Aspera.assert(!manager.nil?)
19
+ Aspera.assert(!data.nil?)
20
+ Aspera.assert_type(id, String)
21
+ Aspera.assert(!id.empty?)
22
22
  @manager = manager
23
23
  @persisted_object = data
24
24
  @object_id = id