aspera-cli 4.16.0 → 4.17.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.
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