@@ -762,4 +762,244 @@ class AnsibleRunnerSpec extends Specification{
762762 yaml. contains(" host_users:" )
763763 }
764764
765+ def " escapePasswordForYaml: should escape special characters" () {
766+ given :
767+ def builder = AnsibleRunner . playbookInline(" test" )
768+ builder. customTmpDirPath(" /tmp" )
769+ def runner = builder. build()
770+
771+ expect :
772+ runner. escapePasswordForYaml(password) == expectedResult
773+
774+ where :
775+ password || expectedResult
776+ " simple" || " simple"
777+ " p@ssword" || " p@ssword"
778+ " pass:word" || " pass:word"
779+ " pass#word" || " pass#word"
780+ ' pass"word' || ' pass\\ "word'
781+ ' pass\\ word' || ' pass\\\\ word'
782+ ' pass\n word' || ' pass\\ nword'
783+ ' pass\r word' || ' pass\\ rword'
784+ ' pass"\\ word' || ' pass\\ "\\\\ word'
785+ " p@ss:w#rd!" || " p@ss:w#rd!"
786+ ' "\\ ' || ' \\ "\\\\ '
787+ " " || " "
788+ }
789+
790+ def " escapePasswordForYaml: should handle complex passwords" () {
791+ given :
792+ def builder = AnsibleRunner . playbookInline(" test" )
793+ builder. customTmpDirPath(" /tmp" )
794+ def runner = builder. build()
795+
796+ when :
797+ def result = runner. escapePasswordForYaml(' My"P@ss\\ word\n 2024' )
798+
799+ then :
800+ result == ' My\\ "P@ss\\\\ word\\ n2024'
801+ }
802+
803+ def " ssh password with special characters should be escaped and quoted" () {
804+ given :
805+ String playbook = " test"
806+ String password = ' p@ss"word:123'
807+
808+ def runnerBuilder = AnsibleRunner . playbookInline(playbook)
809+ runnerBuilder. sshPass(password)
810+ runnerBuilder. sshUser(" user" )
811+ runnerBuilder. sshUsePassword(true )
812+
813+ def process = Mock (Process ) {
814+ waitFor() >> 0
815+ getInputStream() >> new ByteArrayInputStream (" " . getBytes())
816+ getOutputStream() >> new ByteArrayOutputStream ()
817+ getErrorStream() >> new ByteArrayInputStream (" " . getBytes())
818+ }
819+
820+ def processExecutor = Mock (ProcessExecutor ) {
821+ run() >> process
822+ }
823+
824+ List<String > capturedArgs = null
825+ ProcessExecutor.ProcessExecutorBuilder processBuilder = Mock (ProcessExecutor.ProcessExecutorBuilder )
826+
827+ // Configure fluent builder responses
828+ processBuilder. build() >> processExecutor
829+ processBuilder. procArgs(_ as List ) >> { List args ->
830+ capturedArgs = new ArrayList (args)
831+ return processBuilder
832+ }
833+ processBuilder. environmentVariables(_ as Map ) >> { Map e -> return processBuilder }
834+ processBuilder. baseDirectory(_ as File ) >> { File f -> return processBuilder }
835+ processBuilder. stdinVariables(_ as List ) >> { List v -> return processBuilder }
836+ processBuilder. promptStdinLogFile(_ as File ) >> { File f -> return processBuilder }
837+ processBuilder. debug(_ as boolean ) >> { boolean d -> return processBuilder }
838+
839+ def ansibleVault = Mock (AnsibleVault ) {
840+ checkAnsibleVault() >> true
841+ getVaultPasswordScriptFile() >> new File (" vault-script-client.py" )
842+ }
843+
844+ runnerBuilder. processExecutorBuilder(processBuilder)
845+ runnerBuilder. ansibleVault(ansibleVault)
846+
847+ when :
848+ AnsibleRunner runner = runnerBuilder. build()
849+ runner. setCustomTmpDirPath(" /tmp" )
850+ def result = runner. run()
851+
852+ then :
853+ result == 0
854+ 1 * ansibleVault. encryptVariable(_, _) >> " !vault | value"
855+ capturedArgs != null
856+ // Flatten and verify --extra-vars argument exists
857+ def flatArgs = capturedArgs. flatten(). collect { it?. toString() }
858+ flatArgs. any { it. contains(" --extra-vars" ) }
859+ }
860+
861+ def " become password with special characters should be escaped and quoted" () {
862+ given :
863+ String playbook = " test"
864+ String becomePass = ' admin"pass\\ 123'
865+
866+ def runnerBuilder = AnsibleRunner . playbookInline(playbook)
867+ runnerBuilder. become(true )
868+ runnerBuilder. becomeUser(" root" )
869+ runnerBuilder. becomePassword(becomePass)
870+ runnerBuilder. becomeMethod(" sudo" )
871+
872+ def process = Mock (Process ) {
873+ waitFor() >> 0
874+ getInputStream() >> new ByteArrayInputStream (" " . getBytes())
875+ getOutputStream() >> new ByteArrayOutputStream ()
876+ getErrorStream() >> new ByteArrayInputStream (" " . getBytes())
877+ }
878+
879+ def processExecutor = Mock (ProcessExecutor ) {
880+ run() >> process
881+ }
882+
883+ List<String > capturedArgs = null
884+ ProcessExecutor.ProcessExecutorBuilder processBuilder = Mock (ProcessExecutor.ProcessExecutorBuilder )
885+
886+ // Configure fluent builder responses
887+ processBuilder. build() >> processExecutor
888+ processBuilder. procArgs(_ as List ) >> { List args ->
889+ capturedArgs = new ArrayList (args)
890+ return processBuilder
891+ }
892+ processBuilder. environmentVariables(_ as Map ) >> { Map e -> return processBuilder }
893+ processBuilder. baseDirectory(_ as File ) >> { File f -> return processBuilder }
894+ processBuilder. stdinVariables(_ as List ) >> { List v -> return processBuilder }
895+ processBuilder. promptStdinLogFile(_ as File ) >> { File f -> return processBuilder }
896+ processBuilder. debug(_ as boolean ) >> { boolean d -> return processBuilder }
897+
898+ def ansibleVault = Mock (AnsibleVault ) {
899+ checkAnsibleVault() >> true
900+ getVaultPasswordScriptFile() >> new File (" vault-script-client.py" )
901+ }
902+
903+ runnerBuilder. processExecutorBuilder(processBuilder)
904+ runnerBuilder. ansibleVault(ansibleVault)
905+
906+ when :
907+ AnsibleRunner runner = runnerBuilder. build()
908+ runner. setCustomTmpDirPath(" /tmp" )
909+ def result = runner. run()
910+
911+ then :
912+ result == 0
913+ 1 * ansibleVault. encryptVariable(_, _) >> " !vault | value"
914+ capturedArgs != null
915+ // Flatten and verify arguments
916+ def flatArgs = capturedArgs. flatten(). collect { it?. toString() }
917+ flatArgs. contains(" --become" )
918+ flatArgs. any { it. contains(" --extra-vars" ) }
919+ }
920+
921+ def " escapePasswordForYaml: should preserve passwords without special chars" () {
922+ given :
923+ def builder = AnsibleRunner . playbookInline(" test" )
924+ builder. customTmpDirPath(" /tmp" )
925+ def runner = builder. build()
926+
927+ when :
928+ def result = runner. escapePasswordForYaml(" simplePassword123" )
929+
930+ then :
931+ result == " simplePassword123"
932+ }
933+
934+ def " escapePasswordForYaml: should handle multiple escape sequences" () {
935+ given :
936+ def builder = AnsibleRunner . playbookInline(" test" )
937+ builder. customTmpDirPath(" /tmp" )
938+ def runner = builder. build()
939+
940+ when :
941+ // Password with quote, backslash, and newline
942+ def result = runner. escapePasswordForYaml(' test"\\ pass\n ' )
943+
944+ then :
945+ // Each special char should be escaped
946+ result == ' test\\ "\\\\ pass\\ n'
947+ }
948+
949+ def " both ssh and become passwords with special chars should work together" () {
950+ given :
951+ String playbook = " test"
952+ String sshPass = ' ssh"pass:123'
953+ String becomePass = ' sudo\\ pass#456'
954+
955+ def runnerBuilder = AnsibleRunner . playbookInline(playbook)
956+ runnerBuilder. sshPass(sshPass)
957+ runnerBuilder. sshUser(" user" )
958+ runnerBuilder. sshUsePassword(true )
959+ runnerBuilder. become(true )
960+ runnerBuilder. becomeUser(" root" )
961+ runnerBuilder. becomePassword(becomePass)
962+ runnerBuilder. becomeMethod(" sudo" )
963+
964+ def process = Mock (Process ) {
965+ waitFor() >> 0
966+ getInputStream() >> new ByteArrayInputStream (" " . getBytes())
967+ getOutputStream() >> new ByteArrayOutputStream ()
968+ getErrorStream() >> new ByteArrayInputStream (" " . getBytes())
969+ }
970+
971+ def processExecutor = Mock (ProcessExecutor ) {
972+ run() >> process
973+ }
974+
975+ ProcessExecutor.ProcessExecutorBuilder processBuilder = Mock (ProcessExecutor.ProcessExecutorBuilder )
976+
977+ // Configure fluent builder responses
978+ processBuilder. build() >> processExecutor
979+ processBuilder. procArgs(_ as List ) >> { List args -> return processBuilder }
980+ processBuilder. environmentVariables(_ as Map ) >> { Map e -> return processBuilder }
981+ processBuilder. baseDirectory(_ as File ) >> { File f -> return processBuilder }
982+ processBuilder. stdinVariables(_ as List ) >> { List v -> return processBuilder }
983+ processBuilder. promptStdinLogFile(_ as File ) >> { File f -> return processBuilder }
984+ processBuilder. debug(_ as boolean ) >> { boolean d -> return processBuilder }
985+
986+ def ansibleVault = Mock (AnsibleVault ) {
987+ checkAnsibleVault() >> true
988+ getVaultPasswordScriptFile() >> new File (" vault-script-client.py" )
989+ }
990+
991+ runnerBuilder. processExecutorBuilder(processBuilder)
992+ runnerBuilder. ansibleVault(ansibleVault)
993+
994+ when :
995+ AnsibleRunner runner = runnerBuilder. build()
996+ runner. setCustomTmpDirPath(" /tmp" )
997+ def result = runner. run()
998+
999+ then :
1000+ result == 0
1001+ // Both passwords should be encrypted
1002+ 2 * ansibleVault. encryptVariable(_, _) >> " !vault | value"
1003+ }
1004+
7651005}
0 commit comments