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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +3 -5
- data/docs/config_dsl.md +7 -5
- data/lib/hybrid_platforms_conductor/bitbucket.rb +2 -2
- data/lib/hybrid_platforms_conductor/cmd_runner.rb +4 -4
- data/lib/hybrid_platforms_conductor/confluence.rb +2 -2
- data/lib/hybrid_platforms_conductor/connector.rb +1 -1
- data/lib/hybrid_platforms_conductor/credentials.rb +20 -12
- data/lib/hybrid_platforms_conductor/github.rb +1 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/action/bash.rb +1 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/action/remote_bash.rb +27 -17
- data/lib/hybrid_platforms_conductor/hpc_plugins/connector/local.rb +4 -2
- data/lib/hybrid_platforms_conductor/hpc_plugins/connector/my_connector.rb.sample +1 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/connector/ssh.rb +29 -20
- data/lib/hybrid_platforms_conductor/hpc_plugins/provisioner/proxmox.rb +1 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/keepass.rb +1 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/jenkins_ci_conf.rb +1 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/jenkins_ci_masters_ok.rb +2 -2
- data/lib/hybrid_platforms_conductor/logger_helpers.rb +17 -0
- data/lib/hybrid_platforms_conductor/thycotic.rb +2 -2
- data/lib/hybrid_platforms_conductor/version.rb +1 -1
- data/spec/hybrid_platforms_conductor_test/api/actions_executor/actions/bash_spec.rb +15 -0
- data/spec/hybrid_platforms_conductor_test/api/actions_executor/actions/remote_bash_spec.rb +32 -0
- data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/local/remote_actions_spec.rb +9 -0
- data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/remote_actions_spec.rb +38 -0
- data/spec/hybrid_platforms_conductor_test/api/cmd_runner_spec.rb +14 -0
- data/spec/hybrid_platforms_conductor_test/api/credentials_spec.rb +8 -4
- data/spec/hybrid_platforms_conductor_test/test_connector.rb +2 -2
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: daf46892dd3b5a79ff0e54f16d6881b80519e8fe926e9952ad7faafd1706c31e
|
4
|
+
data.tar.gz: 84f9ee06afb8141f140a8754d0961217ceb14b3f8d2d1d2577792be420d42aa2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
334
|
-
|
335
|
-
requester.call 'my_bitbucket_name',
|
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
|
-
|
232
|
-
|
233
|
-
requester.call 'MyUserName',
|
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* (
|
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* (
|
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* (
|
69
|
-
# !!! Never
|
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}"]
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
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,
|
@@ -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?(
|
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?(
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
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* (
|
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)
|
@@ -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: [
|
data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/remote_actions_spec.rb
CHANGED
@@ -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
|
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
|
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
|
+
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-
|
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
|