require 'open3' describe HybridPlatformsConductor::Deployer do context 'when checking secrets_reader plugins' do context 'with keepass' do # Expect some calls to be done on KPScript # # Parameters:: # * *expected_calls* (Array<[String, String or Hash]>): The list of calls and their corresponding mocked response: # * String: Mocked stdout # * Hash: More complete structure defining the mocked response: # * *exit_status* (Integer): The command exit status [default: 0] # * *stdout* (String): The command stdout # * *xml* (String or nil): XML document to generate as an export, or nil for none [default: nil] def expect_calls_to_kpscript(expected_calls) if expected_calls.empty? expect(Open3).not_to receive(:popen3) else expect(Open3).to receive(:popen3).exactly(expected_calls.size).times do |cmd, &block| expected_call, mocked_call = expected_calls.shift if expected_call.is_a?(Regexp) expect(cmd).to match expected_call else expect(cmd).to eq expected_call end mocked_call = { stdout: mocked_call } if mocked_call.is_a?(String) mocked_call[:exit_status] = 0 unless mocked_call.key?(:exit_status) wait_thr_double = instance_double(Process::Waiter) allow(wait_thr_double).to receive(:value) do wait_thr_value_double = instance_double(Process::Status) allow(wait_thr_value_double).to receive(:exitstatus) do mocked_call[:exit_status] end wait_thr_value_double end if mocked_call[:xml] xml_file = cmd.match(/-OutFile:"([^"]+)"/)[1] logger.debug "Mock KPScript XML file #{xml_file} with\n#{mocked_call[:xml]}" File.write(xml_file, mocked_call[:xml]) end block.call( StringIO.new, StringIO.new(mocked_call[:stdout]), StringIO.new, wait_thr_double ) end end end # Setup a platform for tests # # Parameters:: # * *additional_config* (String): Additional config # * *platform_info* (Hash): Platform configuration [default: 1 node having 1 service] # * *mock_keepass_password* (String): Password to be returned by credentials [default: 'test_keepass_password'] # * *expect_key_file* (String or nil): Key file to be expected, or nil if none [default: nil] # * *expect_password_enc* (String or nil): Encrypted password to be expected, or nil if none [default: nil] # * *expect_kpscript_calls* (Boolean): Should we expect calls to KPScript? [default: true] def with_test_platform_for_keepass_test( additional_config, platform_info: { nodes: { 'node' => { services: %w[service] } }, deployable_services: %w[service] }, mock_keepass_password: 'test_keepass_password', mock_databases: { '/path/to/database.kdbx' => xml_single_entry }, expect_key_file: nil, expect_password_enc: nil, expect_kpscript_calls: true ) with_test_platform( platform_info, additional_config: "read_secrets_from :keepass\n#{additional_config}" ) do mock_databases.each do |database, _xml| expect(test_deployer.instance_variable_get(:@secrets_readers)[:keepass]).to receive(:with_credentials_for).with(:keepass, resource: database) do |_id, resource: nil, &client_code| client_code.call nil, mock_keepass_password end end if expect_kpscript_calls expect_calls_to_kpscript( mock_databases.map do |database, xml| [ %r{/path/to/kpscript "#{Regexp.escape(database)}"#{mock_keepass_password.nil? ? '' : " -pw:\"#{Regexp.escape(mock_keepass_password)}\""}#{expect_password_enc.nil? ? '' : " -pw-enc:\"#{Regexp.escape(expect_password_enc)}\""}#{expect_key_file.nil? ? '' : " -keyfile:\"#{Regexp.escape(expect_key_file)}\""} -c:Export -Format:"KeePass XML \(2.x\)" -OutFile:"/tmp/.+"}, { stdout: 'OK: Operation completed successfully.', xml: xml } ] end ) end yield end end # Expect secrets to be set to given values # # Parameters:: # * *expected_secrets* (Hash): Expected secrets def expect_secrets_to_be(expected_secrets) expect(test_services_handler).to receive(:package).with( services: { 'node' => %w[service] }, secrets: expected_secrets, local_environment: false ) { raise 'Abort as testing secrets is enough' } expect { test_deployer.deploy_on(%w[node]) }.to raise_error 'Abort as testing secrets is enough' end let(:xml_single_entry) do <<~EO_XML Iv3JjMzpPEaijOB+SFZpRw== Password TestPassword Title Test Secret UserName Test User Name EO_XML end it 'gets secrets from a KeePass database with password' do with_test_platform_for_keepass_test( <<~EO_CONFIG use_kpscript_from '/path/to/kpscript' secrets_from_keepass(database: '/path/to/database.kdbx') EO_CONFIG ) do expect_secrets_to_be('Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' }) end end it 'gets secrets from a KeePass database with password and key file' do with_test_platform_for_keepass_test( <<~EO_CONFIG, use_kpscript_from '/path/to/kpscript' secrets_from_keepass(database: '/path/to/database.kdbx') EO_CONFIG expect_key_file: '/path/to/database.key' ) do ENV['hpc_key_file_for_keepass'] = '/path/to/database.key' expect_secrets_to_be('Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' }) end end it 'gets secrets from a KeePass database with encrypted password' do with_test_platform_for_keepass_test( <<~EO_CONFIG, use_kpscript_from '/path/to/kpscript' secrets_from_keepass(database: '/path/to/database.kdbx') EO_CONFIG mock_keepass_password: nil, expect_password_enc: 'PASSWORD_ENC' ) do ENV['hpc_password_enc_for_keepass'] = 'PASSWORD_ENC' expect_secrets_to_be('Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' }) end end it 'gets secrets from a KeePass database with encrypted password and key file' do with_test_platform_for_keepass_test( <<~EO_CONFIG, use_kpscript_from '/path/to/kpscript' secrets_from_keepass(database: '/path/to/database.kdbx') EO_CONFIG mock_keepass_password: nil, expect_password_enc: 'PASSWORD_ENC', expect_key_file: '/path/to/database.key' ) do ENV['hpc_password_enc_for_keepass'] = 'PASSWORD_ENC' ENV['hpc_key_file_for_keepass'] = '/path/to/database.key' expect_secrets_to_be('Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' }) end end it 'gets secrets from a KeePass database with key file' do with_test_platform_for_keepass_test( <<~EO_CONFIG, use_kpscript_from '/path/to/kpscript' secrets_from_keepass(database: '/path/to/database.kdbx') EO_CONFIG mock_keepass_password: nil, expect_key_file: '/path/to/database.key' ) do ENV['hpc_key_file_for_keepass'] = '/path/to/database.key' expect_secrets_to_be('Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' }) end end it 'fails to get secrets from a KeePass database when no authentication mechanisms are provided' do with_test_platform_for_keepass_test( <<~EO_CONFIG, use_kpscript_from '/path/to/kpscript' secrets_from_keepass(database: '/path/to/database.kdbx') EO_CONFIG mock_keepass_password: nil, expect_kpscript_calls: false ) do expect { test_deployer.deploy_on(%w[node]) }.to raise_error 'Please specify at least one of password, password_enc or key_file arguments' end end it 'fails to get secrets if KPScript is not configured' do with_test_platform_for_keepass_test( <<~EO_CONFIG, secrets_from_keepass(database: '/path/to/database.kdbx') EO_CONFIG mock_databases: {}, expect_kpscript_calls: false ) do expect { test_deployer.deploy_on(%w[node]) }.to raise_error 'Missing KPScript configuration. Please use use_kpscript_from to set it.' end end it 'gets secrets from KeePass groups' do with_test_platform_for_keepass_test( <<~EO_CONFIG, use_kpscript_from '/path/to/kpscript' secrets_from_keepass(database: '/path/to/database.kdbx') EO_CONFIG mock_databases: { '/path/to/database.kdbx' => <<~EO_XML Password TestPassword0 Title Secret 0 Group1 Password TestPassword1 Title Secret 1 Group2 Password TestPassword2 Title Secret 2 Group3 Password TestPassword3 Title Secret 3 EO_XML } ) do expect_secrets_to_be( 'Secret 0' => { 'password' => 'TestPassword0' }, 'Group1' => { 'Secret 1' => { 'password' => 'TestPassword1' }, 'Group2' => { 'Secret 2' => { 'password' => 'TestPassword2' } }, 'Group3' => { 'Secret 3' => { 'password' => 'TestPassword3' } } } ) end end it 'gets secrets with attachments' do with_test_platform_for_keepass_test( <<~EO_CONFIG, use_kpscript_from '/path/to/kpscript' secrets_from_keepass(database: '/path/to/database.kdbx') EO_CONFIG mock_databases: { '/path/to/database.kdbx' => <<~EO_XML #{ str = StringIO.new gz = Zlib::GzipWriter.new(str) gz.write('File 0 Content') gz.close Base64.encode64(str.string).strip } #{Base64.encode64('File 1 Content').strip} Password TestPassword0 Title Secret 0 file0.txt Group1 Password TestPassword1 Title Secret 1 file1.txt EO_XML } ) do expect_secrets_to_be( 'Secret 0' => { 'file0.txt' => 'File 0 Content', 'password' => 'TestPassword0' }, 'Group1' => { 'Secret 1' => { 'file1.txt' => 'File 1 Content', 'password' => 'TestPassword1' } } ) end end it 'gets secrets from a KeePass database for several nodes' do with_test_platform_for_keepass_test( <<~EO_CONFIG, use_kpscript_from '/path/to/kpscript' secrets_from_keepass(database: '/path/to/database.kdbx') EO_CONFIG platform_info: { nodes: { 'node1' => { services: %w[service1] }, 'node2' => { services: %w[service2] } }, deployable_services: %w[service1 service2] } ) do expect(test_services_handler).to receive(:package).with( services: { 'node1' => %w[service1], 'node2' => %w[service2] }, secrets: { 'Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' } }, local_environment: false ) { raise 'Abort as testing secrets is enough' } expect { test_deployer.deploy_on(%w[node1 node2]) }.to raise_error 'Abort as testing secrets is enough' end end it 'gets secrets from a KeePass database for several databases' do with_test_platform_for_keepass_test( <<~EO_CONFIG, use_kpscript_from '/path/to/kpscript' secrets_from_keepass(database: '/path/to/database1.kdbx') for_nodes('node2') do secrets_from_keepass(database: '/path/to/database2.kdbx') end EO_CONFIG platform_info: { nodes: { 'node1' => { services: %w[service1] }, 'node2' => { services: %w[service2] } }, deployable_services: %w[service1 service2] }, mock_databases: { '/path/to/database1.kdbx' => xml_single_entry, '/path/to/database2.kdbx' => <<~EO_XML Iv3JjMzpPEaijOB+SFZpRw== Password TestPassword2 Title Test Secret 2 UserName Test User Name 2 EO_XML } ) do expect(test_services_handler).to receive(:package).with( services: { 'node1' => %w[service1], 'node2' => %w[service2] }, secrets: { 'Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' }, 'Test Secret 2' => { 'password' => 'TestPassword2', 'user_name' => 'Test User Name 2' } }, local_environment: false ) { raise 'Abort as testing secrets is enough' } expect { test_deployer.deploy_on(%w[node1 node2]) }.to raise_error 'Abort as testing secrets is enough' end end it 'gets secrets from a group path in a KeePass database' do with_test_platform_for_keepass_test( <<~EO_CONFIG, use_kpscript_from '/path/to/kpscript' secrets_from_keepass( database: '/path/to/database.kdbx', group_path: %w[Group1 Group2 Group3] ) EO_CONFIG expect_kpscript_calls: false ) do expect_calls_to_kpscript [ [ %r{/path/to/kpscript "/path/to/database.kdbx" -pw:"test_keepass_password" -c:Export -Format:"KeePass XML \(2.x\)" -OutFile:"/tmp/.+" -GroupPath:"Group1/Group2/Group3"}, { stdout: 'OK: Operation completed successfully.', xml: xml_single_entry } ] ] expect_secrets_to_be('Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' }) end end it 'merges secrets from several KeePass databases' do with_test_platform_for_keepass_test( <<~EO_CONFIG, use_kpscript_from '/path/to/kpscript' secrets_from_keepass(database: '/path/to/database1.kdbx') for_nodes('node2') do secrets_from_keepass(database: '/path/to/database2.kdbx') end EO_CONFIG platform_info: { nodes: { 'node1' => { services: %w[service1] }, 'node2' => { services: %w[service2] } }, deployable_services: %w[service1 service2] }, mock_databases: { '/path/to/database1.kdbx' => <<~EO_XML, Iv3JjMzpPEaijOB+SFZpRw== Password TestPassword1 Title Test Secret 1 UserName Test User Name 1 RsonCc3VHk+k85z5zHhZzQ== Group1 Iv3JjMzpPEaijOB+SFZpRw== Password TestPassword3 Title Test Secret 3 UserName Test User Name 3 EO_XML '/path/to/database2.kdbx' => <<~EO_XML Iv3JjMzpPEaijOB+SFZpRw== Password TestPassword2 Title Test Secret 2 UserName Test User Name 2 RsonCc3VHk+k85z5zHhZzQ== Group1 Iv3JjMzpPEaijOB+SFZpRw== Password TestPassword3 Title Test Secret 3 Notes Notes 3 Iv3JjMzpPEaijOB+SFZpRw== Password TestPassword4 Title Test Secret 4 UserName Test User Name 4 EO_XML } ) do expect(test_services_handler).to receive(:package).with( services: { 'node1' => %w[service1], 'node2' => %w[service2] }, secrets: { 'Test Secret 1' => { 'password' => 'TestPassword1', 'user_name' => 'Test User Name 1' }, 'Test Secret 2' => { 'password' => 'TestPassword2', 'user_name' => 'Test User Name 2' }, 'Group1' => { 'Test Secret 3' => { 'password' => 'TestPassword3', 'user_name' => 'Test User Name 3', 'notes' => 'Notes 3' }, 'Test Secret 4' => { 'password' => 'TestPassword4', 'user_name' => 'Test User Name 4' } } }, local_environment: false ) { raise 'Abort as testing secrets is enough' } expect { test_deployer.deploy_on(%w[node1 node2]) }.to raise_error 'Abort as testing secrets is enough' end end it 'fails in case of secrets conflicts between several KeePass databases' do with_test_platform_for_keepass_test( <<~EO_CONFIG, use_kpscript_from '/path/to/kpscript' secrets_from_keepass(database: '/path/to/database1.kdbx') for_nodes('node2') do secrets_from_keepass(database: '/path/to/database2.kdbx') end EO_CONFIG platform_info: { nodes: { 'node1' => { services: %w[service1] }, 'node2' => { services: %w[service2] } }, deployable_services: %w[service1 service2] }, mock_databases: { '/path/to/database1.kdbx' => <<~EO_XML, Iv3JjMzpPEaijOB+SFZpRw== Password TestPassword1 Title Test Secret 1 UserName Test User Name 1 EO_XML '/path/to/database2.kdbx' => <<~EO_XML Iv3JjMzpPEaijOB+SFZpRw== Password OtherTestPassword1 Title Test Secret 1 UserName Test User Name 1 EO_XML } ) do expect { test_deployer.deploy_on(%w[node1 node2]) }.to raise_error 'Secret set at path Test Secret 1->password by /path/to/database2.kdbx for service service2 on node node2 has conflicting values (set debug for value details).' end end end end end