hybrid_platforms_conductor 33.4.0 → 33.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +3 -5
  4. data/docs/config_dsl.md +7 -5
  5. data/lib/hybrid_platforms_conductor/bitbucket.rb +2 -2
  6. data/lib/hybrid_platforms_conductor/cmd_runner.rb +4 -4
  7. data/lib/hybrid_platforms_conductor/confluence.rb +2 -2
  8. data/lib/hybrid_platforms_conductor/connector.rb +1 -1
  9. data/lib/hybrid_platforms_conductor/credentials.rb +20 -12
  10. data/lib/hybrid_platforms_conductor/github.rb +1 -1
  11. data/lib/hybrid_platforms_conductor/hpc_plugins/action/bash.rb +1 -1
  12. data/lib/hybrid_platforms_conductor/hpc_plugins/action/remote_bash.rb +27 -17
  13. data/lib/hybrid_platforms_conductor/hpc_plugins/connector/local.rb +4 -2
  14. data/lib/hybrid_platforms_conductor/hpc_plugins/connector/my_connector.rb.sample +1 -1
  15. data/lib/hybrid_platforms_conductor/hpc_plugins/connector/ssh.rb +29 -20
  16. data/lib/hybrid_platforms_conductor/hpc_plugins/provisioner/proxmox.rb +1 -1
  17. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/keepass.rb +1 -1
  18. data/lib/hybrid_platforms_conductor/hpc_plugins/test/jenkins_ci_conf.rb +1 -1
  19. data/lib/hybrid_platforms_conductor/hpc_plugins/test/jenkins_ci_masters_ok.rb +2 -2
  20. data/lib/hybrid_platforms_conductor/logger_helpers.rb +17 -0
  21. data/lib/hybrid_platforms_conductor/thycotic.rb +2 -2
  22. data/lib/hybrid_platforms_conductor/version.rb +1 -1
  23. data/spec/hybrid_platforms_conductor_test/api/actions_executor/actions/bash_spec.rb +15 -0
  24. data/spec/hybrid_platforms_conductor_test/api/actions_executor/actions/remote_bash_spec.rb +32 -0
  25. data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/local/remote_actions_spec.rb +9 -0
  26. data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/remote_actions_spec.rb +38 -0
  27. data/spec/hybrid_platforms_conductor_test/api/cmd_runner_spec.rb +14 -0
  28. data/spec/hybrid_platforms_conductor_test/api/credentials_spec.rb +8 -4
  29. data/spec/hybrid_platforms_conductor_test/test_connector.rb +2 -2
  30. metadata +16 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5f1fa4755dba3c7830397e4b43204517131487eaa5942bd0492f64bb835def5
4
- data.tar.gz: 3d6d5de92054abbcfebc902f523466ba79167da40e33c25defc413ed28d2cc28
3
+ metadata.gz: daf46892dd3b5a79ff0e54f16d6881b80519e8fe926e9952ad7faafd1706c31e
4
+ data.tar.gz: 84f9ee06afb8141f140a8754d0961217ceb14b3f8d2d1d2577792be420d42aa2
5
5
  SHA512:
6
- metadata.gz: 944569bc9c74fbbbeff909f6d73ae3c2f8064cb3c6853c4f29c74e1a3e906d446f7db4600b3c66d64bc8642bdd173a694dc337fa78a411cb9da30107e14c7643
7
- data.tar.gz: 13acf6ece0db85b0a92caf752aa56b6510f3cfa3f82091c38f0cbd9dc0048056c9f6e9995e5db4dd97141fe0a1bea7e7db75db954b8f114c25acaadf11a7c228
6
+ metadata.gz: 7b1355694d9c7dbc5b2d2f9f6555972c25033d11bd07b14dfc728c94b2965ac1a852c91cb6ed4f20a26bc41cceb12f437a8b599177331cbf544e38ebc387cd4a
7
+ data.tar.gz: 8636507ef5724bd9f0b3ffbb1f08c3efd1a48d39fe7161a4421350b4a03a9afff279b5522175418742ba87f7daca4dcdb4382e10f3283c39f9816eceba04bdc3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # [v33.5.0](https://github.com/sweet-delights/hybrid-platforms-conductor/compare/v33.4.0...v33.5.0) (2021-07-07 11:03:01)
2
+
3
+ ### Features
4
+
5
+ * [[Feature] [Security] [#80] Use SecretString for credentials to avoid passwords and secrets accidental leakage](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/0db5f529e539b81f86b9f618a63e62bdbd58cb38)
6
+
1
7
  # [v33.4.0](https://github.com/sweet-delights/hybrid-platforms-conductor/compare/v33.3.0...v33.4.0) (2021-07-05 13:24:27)
2
8
 
3
9
  ### Features
data/README.md CHANGED
@@ -330,11 +330,9 @@ credentials_for(:bitbucket) do |resource, requester|
330
330
  puts 'Input Bitbucket password...'
331
331
  password = ''
332
332
  $stdin.noecho { |io| io.sysread(256, password) }
333
- begin
334
- password.chomp!
335
- requester.call 'my_bitbucket_name', password
336
- ensure
337
- SecretString.erase(password)
333
+ password.chomp!
334
+ SecretString.protect(password) do |secret_password|
335
+ requester.call 'my_bitbucket_name', secret_password
338
336
  end
339
337
  end
340
338
  ```
data/docs/config_dsl.md CHANGED
@@ -216,6 +216,10 @@ It takes the following parameters:
216
216
  * a code block that will be called back by any process needing credentials matching the ID and resource specification.
217
217
  The code block will be given both the resource name being accessed, and a requester object that needs to be given the corresponding user and password. When the requester finishes running, the credentials are not needed anymore and should be cleaned from memory to avoid vulnerabilities.
218
218
 
219
+ A secure way to five the password is to use a [`SecretString`](https://github.com/Muriel-Salvan/secret_string) class that will give the following guarantees:
220
+ * Avoid the password to leak inadvertently in logs, on-screen, files...
221
+ * Clear the password from memory as soon as it is not needed anymore.
222
+
219
223
  Examples:
220
224
  ```ruby
221
225
  # Using an environment variable as a password
@@ -228,11 +232,9 @@ credentials_for(:github) do |resource, requester|
228
232
  puts 'Input Github password...'
229
233
  password = ''
230
234
  $stdin.noecho { |io| io.sysread(256, password) }
231
- begin
232
- password.chomp!
233
- requester.call 'MyUserName', password
234
- ensure
235
- SecretString.erase(password)
235
+ password.chomp!
236
+ SecretString.protect(password) do |secret_password|
237
+ requester.call 'MyUserName', secret_password
236
238
  end
237
239
  end
238
240
 
@@ -77,7 +77,7 @@ module HybridPlatformsConductor
77
77
  # Parameters::
78
78
  # * *bitbucket_url* (String): The Bitbucket URL
79
79
  # * *bitbucket_user_name* (String): Bitbucket user name to be used when querying the API
80
- # * *bitbucket_password* (String): Bitbucket password to be used when querying the API
80
+ # * *bitbucket_password* (SecretString): Bitbucket password to be used when querying the API
81
81
  # * *logger* (Logger): Logger to be used [default = Logger.new(STDOUT)]
82
82
  # * *logger_stderr* (Logger): Logger to be used for stderr [default = Logger.new(STDERR)]
83
83
  def initialize(bitbucket_url, bitbucket_user_name, bitbucket_password, logger: Logger.new($stdout), logger_stderr: Logger.new($stderr))
@@ -148,7 +148,7 @@ module HybridPlatformsConductor
148
148
  http_response = nil
149
149
  loop do
150
150
  begin
151
- http_response = URI.parse(api_url).open(http_basic_authentication: [@bitbucket_user_name, @bitbucket_password])
151
+ http_response = URI.parse(api_url).open(http_basic_authentication: [@bitbucket_user_name, @bitbucket_password&.to_unprotected])
152
152
  rescue
153
153
  raise if retries.zero?
154
154
 
@@ -59,7 +59,7 @@ module HybridPlatformsConductor
59
59
  # Raise an exception if the exit status is not the expected one.
60
60
  #
61
61
  # Parameters::
62
- # * *cmd* (String): Command to be run
62
+ # * *cmd* (String or SecretString): Command to be run
63
63
  # * *log_to_file* (String or nil): Log file capturing stdout or stderr (or nil for none). [default: nil]
64
64
  # * *log_to_stdout* (Boolean): Do we send the output to stdout? [default: true]
65
65
  # * *log_stdout_to_io* (IO or nil): IO to send command's stdout to, or nil for none. [default: nil]
@@ -108,7 +108,7 @@ module HybridPlatformsConductor
108
108
  bash_file = nil
109
109
  if force_bash
110
110
  bash_file = Tempfile.new('hpc_bash')
111
- bash_file.write(cmd)
111
+ bash_file.write(cmd.to_unprotected)
112
112
  bash_file.chmod 0o700
113
113
  bash_file.close
114
114
  cmd = "/bin/bash -c #{bash_file.path}"
@@ -136,7 +136,7 @@ module HybridPlatformsConductor
136
136
  pty: true,
137
137
  timeout: timeout,
138
138
  uuid: false
139
- ).run!(cmd) do |stdout, stderr|
139
+ ).run!(cmd.to_unprotected) do |stdout, stderr|
140
140
  stdout_queue << stdout if stdout
141
141
  stderr_queue << stderr if stderr
142
142
  end
@@ -162,7 +162,7 @@ module HybridPlatformsConductor
162
162
  log_debug "Finished in #{elapsed} seconds with exit status #{exit_status} (#{(expected_code.include?(exit_status) ? 'success'.light_green : 'failure'.light_red).bold})"
163
163
  end
164
164
  unless expected_code.include?(exit_status)
165
- error_title = "Command '#{cmd.split("\n").first}' returned error code #{exit_status} (expected #{expected_code.join(', ')})."
165
+ error_title = "Command '#{cmd.to_s.split("\n").first}' returned error code #{exit_status} (expected #{expected_code.join(', ')})."
166
166
  if no_exception
167
167
  # We consider the caller is responsible for logging what he wants about the details of the error (stdout and stderr)
168
168
  log_error error_title
@@ -34,7 +34,7 @@ module HybridPlatformsConductor
34
34
  # Parameters::
35
35
  # * *confluence_url* (String): The Confluence URL
36
36
  # * *confluence_user_name* (String): Confluence user name to be used when querying the API
37
- # * *confluence_password* (String): Confluence password to be used when querying the API
37
+ # * *confluence_password* (SecretString): Confluence password to be used when querying the API
38
38
  # * *logger* (Logger): Logger to be used [default = Logger.new(STDOUT)]
39
39
  # * *logger_stderr* (Logger): Logger to be used for stderr [default = Logger.new(STDERR)]
40
40
  def initialize(confluence_url, confluence_user_name, confluence_password, logger: Logger.new($stdout), logger_stderr: Logger.new($stderr))
@@ -109,7 +109,7 @@ module HybridPlatformsConductor
109
109
  page_url = URI.parse("#{@confluence_url}/#{api_path}")
110
110
  Net::HTTP.start(page_url.host, page_url.port, use_ssl: true) do |http|
111
111
  request = Net::HTTP.const_get(http_method.to_s.capitalize.to_sym).new(page_url.request_uri)
112
- request.basic_auth @confluence_user_name, @confluence_password
112
+ request.basic_auth @confluence_user_name, @confluence_password&.to_unprotected
113
113
  yield request if block_given?
114
114
  response = http.request(request)
115
115
  raise "Confluence page API request on #{page_url} returned an error: #{response.code}\n#{response.body}\n===== Request body =====\n#{request.body}" unless response.is_a?(Net::HTTPSuccess)
@@ -67,7 +67,7 @@ module HybridPlatformsConductor
67
67
  # Handle the redirection of standard output and standard error to file and stdout depending on the context of the run.
68
68
  #
69
69
  # Parameters::
70
- # * *cmd* (String): The command to be run
70
+ # * *cmd* (String or SecretString): The command to be run
71
71
  # * *force_bash* (Boolean): If true, then make sure command is invoked with bash instead of sh [default: false]
72
72
  # Result::
73
73
  # * Integer: Exit code
@@ -1,4 +1,5 @@
1
1
  require 'netrc'
2
+ require 'secret_string'
2
3
  require 'uri'
3
4
  require 'hybrid_platforms_conductor/logger_helpers'
4
5
 
@@ -65,8 +66,8 @@ module HybridPlatformsConductor
65
66
  # * Proc: Client code called with credentials provided
66
67
  # * Parameters::
67
68
  # * *user* (String or nil): User name, or nil if none
68
- # * *password* (String or nil): Password, or nil if none.
69
- # !!! Never store this password in a scope broader than the client code itself !!!
69
+ # * *password* (SecretString or nil): Password, or nil if none.
70
+ # !!! Never clone this password in a scope broader than the client code itself !!!
70
71
  def with_credentials_for(id, resource: nil)
71
72
  # Get the credentials provider
72
73
  provider = nil
@@ -81,7 +82,8 @@ module HybridPlatformsConductor
81
82
 
82
83
  provider ||= proc do |requested_resource, requester|
83
84
  # Check environment variables
84
- user = ENV["hpc_user_for_#{id}"].dup
85
+ user = ENV["hpc_user_for_#{id}"]
86
+ # Clone the password as we are going to treat it as a secret string that will be wiped out
85
87
  password = ENV["hpc_password_for_#{id}"].dup
86
88
  if user.nil? || user.empty? || password.nil? || password.empty?
87
89
  log_debug "[ Credentials for #{id} ] - Credentials not found from environment variables."
@@ -103,6 +105,7 @@ module HybridPlatformsConductor
103
105
  # TODO: Add more credentials source if needed here
104
106
  log_warn "[ Credentials for #{id} ] - Unable to get credentials for #{id} (Resource: #{requested_resource})."
105
107
  else
108
+ # Clone in memory as we are going to wipe out ::Netrc's memory
106
109
  user = netrc_user.dup
107
110
  password = netrc_password.dup
108
111
  log_debug "[ Credentials for #{id} ] - Credentials retrieved from .netrc using #{requested_resource}."
@@ -115,19 +118,18 @@ module HybridPlatformsConductor
115
118
  data_string.replace('GotYou!!!' * 100)
116
119
  end
117
120
  end
118
- # We do this assignment on purpose so that GC can remove sensitive data later
119
- # rubocop:disable Lint/UselessAssignment
120
- netrc = nil
121
- # rubocop:enable Lint/UselessAssignment
122
121
  end
123
122
  end
124
123
  else
125
124
  log_debug "[ Credentials for #{id} ] - Credentials retrieved from environment variables."
126
125
  end
127
- GC.start
128
- requester.call user, password
129
- password&.replace('gotyou!' * 100)
130
- GC.start
126
+ if password.nil?
127
+ requester.call user, password
128
+ else
129
+ SecretString.protect(password) do |secret_password|
130
+ requester.call user, secret_password
131
+ end
132
+ end
131
133
  end
132
134
 
133
135
  requester_called = false
@@ -135,7 +137,13 @@ module HybridPlatformsConductor
135
137
  resource,
136
138
  proc do |user, password|
137
139
  requester_called = true
138
- yield user, password
140
+ if password.is_a?(String)
141
+ SecretString.protect(password) do |secret_password|
142
+ yield user, secret_password
143
+ end
144
+ else
145
+ yield user, password
146
+ end
139
147
  end
140
148
  )
141
149
 
@@ -23,7 +23,7 @@ module HybridPlatformsConductor
23
23
  c.api_endpoint = repo_info[:url]
24
24
  end
25
25
  with_credentials_for(:github, resource: repo_info[:url]) do |_github_user, github_token|
26
- client = Octokit::Client.new(access_token: github_token)
26
+ client = Octokit::Client.new(access_token: github_token&.to_unprotected)
27
27
  (repo_info[:repos] == :all ? client.repositories(repo_info[:user]).map { |repo| repo[:name] } : repo_info[:repos]).each do |name|
28
28
  yield client, {
29
29
  name: name,
@@ -14,7 +14,7 @@ module HybridPlatformsConductor
14
14
  # [API] - @actions_executor is accessible
15
15
  #
16
16
  # Parameters::
17
- # * *cmd* (String): The bash command to execute
17
+ # * *cmd* (String or SecretString): The bash command to execute
18
18
  def setup(cmd)
19
19
  @cmd = cmd
20
20
  end
@@ -15,30 +15,30 @@ module HybridPlatformsConductor
15
15
  #
16
16
  # Parameters::
17
17
  # * *remote_bash* (Array or Object): List of commands (or single command) to be executed. Each command can be the following:
18
- # * String: Simple bash command.
18
+ # * String or SecretString: Simple bash command.
19
19
  # * Hash<Symbol, Object>: Information about the commands to execute. Can have the following properties:
20
- # * *commands* (Array<String> or String): List of bash commands to execute (can be a single one) [default: ''].
20
+ # * *commands* (Array<String or SecretString> or String or SecretString): List of bash commands to execute (can be a single one) [default: ''].
21
21
  # * *file* (String): Name of file from which commands should be taken [optional].
22
- # * *env* (Hash<String, String>): Environment variables to be set before executing those commands [default: {}].
22
+ # * *env* (Hash<String, String or SecretString>): Environment variables to be set before executing those commands [default: {}].
23
23
  def setup(remote_bash)
24
24
  # Normalize the parameters.
25
25
  # Array< Hash<Symbol,Object> >: Simple array of info:
26
- # * *commands* (Array<String>): List of bash commands to execute.
27
- # * *env* (Hash<String, String>): Environment variables to be set before executing those commands.
26
+ # * *commands* (Array<String or SecretString>): List of bash commands to execute.
27
+ # * *env* (Hash<String, String or SecretString>): Environment variables to be set before executing those commands.
28
28
  @remote_bash = (remote_bash.is_a?(Array) ? remote_bash : [remote_bash]).map do |cmd_info|
29
- if cmd_info.is_a?(String)
30
- {
31
- commands: [cmd_info],
32
- env: {}
33
- }
34
- else
29
+ if cmd_info.is_a?(Hash)
35
30
  commands = []
36
- commands.concat(cmd_info[:commands].is_a?(String) ? [cmd_info[:commands]] : cmd_info[:commands]) if cmd_info[:commands]
31
+ commands.concat(cmd_info[:commands].is_a?(Array) ? cmd_info[:commands] : [cmd_info[:commands]]) if cmd_info[:commands]
37
32
  commands << File.read(cmd_info[:file]) if cmd_info[:file]
38
33
  {
39
34
  commands: commands,
40
35
  env: cmd_info[:env] || {}
41
36
  }
37
+ else
38
+ {
39
+ commands: [cmd_info],
40
+ env: {}
41
+ }
42
42
  end
43
43
  end
44
44
  end
@@ -63,11 +63,21 @@ module HybridPlatformsConductor
63
63
  # [API] - @stderr_io can be used to log stderr messages
64
64
  # [API] - run_cmd(String) method can be used to execute a command. See CmdRunner#run_cmd to know about the result's signature.
65
65
  def execute
66
- bash_str = @remote_bash.map do |cmd_info|
67
- (cmd_info[:env].map { |var_name, var_value| "export #{var_name}='#{var_value}'" } + cmd_info[:commands]).join("\n")
68
- end.join("\n")
69
- log_debug "[#{@node}] - Execute remote Bash commands \"#{bash_str}\"..."
70
- @connector.remote_bash bash_str
66
+ # The commands or ENV variables can contain secrets, so make sure to protect all strings from secrets leaking
67
+ bash_cmds = @remote_bash.map do |cmd_info|
68
+ cmd_info[:env].map do |var_name, var_value|
69
+ SecretString.new("export #{var_name}='#{var_value.to_unprotected}'", silenced_str: "export #{var_name}='#{var_value}'")
70
+ end + cmd_info[:commands]
71
+ end.flatten
72
+ begin
73
+ SecretString.protect(bash_cmds.map(&:to_unprotected).join("\n"), silenced_str: bash_cmds.join("\n")) do |bash_str|
74
+ log_debug "[#{@node}] - Execute remote Bash commands \"#{bash_str}\"..."
75
+ @connector.remote_bash bash_str
76
+ end
77
+ ensure
78
+ # Make sure we erase all secret strings
79
+ bash_cmds.each(&:erase)
80
+ end
71
81
  end
72
82
 
73
83
  end
@@ -35,9 +35,11 @@ module HybridPlatformsConductor
35
35
  # [API] - @stderr_io can be used to send stderr output
36
36
  #
37
37
  # Parameters::
38
- # * *bash_cmds* (String): Bash commands to execute
38
+ # * *bash_cmds* (String or SecretString): Bash commands to execute. Use #to_unprotected to access the real content (otherwise secrets are obfuscated).
39
39
  def remote_bash(bash_cmds)
40
- run_cmd "cd #{workspace_for(@node)} ; #{bash_cmds}", force_bash: true
40
+ SecretString.protect("cd #{workspace_for(@node)} ; #{bash_cmds.to_unprotected}", silenced_str: "cd #{workspace_for(@node)} ; #{bash_cmds}") do |cmd|
41
+ run_cmd cmd, force_bash: true
42
+ end
41
43
  end
42
44
 
43
45
  # Execute an interactive shell on the remote node
@@ -104,7 +104,7 @@ module HybridPlatformsConductor
104
104
  # [API] - @stderr_io can be used to send stderr output
105
105
  #
106
106
  # Parameters::
107
- # * *bash_cmds* (String): Bash commands to execute
107
+ # * *bash_cmds* (String or SecretString): Bash commands to execute. Use #to_unprotected to access the real content (otherwise secrets are obfuscated).
108
108
  def remote_bash(bash_cmds)
109
109
  MyConnectLib.connect_to(@nodes_handler.get_host_ip_of(@node)).run_bash(bash_cmds)
110
110
  end
@@ -236,31 +236,40 @@ module HybridPlatformsConductor
236
236
  # [API] - @stderr_io can be used to send stderr output
237
237
  #
238
238
  # Parameters::
239
- # * *bash_cmds* (String): Bash commands to execute
239
+ # * *bash_cmds* (String or SecretString): Bash commands to execute. Use #to_unprotected to access the real content (otherwise secrets are obfuscated).
240
240
  def remote_bash(bash_cmds)
241
- ssh_cmd =
241
+ SecretString.protect(
242
242
  if @nodes_handler.get_ssh_session_exec_of(@node) == false
243
243
  # When ExecSession is disabled we need to use stdin directly
244
- "{ cat | #{ssh_exec} #{ssh_url} -T; } <<'HPC_EOF'\n#{bash_cmds}\nHPC_EOF"
244
+ "{ cat | #{ssh_exec} #{ssh_url} -T; } <<'HPC_EOF'\n#{bash_cmds.to_unprotected}\nHPC_EOF"
245
245
  else
246
- "#{ssh_exec} #{ssh_url} /bin/bash <<'HPC_EOF'\n#{bash_cmds}\nHPC_EOF"
247
- end
248
- # Due to a limitation of Process.spawn, each individual argument is limited to 128KB of size.
249
- # Therefore we need to make sure that if bash_cmds exceeds MAX_CMD_ARG_LENGTH bytes (considering EOF chars) then we use an intermediary shell script to store the commands.
250
- if bash_cmds.size > MAX_CMD_ARG_LENGTH
251
- # Write the commands in a file
252
- temp_file = "#{Dir.tmpdir}/hpc_temp_cmds_#{Digest::MD5.hexdigest(bash_cmds)}.sh"
253
- File.open(temp_file, 'w+') do |file|
254
- file.write ssh_cmd
255
- file.chmod 0o700
256
- end
257
- begin
258
- run_cmd(temp_file)
259
- ensure
260
- File.unlink(temp_file)
246
+ "#{ssh_exec} #{ssh_url} /bin/bash <<'HPC_EOF'\n#{bash_cmds.to_unprotected}\nHPC_EOF"
247
+ end,
248
+ silenced_str:
249
+ if @nodes_handler.get_ssh_session_exec_of(@node) == false
250
+ # When ExecSession is disabled we need to use stdin directly
251
+ "{ cat | #{ssh_exec} #{ssh_url} -T; } <<'HPC_EOF'\n#{bash_cmds}\nHPC_EOF"
252
+ else
253
+ "#{ssh_exec} #{ssh_url} /bin/bash <<'HPC_EOF'\n#{bash_cmds}\nHPC_EOF"
254
+ end
255
+ ) do |ssh_cmd|
256
+ # Due to a limitation of Process.spawn, each individual argument is limited to 128KB of size.
257
+ # Therefore we need to make sure that if bash_cmds exceeds MAX_CMD_ARG_LENGTH bytes (considering EOF chars) then we use an intermediary shell script to store the commands.
258
+ if bash_cmds.to_unprotected.size > MAX_CMD_ARG_LENGTH
259
+ # Write the commands in a file
260
+ temp_file = "#{Dir.tmpdir}/hpc_temp_cmds_#{Digest::MD5.hexdigest(bash_cmds.to_unprotected)}.sh"
261
+ File.open(temp_file, 'w+') do |file|
262
+ file.write ssh_cmd.to_unprotected
263
+ file.chmod 0o700
264
+ end
265
+ begin
266
+ run_cmd(temp_file)
267
+ ensure
268
+ File.unlink(temp_file)
269
+ end
270
+ else
271
+ run_cmd ssh_cmd
261
272
  end
262
- else
263
- run_cmd ssh_cmd
264
273
  end
265
274
  end
266
275
 
@@ -284,7 +284,7 @@ module HybridPlatformsConductor
284
284
  # cf https://pve.proxmox.com/wiki/Renaming_a_PVE_node
285
285
  URI.parse(url).host.downcase.split('.').first,
286
286
  user,
287
- password,
287
+ password&.to_unprotected,
288
288
  ENV['hpc_realm_for_proxmox'] || 'pam',
289
289
  {
290
290
  verify_ssl: false,
@@ -90,7 +90,7 @@ module HybridPlatformsConductor
90
90
  key_file = ENV['hpc_key_file_for_keepass']
91
91
  password_enc = ENV['hpc_password_enc_for_keepass']
92
92
  keepass_credentials = {}
93
- keepass_credentials[:password] = password if password
93
+ keepass_credentials[:password] = password.to_unprotected if password
94
94
  keepass_credentials[:password_enc] = password_enc if password_enc
95
95
  keepass_credentials[:key_file] = key_file if key_file
96
96
  KeepassKpscript.
@@ -26,7 +26,7 @@ module HybridPlatformsConductor
26
26
  else
27
27
  with_credentials_for(:jenkins_ci, resource: repo_info[:jenkins_ci_url]) do |jenkins_user, jenkins_password|
28
28
  # Get its config
29
- doc = Nokogiri::XML(URI.parse("#{repo_info[:jenkins_ci_url]}/config.xml").open(http_basic_authentication: [jenkins_user, jenkins_password]).read)
29
+ doc = Nokogiri::XML(URI.parse("#{repo_info[:jenkins_ci_url]}/config.xml").open(http_basic_authentication: [jenkins_user, jenkins_password&.to_unprotected]).read)
30
30
  # Check that this job builds the correct Bitbucket repository
31
31
  assert_equal(
32
32
  doc.xpath('/org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject/sources/data/jenkins.branch.BranchSource/source/serverUrl').text,
@@ -34,10 +34,10 @@ module HybridPlatformsConductor
34
34
  master_info_url = "#{repo_info[:jenkins_ci_url]}/job/master/api/json"
35
35
  with_credentials_for(:jenkins_ci, resource: master_info_url) do |jenkins_user, jenkins_password|
36
36
  # Get the master branch info from the API
37
- master_info = JSON.parse(URI.parse(master_info_url).open(http_basic_authentication: [jenkins_user, jenkins_password]).read)
37
+ master_info = JSON.parse(URI.parse(master_info_url).open(http_basic_authentication: [jenkins_user, jenkins_password&.to_unprotected]).read)
38
38
  # Get the last build's URL
39
39
  last_build_info_url = "#{master_info['lastBuild']['url']}/api/json"
40
- last_build_info = JSON.parse(URI.parse(last_build_info_url).open(http_basic_authentication: [jenkins_user, jenkins_password]).read)
40
+ last_build_info = JSON.parse(URI.parse(last_build_info_url).open(http_basic_authentication: [jenkins_user, jenkins_password&.to_unprotected]).read)
41
41
  log_debug "Build info for #{master_info_url}:\n#{JSON.pretty_generate(last_build_info)}"
42
42
  error "Last build for job #{repo_info[:project]}/#{repo_info[:name]} is in status #{last_build_info['result']}: #{master_info['lastBuild']['url']}" unless SUCCESS_STATUSES.include?(last_build_info['result'])
43
43
  rescue
@@ -1,6 +1,23 @@
1
1
  require 'colorize'
2
2
  require 'logger'
3
3
  require 'ruby-progressbar'
4
+ require 'secret_string'
5
+
6
+ # Add colorization methods to SecretString, but always directed to the silenced string as we NEVER want to modiy/clone a secret
7
+ class SecretString
8
+
9
+ extend Colorize::ClassMethods
10
+
11
+ def_delegators :@silenced_str, *%i[
12
+ colorize
13
+ uncolorize
14
+ colorized?
15
+ ]
16
+
17
+ color_methods
18
+ modes_methods
19
+
20
+ end
4
21
 
5
22
  module HybridPlatformsConductor
6
23
 
@@ -33,7 +33,7 @@ module HybridPlatformsConductor
33
33
  # Parameters::
34
34
  # * *url* (String): URL of the Thycotic Secret Server
35
35
  # * *user* (String): User name to be used to connect to Thycotic
36
- # * *password* (String): Password to be used to connect to Thycotic
36
+ # * *password* (SecretString): Password to be used to connect to Thycotic
37
37
  # * *domain* (String): Domain to use for authentication to Thycotic [default: ENV['hpc_domain_for_thycotic']]
38
38
  # * *logger* (Logger): Logger to be used [default: Logger.new(STDOUT)]
39
39
  # * *logger_stderr* (Logger): Logger to be used for stderr [default: Logger.new(STDERR)]
@@ -57,7 +57,7 @@ module HybridPlatformsConductor
57
57
  :authenticate,
58
58
  message: {
59
59
  username: user,
60
- password: password,
60
+ password: password&.to_unprotected,
61
61
  domain: domain
62
62
  }
63
63
  ).to_hash.dig(:authenticate_response, :authenticate_result, :token)
@@ -1,5 +1,5 @@
1
1
  module HybridPlatformsConductor
2
2
 
3
- VERSION = '33.4.0'
3
+ VERSION = '33.5.0'
4
4
 
5
5
  end
@@ -17,6 +17,21 @@ describe HybridPlatformsConductor::ActionsExecutor do
17
17
  end
18
18
  end
19
19
 
20
+ it 'executes local Bash code from a SecretString' do
21
+ with_test_platform_for_action_plugins do |repository|
22
+ expect(
23
+ test_actions_executor.execute_actions(
24
+ {
25
+ 'node' => {
26
+ bash: SecretString.new("echo TestContent >#{repository}/test_file ; echo TestStdout ; echo TestStderr 1>&2", silenced_str: '__INVALID_BASH__')
27
+ }
28
+ }
29
+ )['node']
30
+ ).to eq [0, "TestStdout\n", "TestStderr\n"]
31
+ expect(File.read("#{repository}/test_file")).to eq "TestContent\n"
32
+ end
33
+ end
34
+
20
35
  it 'executes local Bash code with timeout' do
21
36
  with_test_platform_for_action_plugins do
22
37
  expect(test_actions_executor.execute_actions(
@@ -13,6 +13,17 @@ describe HybridPlatformsConductor::ActionsExecutor do
13
13
  end
14
14
  end
15
15
 
16
+ it 'executes remote Bash code from a SecretString' do
17
+ with_test_platform_for_action_plugins do
18
+ test_actions_executor.execute_actions({ 'node' => { remote_bash: SecretString.new('remote_bash_cmd.bash', silenced_str: '__INVALID_BASH__') } })
19
+ expect(test_actions_executor.connector(:test_connector).calls).to eq [
20
+ [:connectable_nodes_from, ['node']],
21
+ [:with_connection_to, ['node'], { no_exception: true }],
22
+ [:remote_bash, 'remote_bash_cmd.bash']
23
+ ]
24
+ end
25
+ end
26
+
16
27
  it 'executes remote Bash code with timeout' do
17
28
  with_test_platform_for_action_plugins do
18
29
  test_actions_executor.connector(:test_connector).remote_bash_code = proc do |_stdout, _stderr, connector|
@@ -124,6 +135,27 @@ describe HybridPlatformsConductor::ActionsExecutor do
124
135
  end
125
136
  end
126
137
 
138
+ it 'executes remote Bash code with environment variables set using SecretStrings' do
139
+ with_test_platform_for_action_plugins do
140
+ test_actions_executor.execute_actions(
141
+ {
142
+ 'node' => { remote_bash: {
143
+ commands: 'bash_cmd.bash',
144
+ env: {
145
+ 'var1' => SecretString.new('value1', silenced_str: 'SILENCED_VALUE'),
146
+ 'var2' => 'value2'
147
+ }
148
+ } }
149
+ }
150
+ )
151
+ expect(test_actions_executor.connector(:test_connector).calls).to eq [
152
+ [:connectable_nodes_from, ['node']],
153
+ [:with_connection_to, ['node'], { no_exception: true }],
154
+ [:remote_bash, "export var1='value1'\nexport var2='value2'\nbash_cmd.bash"]
155
+ ]
156
+ end
157
+ end
158
+
127
159
  end
128
160
 
129
161
  end
@@ -54,6 +54,15 @@ describe HybridPlatformsConductor::ActionsExecutor do
54
54
  end
55
55
  end
56
56
 
57
+ it 'executes bash commands remotely from a SecretString' do
58
+ with_test_platform_for_remote_testing(
59
+ expected_cmds: [['cd /tmp/hpc_local_workspaces/node ; bash_cmd.bash', proc { [0, 'Bash commands executed on node', ''] }]],
60
+ expected_stdout: 'Bash commands executed on node'
61
+ ) do
62
+ test_connector.remote_bash(SecretString.new('bash_cmd.bash', silenced_str: '__INVALID_BASH__'))
63
+ end
64
+ end
65
+
57
66
  it 'executes bash commands remotely with timeout' do
58
67
  with_test_platform_for_remote_testing(
59
68
  expected_cmds: [
@@ -13,6 +13,15 @@ describe HybridPlatformsConductor::ActionsExecutor do
13
13
  end
14
14
  end
15
15
 
16
+ it 'executes bash commands remotely from a SecretString' do
17
+ with_test_platform_for_remote_testing(
18
+ expected_cmds: [[%r{.+/ssh hpc\.node /bin/bash <<'HPC_EOF'\nbash_cmd.bash\nHPC_EOF}, proc { [0, 'Bash commands executed on node', ''] }]],
19
+ expected_stdout: 'Bash commands executed on node'
20
+ ) do
21
+ test_connector.remote_bash(SecretString.new('bash_cmd.bash', silenced_str: '__INVALID_BASH__'))
22
+ end
23
+ end
24
+
16
25
  it 'executes bash commands remotely with timeout' do
17
26
  with_test_platform_for_remote_testing(
18
27
  expected_cmds: [
@@ -88,6 +97,25 @@ describe HybridPlatformsConductor::ActionsExecutor do
88
97
  end
89
98
  end
90
99
 
100
+ it 'executes really big bash commands remotely using a SecretString' do
101
+ cmd = "echo #{'1' * 131_060}"
102
+ with_test_platform_for_remote_testing(
103
+ expected_cmds: [
104
+ [
105
+ %r{.+/hpc_temp_cmds_.+\.sh$},
106
+ proc do |received_cmd|
107
+ expect(File.read(received_cmd)).to match(%r{.+/ssh hpc\.node /bin/bash <<'HPC_EOF'\n#{Regexp.escape(cmd)}\nHPC_EOF})
108
+ [0, 'Bash commands executed on node', '']
109
+ end
110
+ ]
111
+ ],
112
+ expected_stdout: 'Bash commands executed on node'
113
+ ) do
114
+ # Use an argument that exceeds the max arg length limit
115
+ test_connector.remote_bash(SecretString.new(cmd, silenced_str: '__INVALID_BASH__'))
116
+ end
117
+ end
118
+
91
119
  it 'copies files remotely with sudo' do
92
120
  with_test_platform_for_remote_testing(
93
121
  expected_cmds: [
@@ -153,6 +181,16 @@ describe HybridPlatformsConductor::ActionsExecutor do
153
181
  end
154
182
  end
155
183
 
184
+ it 'executes bash commands remotely without Session Exec capabilities using a SecretString' do
185
+ with_test_platform_for_remote_testing(
186
+ expected_cmds: [[%r{^\{ cat \| .+/ssh hpc\.node -T; \} <<'HPC_EOF'\nbash_cmd.bash\nHPC_EOF$}, proc { [0, 'Bash commands executed on node', ''] }]],
187
+ expected_stdout: 'Bash commands executed on node',
188
+ session_exec: false
189
+ ) do
190
+ test_connector.remote_bash(SecretString.new('bash_cmd.bash', silenced_str: '__INVALID_BASH__'))
191
+ end
192
+ end
193
+
156
194
  it 'copies files remotely without Session Exec capabilities' do
157
195
  with_test_platform_for_remote_testing(
158
196
  expected_cmds: [
@@ -7,6 +7,13 @@ describe HybridPlatformsConductor::CmdRunner do
7
7
  end
8
8
  end
9
9
 
10
+ it 'runs a simple bash command in a SecretString' do
11
+ with_repository do |repository|
12
+ test_cmd_runner.run_cmd SecretString.new("echo TestContent >#{repository}/test_file", silenced_str: '__INVALID_BASH__')
13
+ expect(File.read("#{repository}/test_file")).to eq "TestContent\n"
14
+ end
15
+ end
16
+
10
17
  it 'runs a simple bash command and returns exit code, stdout and stderr correctly' do
11
18
  with_repository do
12
19
  expect(test_cmd_runner.run_cmd('echo TestStderr 1>&2 ; echo TestStdout')).to eq [0, "TestStdout\n", "TestStderr\n"]
@@ -20,6 +27,13 @@ describe HybridPlatformsConductor::CmdRunner do
20
27
  end
21
28
  end
22
29
 
30
+ it 'runs a simple bash command and forces usage of bash in a SecretString' do
31
+ with_repository do
32
+ # Use set -o pipefail that does not work in /bin/sh
33
+ expect(test_cmd_runner.run_cmd(SecretString.new('set -o pipefail ; echo TestStderr 1>&2 ; echo TestStdout', silenced_str: '__INVALID_BASH__'), force_bash: true)).to eq [0, "TestStdout\n", "TestStderr\n"]
34
+ end
35
+ end
36
+
23
37
  it 'runs a simple bash command and logs stdout and stderr to a file' do
24
38
  with_repository do |repository|
25
39
  test_cmd_runner.run_cmd 'echo TestStderr 1>&2 ; sleep 1 ; echo TestStdout', log_to_file: "#{repository}/test_file"
@@ -15,15 +15,19 @@ describe HybridPlatformsConductor::Credentials do
15
15
  # * *resource* (String or nil): The resource for which we query the credentials, or nil if none [default: nil]
16
16
  def expect_credentials_to_be(expected_user, expected_password, resource: nil)
17
17
  creds = {}
18
+ password_class = nil
18
19
  credential_tester_class.new(logger: logger, logger_stderr: logger, config: test_config).instance_exec do
19
20
  with_credentials_for(:test_credential, resource: resource) do |user, password|
21
+ password_class = password.class
20
22
  creds = {
21
23
  user: user,
22
24
  # We clone the value as for security reasons it is removed when exiting the block
23
- password: password.clone
25
+ password: password&.to_unprotected.clone
24
26
  }
25
27
  end
26
28
  end
29
+ # Make sure we always return a SecretString for the password
30
+ expect(password_class).to be SecretString unless password_class == NilClass
27
31
  expect(creds).to eq(
28
32
  user: expected_user,
29
33
  password: expected_password
@@ -64,7 +68,7 @@ describe HybridPlatformsConductor::Credentials do
64
68
  leaked_password = password
65
69
  end
66
70
  end
67
- expect(leaked_password).to eq 'gotyou!' * 100
71
+ expect(leaked_password.to_unprotected).to eq "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
68
72
  ensure
69
73
  ENV.delete('hpc_user_for_test_credential')
70
74
  ENV.delete('hpc_password_for_test_credential')
@@ -96,7 +100,7 @@ describe HybridPlatformsConductor::Credentials do
96
100
  end
97
101
  end
98
102
 
99
- it 'erases the value of the password taken from netrc' do
103
+ it 'erases the value of the password taken from netrc after usage' do
100
104
  with_platforms '' do
101
105
  netrc_data = [['mocked_data']]
102
106
  expect(::Netrc).to receive(:read) do
@@ -111,7 +115,7 @@ describe HybridPlatformsConductor::Credentials do
111
115
  leaked_password = password
112
116
  end
113
117
  end
114
- expect(leaked_password).to eq 'gotyou!' * 100
118
+ expect(leaked_password).to eq "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
115
119
  expect(netrc_data).to eq [['GotYou!!!' * 100]]
116
120
  end
117
121
  end
@@ -100,9 +100,9 @@ module HybridPlatformsConductorTest
100
100
  # [API] - @stderr_io can be used to send stderr output
101
101
  #
102
102
  # Parameters::
103
- # * *bash_cmds* (String): Bash commands to execute
103
+ # * *bash_cmds* (String or SecretString): Bash commands to execute. Use #to_unprotected to access the real content (otherwise secrets are obfuscated).
104
104
  def remote_bash(bash_cmds)
105
- @calls << [:remote_bash, bash_cmds]
105
+ @calls << [:remote_bash, bash_cmds.to_unprotected.clone]
106
106
  @remote_bash_code&.call(@stdout_io, @stderr_io, self)
107
107
  end
108
108
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hybrid_platforms_conductor
3
3
  version: !ruby/object:Gem::Version
4
- version: 33.4.0
4
+ version: 33.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-05 00:00:00.000000000 Z
11
+ date: 2021-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: range_operators
@@ -276,6 +276,20 @@ dependencies:
276
276
  - - "~>"
277
277
  - !ruby/object:Gem::Version
278
278
  version: '1.0'
279
+ - !ruby/object:Gem::Dependency
280
+ name: secret_string
281
+ requirement: !ruby/object:Gem::Requirement
282
+ requirements:
283
+ - - "~>"
284
+ - !ruby/object:Gem::Version
285
+ version: '1.1'
286
+ type: :runtime
287
+ prerelease: false
288
+ version_requirements: !ruby/object:Gem::Requirement
289
+ requirements:
290
+ - - "~>"
291
+ - !ruby/object:Gem::Version
292
+ version: '1.1'
279
293
  - !ruby/object:Gem::Dependency
280
294
  name: rspec
281
295
  requirement: !ruby/object:Gem::Requirement