aspera-cli 4.7.0 → 4.9.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 (96) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/README.md +1267 -999
  4. data/bin/ascli +20 -1
  5. data/bin/asession +37 -34
  6. data/docs/test_env.conf +7 -3
  7. data/examples/aoc.rb +13 -12
  8. data/examples/dascli +23 -0
  9. data/examples/faspex4.rb +34 -29
  10. data/examples/{transfer.rb → node.rb} +31 -59
  11. data/examples/server.rb +93 -0
  12. data/lib/aspera/aoc.rb +153 -143
  13. data/lib/aspera/ascmd.rb +56 -45
  14. data/lib/aspera/ats_api.rb +9 -6
  15. data/lib/aspera/cli/basic_auth_plugin.rb +18 -16
  16. data/lib/aspera/cli/extended_value.rb +33 -30
  17. data/lib/aspera/cli/formater.rb +105 -111
  18. data/lib/aspera/cli/info.rb +3 -2
  19. data/lib/aspera/cli/listener/line_dump.rb +1 -0
  20. data/lib/aspera/cli/listener/logger.rb +1 -0
  21. data/lib/aspera/cli/listener/progress.rb +13 -12
  22. data/lib/aspera/cli/listener/progress_multi.rb +21 -20
  23. data/lib/aspera/cli/main.rb +110 -90
  24. data/lib/aspera/cli/manager.rb +99 -88
  25. data/lib/aspera/cli/plugin.rb +98 -39
  26. data/lib/aspera/cli/plugins/alee.rb +6 -5
  27. data/lib/aspera/cli/plugins/aoc.rb +581 -450
  28. data/lib/aspera/cli/plugins/ats.rb +84 -83
  29. data/lib/aspera/cli/plugins/bss.rb +30 -27
  30. data/lib/aspera/cli/plugins/config.rb +488 -397
  31. data/lib/aspera/cli/plugins/console.rb +17 -15
  32. data/lib/aspera/cli/plugins/cos.rb +26 -35
  33. data/lib/aspera/cli/plugins/faspex.rb +206 -172
  34. data/lib/aspera/cli/plugins/faspex5.rb +109 -74
  35. data/lib/aspera/cli/plugins/node.rb +379 -189
  36. data/lib/aspera/cli/plugins/orchestrator.rb +71 -65
  37. data/lib/aspera/cli/plugins/preview.rb +131 -122
  38. data/lib/aspera/cli/plugins/server.rb +50 -150
  39. data/lib/aspera/cli/plugins/shares.rb +61 -27
  40. data/lib/aspera/cli/plugins/sync.rb +15 -14
  41. data/lib/aspera/cli/transfer_agent.rb +75 -64
  42. data/lib/aspera/cli/version.rb +2 -1
  43. data/lib/aspera/colors.rb +29 -28
  44. data/lib/aspera/command_line_builder.rb +50 -43
  45. data/lib/aspera/cos_node.rb +64 -38
  46. data/lib/aspera/data_repository.rb +1 -0
  47. data/lib/aspera/environment.rb +33 -10
  48. data/lib/aspera/fasp/agent_base.rb +35 -30
  49. data/lib/aspera/fasp/agent_connect.rb +35 -30
  50. data/lib/aspera/fasp/agent_direct.rb +68 -60
  51. data/lib/aspera/fasp/agent_httpgw.rb +71 -64
  52. data/lib/aspera/fasp/agent_node.rb +24 -23
  53. data/lib/aspera/fasp/agent_trsdk.rb +19 -20
  54. data/lib/aspera/fasp/error.rb +2 -1
  55. data/lib/aspera/fasp/error_info.rb +79 -68
  56. data/lib/aspera/fasp/installation.rb +130 -126
  57. data/lib/aspera/fasp/listener.rb +1 -0
  58. data/lib/aspera/fasp/parameters.rb +71 -60
  59. data/lib/aspera/fasp/parameters.yaml +69 -17
  60. data/lib/aspera/fasp/resume_policy.rb +14 -11
  61. data/lib/aspera/fasp/transfer_spec.rb +6 -5
  62. data/lib/aspera/fasp/uri.rb +25 -24
  63. data/lib/aspera/faspex_gw.rb +83 -72
  64. data/lib/aspera/hash_ext.rb +23 -13
  65. data/lib/aspera/id_generator.rb +16 -13
  66. data/lib/aspera/keychain/encrypted_hash.rb +61 -46
  67. data/lib/aspera/keychain/macos_security.rb +26 -24
  68. data/lib/aspera/log.rb +35 -39
  69. data/lib/aspera/nagios.rb +36 -28
  70. data/lib/aspera/node.rb +19 -19
  71. data/lib/aspera/oauth.rb +120 -100
  72. data/lib/aspera/open_application.rb +25 -22
  73. data/lib/aspera/persistency_action_once.rb +9 -8
  74. data/lib/aspera/persistency_folder.rb +13 -9
  75. data/lib/aspera/preview/file_types.rb +261 -266
  76. data/lib/aspera/preview/generator.rb +74 -73
  77. data/lib/aspera/preview/image_error.png +0 -0
  78. data/lib/aspera/preview/options.rb +7 -6
  79. data/lib/aspera/preview/utils.rb +30 -33
  80. data/lib/aspera/preview/video_error.png +0 -0
  81. data/lib/aspera/proxy_auto_config.rb +27 -23
  82. data/lib/aspera/rest.rb +73 -74
  83. data/lib/aspera/rest_call_error.rb +1 -0
  84. data/lib/aspera/rest_error_analyzer.rb +23 -19
  85. data/lib/aspera/rest_errors_aspera.rb +43 -40
  86. data/lib/aspera/secret_hider.rb +74 -0
  87. data/lib/aspera/ssh.rb +13 -10
  88. data/lib/aspera/sync.rb +49 -47
  89. data/lib/aspera/temp_file_manager.rb +7 -5
  90. data/lib/aspera/timer_limiter.rb +9 -8
  91. data/lib/aspera/uri_reader.rb +17 -18
  92. data/lib/aspera/web_auth.rb +17 -15
  93. data.tar.gz.sig +5 -0
  94. metadata +119 -35
  95. metadata.gz.sig +0 -0
  96. data/bin/dascli +0 -13
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'aspera/log'
3
4
  require 'aspera/aoc'
4
5
  require 'aspera/fasp/transfer_spec'
@@ -13,14 +14,14 @@ module Aspera
13
14
  # this class answers the Faspex /send API and creates a package on Aspera on Cloud
14
15
  class FaspexGW
15
16
  class FxGwServlet < WEBrick::HTTPServlet::AbstractServlet
16
- def initialize(_server,a_aoc_api_user,a_workspace_id)
17
+ def initialize(_server, a_aoc_api_user, a_workspace_id)
17
18
  super
18
- @aoc_api_user=a_aoc_api_user
19
- @aoc_workspace_id=a_workspace_id
19
+ @aoc_api_user = a_aoc_api_user
20
+ @aoc_workspace_id = a_workspace_id
20
21
  end
21
22
 
22
23
  # parameters from user to Faspex API call
23
- #{"delivery":{"use_encryption_at_rest":false,"note":"note","sources":[{"paths":["file1"]}],"title":"my title","recipients":["email1"],"send_upload_result":true}}
24
+ # {"delivery":{"use_encryption_at_rest":false,"note":"note","sources":[{"paths":["file1"]}],"title":"my title","recipients":["email1"],"send_upload_result":true}}
24
25
  # {
25
26
  # "delivery"=>{
26
27
  # "use_encryption_at_rest"=>false,
@@ -33,127 +34,137 @@ module Aspera
33
34
  # }
34
35
  def process_faspex_send(request, response)
35
36
  raise 'no payload' if request.body.nil?
36
- faspex_pkg_parameters=JSON.parse(request.body)
37
- faspex_pkg_delivery=faspex_pkg_parameters['delivery']
37
+
38
+ faspex_pkg_parameters = JSON.parse(request.body)
39
+ faspex_pkg_delivery = faspex_pkg_parameters['delivery']
38
40
  Log.log.debug("faspex pkg create parameters=#{faspex_pkg_parameters}")
39
41
 
40
42
  # get recipient ids
41
- files_pkg_recipients=[]
43
+ files_pkg_recipients = []
42
44
  faspex_pkg_delivery['recipients'].each do |recipient_email|
43
- user_lookup=@aoc_api_user.read('contacts',{'current_workspace_id'=>@aoc_workspace_id,'q'=>recipient_email})[:data]
44
- raise StandardError,"no such unique user: #{recipient_email} / #{user_lookup}" unless !user_lookup.nil? && user_lookup.length.eql?(1)
45
- recipient_user_info=user_lookup.first
46
- files_pkg_recipients.push({'id'=>recipient_user_info['source_id'],'type'=>recipient_user_info['source_type']})
45
+ user_lookup = @aoc_api_user.read('contacts',
46
+ { 'current_workspace_id' => @aoc_workspace_id, 'q' => recipient_email })[:data]
47
+ raise StandardError,
48
+ "no such unique user: #{recipient_email} / #{user_lookup}" unless !user_lookup.nil? && user_lookup.length.eql?(1)
49
+
50
+ recipient_user_info = user_lookup.first
51
+ files_pkg_recipients.push({
52
+ 'id' => recipient_user_info['source_id'],
53
+ 'type' => recipient_user_info['source_type']
54
+ })
47
55
  end
48
56
 
49
57
  # create a new package with one file
50
- the_package=@aoc_api_user.create('packages',{
51
- 'file_names' =>faspex_pkg_delivery['sources'][0]['paths'],
52
- 'name' =>faspex_pkg_delivery['title'],
53
- 'note' =>faspex_pkg_delivery['note'],
54
- 'recipients' =>files_pkg_recipients,
55
- 'workspace_id'=>@aoc_workspace_id})[:data]
58
+ the_package = @aoc_api_user.create('packages', {
59
+ 'file_names' => faspex_pkg_delivery['sources'][0]['paths'],
60
+ 'name' => faspex_pkg_delivery['title'],
61
+ 'note' => faspex_pkg_delivery['note'],
62
+ 'recipients' => files_pkg_recipients,
63
+ 'workspace_id' => @aoc_workspace_id
64
+ })[:data]
56
65
 
57
66
  # get node information for the node on which package must be created
58
- node_info=@aoc_api_user.read("nodes/#{the_package['node_id']}")[:data]
67
+ node_info = @aoc_api_user.read("nodes/#{the_package['node_id']}")[:data]
59
68
 
60
69
  # get transfer token (for node)
61
- node_auth_bearer_token=@aoc_api_user.oauth_token(scope: AoC.node_scope(node_info['access_key'],AoC::SCOPE_NODE_USER))
70
+ node_auth_bearer_token = @aoc_api_user.oauth_token(scope: AoC.node_scope(node_info['access_key'],
71
+ AoC::SCOPE_NODE_USER))
62
72
 
63
73
  # tell Files what to expect in package: 1 transfer (can also be done after transfer)
64
- @aoc_api_user.update("packages/#{the_package['id']}",{'sent'=>true,'transfers_expected'=>1})
74
+ @aoc_api_user.update("packages/#{the_package['id']}", { 'sent' => true, 'transfers_expected' => 1 })
65
75
 
66
76
  # to return an error:
67
77
  # response.status=400
68
78
  # return 'ERROR HERE'
69
79
 
70
80
  # TODO: check about xfer_*
71
- ts_tags={
81
+ ts_tags = {
72
82
  'aspera' => {
73
- 'files' => { 'package_id' => the_package['id'], 'package_operation' => 'upload' },
74
- 'node' => { 'access_key' => node_info['access_key'], 'file_id' => the_package['contents_file_id'] },
75
- 'xfer_id' => SecureRandom.uuid,
76
- 'xfer_retry' => 3600 } }
83
+ 'files' => { 'package_id' => the_package['id'], 'package_operation' => 'upload' },
84
+ 'node' => { 'access_key' => node_info['access_key'], 'file_id' => the_package['contents_file_id'] },
85
+ 'xfer_id' => SecureRandom.uuid,
86
+ 'xfer_retry' => 3600
87
+ }
88
+ }
77
89
  # this transfer spec is for transfer to AoC
78
- faspex_transfer_spec={
79
- 'direction' => 'send',
80
- 'remote_host' => node_info['host'],
81
- 'remote_user' => Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER,
82
- 'ssh_port' => Fasp::TransferSpec::SSH_PORT,
83
- 'fasp_port' => Fasp::TransferSpec::UDP_PORT,
84
- 'tags' => ts_tags,
85
- 'token' => node_auth_bearer_token,
86
- 'paths' => [{'destination' => '/'}],
87
- 'cookie' => 'unused',
88
- 'create_dir' => true,
89
- 'rate_policy' => 'fair',
90
- 'rate_policy_allowed' => 'fixed',
91
- 'min_rate_cap_kbps' => nil,
92
- 'min_rate_kbps' => 0,
90
+ faspex_transfer_spec = {
91
+ 'direction' => 'send',
92
+ 'remote_host' => node_info['host'],
93
+ 'remote_user' => Fasp::TransferSpec::ACCESS_KEY_TRANSFER_USER,
94
+ 'ssh_port' => Fasp::TransferSpec::SSH_PORT,
95
+ 'fasp_port' => Fasp::TransferSpec::UDP_PORT,
96
+ 'tags' => ts_tags,
97
+ 'token' => node_auth_bearer_token,
98
+ 'paths' => [{ 'destination' => '/' }],
99
+ 'cookie' => 'unused',
100
+ 'create_dir' => true,
101
+ 'rate_policy' => 'fair',
102
+ 'rate_policy_allowed' => 'fixed',
103
+ 'min_rate_cap_kbps' => nil,
104
+ 'min_rate_kbps' => 0,
93
105
  'target_rate_percentage' => nil,
94
- 'lock_target_rate' => nil,
95
- 'fasp_url' => 'unused',
96
- 'lock_min_rate' => true,
97
- 'lock_rate_policy' => true,
98
- 'source_root' => '',
99
- 'content_protection' => nil,
100
- 'target_rate_cap_kbps' => 20_000, # TODO: is this value useful ?
101
- 'target_rate_kbps' => 10_000, # TODO: get from where?
102
- 'cipher' => 'aes-128',
103
- 'cipher_allowed' => nil,
104
- 'http_fallback' => false,
105
- 'http_fallback_port' => nil,
106
- 'https_fallback_port' => nil,
107
- 'destination_root' => '/'
106
+ 'lock_target_rate' => nil,
107
+ 'fasp_url' => 'unused',
108
+ 'lock_min_rate' => true,
109
+ 'lock_rate_policy' => true,
110
+ 'source_root' => '',
111
+ 'content_protection' => nil,
112
+ 'target_rate_cap_kbps' => 20_000, # TODO: is this value useful ?
113
+ 'target_rate_kbps' => 10_000, # TODO: get from where?
114
+ 'cipher' => 'aes-128',
115
+ 'cipher_allowed' => nil,
116
+ 'http_fallback' => false,
117
+ 'http_fallback_port' => nil,
118
+ 'https_fallback_port' => nil,
119
+ 'destination_root' => '/'
108
120
  }
109
121
  # but we place it in a Faspex package creation response
110
- faspex_package_create_result={
111
- 'links' => {'status' => 'unused'},
122
+ faspex_package_create_result = {
123
+ 'links' => { 'status' => 'unused' },
112
124
  'xfer_sessions' => [faspex_transfer_spec]
113
125
  }
114
126
  Log.log.info("faspex_package_create_result=#{faspex_package_create_result}")
115
- response.status=200
127
+ response.status = 200
116
128
  response.content_type = 'application/json'
117
- response.body=JSON.generate(faspex_package_create_result)
129
+ response.body = JSON.generate(faspex_package_create_result)
118
130
  end
119
131
 
120
- def do_GET(request, response) # rubocop:disable Naming/MethodName
132
+ def do_GET(request, response)
121
133
  case request.path
122
134
  when '/aspera/faspex/send'
123
135
  process_faspex_send(request, response)
124
136
  else
125
- response.status=400
126
- return 'ERROR HERE'
137
+ response.status = 400
138
+ 'ERROR HERE'
127
139
  end
128
140
  end
129
141
  end # FxGwServlet
130
142
 
131
143
  class NewUserServlet < WEBrick::HTTPServlet::AbstractServlet
132
- def do_GET(request, response) # rubocop:disable Naming/MethodName
144
+ def do_GET(request, response)
133
145
  case request.path
134
146
  when '/newuser'
135
- response.status=200
147
+ response.status = 200
136
148
  response.content_type = 'text/html'
137
- response.body='<html><body>hello world</body></html>'
149
+ response.body = '<html><body>hello world</body></html>'
138
150
  else
139
151
  raise "unsupported path: [#{request.path}]"
140
152
  end
141
153
  end
142
154
  end
143
155
 
144
- def initialize(a_aoc_api_user,a_workspace_id)
156
+ def initialize(a_aoc_api_user, a_workspace_id)
145
157
  webrick_options = {
146
- Port: 9443,
147
- Logger: Log.log,
148
- SSLEnable: true,
149
- SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE,
150
- SSLCertName: [['CN',WEBrick::Utils.getservername]]
158
+ Port: 9443,
159
+ Logger: Log.log,
160
+ SSLEnable: true,
161
+ SSLCertName: [['CN', WEBrick::Utils.getservername]]
151
162
  }
152
163
  Log.log.info("Server started on port #{webrick_options[:Port]}")
153
164
  @server = WEBrick::HTTPServer.new(webrick_options)
154
- @server.mount('/aspera/faspex', FxGwServlet,a_aoc_api_user,a_workspace_id)
165
+ @server.mount('/aspera/faspex', FxGwServlet, a_aoc_api_user, a_workspace_id)
155
166
  @server.mount('/newuser', NewUserServlet)
156
- trap('INT') {@server.shutdown}
167
+ trap('INT') { @server.shutdown }
157
168
  end
158
169
 
159
170
  def start_server
@@ -1,29 +1,39 @@
1
1
  # frozen_string_literal: true
2
- # for older rubies
3
- unless Hash.method_defined?(:dig)
4
- class Hash
5
- def dig(*path)
6
- path.inject(self) do |location, key|
7
- location.respond_to?(:keys) ? location[key] : nil
8
- end
9
- end
10
- end
11
- end
12
2
 
13
3
  class ::Hash
14
4
  def deep_merge(second)
15
- merge(second){|_key,v1,v2|Hash===v1&&Hash===v2 ? v1.deep_merge(v2) : v2}
5
+ merge(second){|_key,v1,v2|Hash === v1 && Hash === v2 ? v1.deep_merge(v2) : v2}
16
6
  end
17
7
 
18
8
  def deep_merge!(second)
19
- merge!(second){|_key,v1,v2|Hash===v1&&Hash===v2 ? v1.deep_merge!(v2) : v2}
9
+ merge!(second){|_key,v1,v2|Hash === v1 && Hash === v2 ? v1.deep_merge!(v2) : v2}
10
+ end
11
+ end
12
+
13
+ # in 2.5
14
+ unless Hash.method_defined?(:transform_keys)
15
+ class Hash
16
+ def transform_keys
17
+ return each_with_object({}){|(k,v),memo|memo[yield(k)]=v} if block_given?
18
+ raise 'missing block'
19
+ end
20
20
  end
21
21
  end
22
22
 
23
+ # rails
23
24
  unless Hash.method_defined?(:symbolize_keys)
24
25
  class Hash
25
26
  def symbolize_keys
26
- return each_with_object({}){|(k,v),memo| memo[k.to_sym] = v; }
27
+ return transform_keys(&:to_sym)
28
+ end
29
+ end
30
+ end
31
+
32
+ # rails
33
+ unless Hash.method_defined?(:stringify_keys)
34
+ class Hash
35
+ def stringify_keys
36
+ return transform_keys(&:to_s)
27
37
  end
28
38
  end
29
39
  end
@@ -1,23 +1,26 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'uri'
3
4
 
4
5
  module Aspera
5
6
  class IdGenerator
6
- ID_SEPARATOR='_'
7
- WINDOWS_PROTECTED_CHAR=%r{[/:"<>\\*?]}
8
- PROTECTED_CHAR_REPLACE='_'
7
+ ID_SEPARATOR = '_'
8
+ WINDOWS_PROTECTED_CHAR = %r{[/:"<>\\*?]}.freeze
9
+ PROTECTED_CHAR_REPLACE = '_'
9
10
  private_constant :ID_SEPARATOR,:PROTECTED_CHAR_REPLACE,:WINDOWS_PROTECTED_CHAR
10
- def self.from_list(object_id)
11
- if object_id.is_a?(Array)
12
- object_id=object_id.reject(&:nil?).map do |i|
13
- i.is_a?(String) && i.start_with?('https://') ? URI.parse(i).host : i.to_s
14
- end.join(ID_SEPARATOR)
11
+ class << self
12
+ def from_list(object_id)
13
+ if object_id.is_a?(Array)
14
+ object_id = object_id.compact.map do |i|
15
+ i.is_a?(String) && i.start_with?('https://') ? URI.parse(i).host : i.to_s
16
+ end.join(ID_SEPARATOR)
17
+ end
18
+ raise 'id must be a String' unless object_id.is_a?(String)
19
+ return object_id.
20
+ gsub(WINDOWS_PROTECTED_CHAR,PROTECTED_CHAR_REPLACE). # remove windows forbidden chars
21
+ gsub('.',PROTECTED_CHAR_REPLACE). # keep dot for extension only (nicer)
22
+ downcase
15
23
  end
16
- raise 'id must be a String' unless object_id.is_a?(String)
17
- return object_id.
18
- gsub(WINDOWS_PROTECTED_CHAR,PROTECTED_CHAR_REPLACE). # remove windows forbidden chars
19
- gsub('.',PROTECTED_CHAR_REPLACE). # keep dot for extension only (nicer)
20
- downcase
21
24
  end
22
25
  end
23
26
  end
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require 'aspera/hash_ext'
2
4
  require 'openssl'
3
5
 
4
6
  module Aspera
5
7
  module Keychain
6
8
  class SimpleCipher
7
9
  def initialize(key)
8
- @key=Digest::SHA1.hexdigest(key)[0..23]
10
+ @key = Digest::SHA1.hexdigest(key+('*'*23))[0..23]
9
11
  @cipher = OpenSSL::Cipher.new('DES-EDE3-CBC')
10
12
  end
11
13
 
@@ -26,93 +28,106 @@ module Aspera
26
28
 
27
29
  # Manage secrets in a simple Hash
28
30
  class EncryptedHash
29
- SEPARATOR='%'
31
+ SEPARATOR = '%'
32
+ ACCEPTED_KEYS = %i[username url secret description].freeze
30
33
  private_constant :SEPARATOR
34
+ attr_reader :legacy_detected
31
35
  def initialize(values)
32
36
  raise 'values shall be Hash' unless values.is_a?(Hash)
33
- @all_secrets=values
37
+ @all_secrets = values
38
+ end
39
+
40
+ def identifier(options)
41
+ return options[:username] if options[:url].to_s.empty?
42
+ %i[url username].map{|s|options[s]}.join(SEPARATOR)
34
43
  end
35
44
 
36
45
  def set(options)
37
46
  raise 'options shall be Hash' unless options.is_a?(Hash)
38
- unsupported=options.keys-[:username,:url,:secret,:description]
47
+ unsupported = options.keys - ACCEPTED_KEYS
39
48
  raise "unsupported options: #{unsupported}" unless unsupported.empty?
40
- username=options[:username]
49
+ username = options[:username]
41
50
  raise 'options shall have username' if username.nil?
42
- url=options[:url]
51
+ url = options[:url]
43
52
  raise 'options shall have username' if url.nil?
44
- secret=options[:secret]
53
+ secret = options[:secret]
45
54
  raise 'options shall have secret' if secret.nil?
46
- key=[url,username].join(SEPARATOR)
55
+ key = identifier(options)
47
56
  raise "secret #{key} already exist, delete first" if @all_secrets.has_key?(key)
48
- obj={username: username, url: url, secret: SimpleCipher.new(key).encrypt(secret)}
49
- obj[:description]=options[:description] if options.has_key?(:description)
50
- @all_secrets[key]=obj
57
+ obj = {username: username, url: url, secret: SimpleCipher.new(key).encrypt(secret)}
58
+ obj[:description] = options[:description] if options.has_key?(:description)
59
+ @all_secrets[key] = obj.stringify_keys
51
60
  nil
52
61
  end
53
62
 
54
63
  def list
55
- result=[]
56
- @all_secrets.each do |k,v|
57
- case v
58
- when String
59
- o={username: k, url: '', description: ''}
60
- when Hash
61
- o=v.clone
62
- o.delete(:secret)
63
- o[:description]||=''
64
- else raise 'error'
65
- end
66
- o[:description]=v[:description] if v.is_a?(Hash) && v[:description].is_a?(String)
67
- result.push(o)
64
+ result = []
65
+ legacy_detected=false
66
+ @all_secrets.each do |name,value|
67
+ normal = # normalized version
68
+ case value
69
+ when String
70
+ legacy_detected=true
71
+ {username: name, url: '', secret: value}
72
+ when Hash then value.symbolize_keys
73
+ else raise 'error secret must be String (legacy) or Hash (new)'
74
+ end
75
+ normal[:description] = '' unless normal.has_key?(:description)
76
+ extraneous_keys=normal.keys - ACCEPTED_KEYS
77
+ Log.log.error("wrongs keys in secret hash: #{extraneous_keys.map(&:to_s).join(',')}") unless extraneous_keys.empty?
78
+ result.push(normal)
68
79
  end
80
+ Log.log.warn('Legacy vault format detected in config file, please refer to documentation to convert to new format.') if legacy_detected
69
81
  return result
70
82
  end
71
83
 
72
84
  def delete(options)
73
85
  raise 'options shall be Hash' unless options.is_a?(Hash)
74
- unsupported=options.keys-[:username,:url]
86
+ unsupported = options.keys - %i[username url]
75
87
  raise "unsupported options: #{unsupported}" unless unsupported.empty?
76
- username=options[:username]
88
+ username = options[:username]
77
89
  raise 'options shall have username' if username.nil?
78
- url=options[:url]
79
- key=nil
90
+ url = options[:url]
91
+ key = nil
80
92
  if !url.nil?
81
- extk=[url,username].join(SEPARATOR)
82
- key=extk if @all_secrets.has_key?(extk)
93
+ extk = identifier(options)
94
+ key = extk if @all_secrets.has_key?(extk)
83
95
  end
84
96
  # backward compatibility: TODO: remove in future ? (make url mandatory ?)
85
- key=username if key.nil? && @all_secrets.has_key?(username)
97
+ key = username if key.nil? && @all_secrets.has_key?(username)
86
98
  raise 'no such secret' if key.nil?
87
99
  @all_secrets.delete(key)
88
100
  end
89
101
 
90
102
  def get(options)
91
103
  raise 'options shall be Hash' unless options.is_a?(Hash)
92
- unsupported=options.keys-[:username,:url]
104
+ unsupported = options.keys - %i[username url]
93
105
  raise "unsupported options: #{unsupported}" unless unsupported.empty?
94
- username=options[:username]
106
+ username = options[:username]
95
107
  raise 'options shall have username' if username.nil?
96
- url=options[:url]
97
- val=nil
108
+ url = options[:url]
109
+ info = nil
98
110
  if !url.nil?
99
- val=@all_secrets[[url,username].join(SEPARATOR)]
111
+ info = @all_secrets[identifier(options)]
100
112
  end
101
113
  # backward compatibility: TODO: remove in future ? (make url mandatory ?)
102
- if val.nil?
103
- val=@all_secrets[username]
114
+ if info.nil?
115
+ info = @all_secrets[username]
104
116
  end
105
- result=options.clone
106
- case val
117
+ result = options.clone
118
+ case info
107
119
  when NilClass
108
- raise 'no such secret'
120
+ raise "no such secret: [#{url}|#{username}] in #{@all_secrets.keys.join(',')}"
109
121
  when String
110
- result.merge!({secret: val, description: ''})
122
+ result[:secret] = info
123
+ result[:description] = ''
111
124
  when Hash
112
- key=[url,username].join(SEPARATOR)
113
- plain=SimpleCipher.new(key).decrypt(val[:secret])
114
- result.merge!({secret: plain, description: val[:description]})
115
- else raise 'error'
125
+ info=info.symbolize_keys
126
+ key = identifier(options)
127
+ plain = SimpleCipher.new(key).decrypt(info[:secret]) rescue info[:secret]
128
+ result[:secret] = plain
129
+ result[:description] = info[:description]
130
+ else raise "#{info.class} is not an expected type"
116
131
  end
117
132
  return result
118
133
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'security'
3
4
 
4
5
  # enhance the gem to support other keychains
@@ -6,7 +7,7 @@ module Security
6
7
  class Keychain
7
8
  class << self
8
9
  def by_name(name)
9
- keychains_from_output('security list-keychains').select{|kc|kc.filename.end_with?("/#{name}.keychain-db")}.first
10
+ keychains_from_output('security list-keychains').find{|kc|kc.filename.end_with?("/#{name}.keychain-db")}
10
11
  end
11
12
  end
12
13
  end
@@ -14,20 +15,20 @@ module Security
14
15
  class Password
15
16
  class << self
16
17
  # add some login to original method
17
- alias orig_flags_for_options flags_for_options
18
+ alias_method :orig_flags_for_options, :flags_for_options
18
19
  def flags_for_options(options = {})
19
- keychain=options.delete(:keychain)
20
- url=options.delete(:url)
20
+ keychain = options.delete(:keychain)
21
+ url = options.delete(:url)
21
22
  if !url.nil?
22
- uri=URI.parse(url)
23
+ uri = URI.parse(url)
23
24
  raise 'only https' unless uri.scheme.eql?('https')
24
- options[:r]='htps'
25
+ options[:r] = 'htps'
25
26
  raise 'host required in URL' if uri.host.nil?
26
- options[:s]=uri.host
27
- options[:p]=uri.path unless ['','/'].include?(uri.path)
28
- options[:P]=uri.port unless uri.port.eql?(443) && !url.include?(':443/')
27
+ options[:s] = uri.host
28
+ options[:p] = uri.path unless ['','/'].include?(uri.path)
29
+ options[:P] = uri.port unless uri.port.eql?(443) && !url.include?(':443/')
29
30
  end
30
- flags=[orig_flags_for_options(options)]
31
+ flags = [orig_flags_for_options(options)]
31
32
  flags.push(keychain.filename) unless keychain.nil?
32
33
  flags.join(' ')
33
34
  end
@@ -40,35 +41,36 @@ module Aspera
40
41
  # keychain based on macOS keychain, using `security` cmmand line
41
42
  class MacosSecurity
42
43
  def initialize(name=nil)
43
- @keychain=name.nil? ? Security::Keychain.default_keychain : Security::Keychain.by_name(name)
44
+ @keychain = name.nil? ? Security::Keychain.default_keychain : Security::Keychain.by_name(name)
44
45
  raise "no such keychain #{name}" if @keychain.nil?
45
46
  end
46
47
 
47
48
  def set(options)
48
49
  raise 'options shall be Hash' unless options.is_a?(Hash)
49
- unsupported=options.keys-[:username,:url,:secret,:description]
50
+ unsupported = options.keys - %i[username url secret description]
50
51
  raise "unsupported options: #{unsupported}" unless unsupported.empty?
51
- username=options[:username]
52
+ username = options[:username]
52
53
  raise 'options shall have username' if username.nil?
53
- url=options[:url]
54
+ url = options[:url]
54
55
  raise 'options shall have url' if url.nil?
55
- secret=options[:secret]
56
+ secret = options[:secret]
56
57
  raise 'options shall have secret' if secret.nil?
57
58
  raise 'set not implemented'
58
59
  end
59
60
 
60
61
  def get(options)
61
62
  raise 'options shall be Hash' unless options.is_a?(Hash)
62
- unsupported=options.keys-[:username,:url]
63
+ unsupported = options.keys - %i[username url]
63
64
  raise "unsupported options: #{unsupported}" unless unsupported.empty?
64
- username=options[:username]
65
+ username = options[:username]
65
66
  raise 'options shall have username' if username.nil?
66
- url=options[:url]
67
+ url = options[:url]
67
68
  raise 'options shall have url' if url.nil?
68
- info=Security::InternetPassword.find(keychain: @keychain, url: url, account: username)
69
+ info = Security::InternetPassword.find(keychain: @keychain, url: url, account: username)
69
70
  raise 'not found' if info.nil?
70
- result=options.clone
71
- result.merge!({secret: info.password, description: info.attributes['icmt']})
71
+ result = options.clone
72
+ result[:secret] = info.password
73
+ result[:description] = info.attributes['icmt']
72
74
  return result
73
75
  end
74
76
 
@@ -78,11 +80,11 @@ module Aspera
78
80
 
79
81
  def delete(options)
80
82
  raise 'options shall be Hash' unless options.is_a?(Hash)
81
- unsupported=options.keys-[:username,:url]
83
+ unsupported = options.keys - %i[username url]
82
84
  raise "unsupported options: #{unsupported}" unless unsupported.empty?
83
- username=options[:username]
85
+ username = options[:username]
84
86
  raise 'options shall have username' if username.nil?
85
- url=options[:url]
87
+ url = options[:url]
86
88
  raise "delete not implemented #{url}"
87
89
  end
88
90
  end