Skip to content

Commit d6fd35a

Browse files
committed
Added unit tests for passwords with special characters
1 parent 22d8b74 commit d6fd35a

1 file changed

Lines changed: 240 additions & 0 deletions

File tree

src/test/groovy/com/rundeck/plugins/ansible/ansible/AnsibleRunnerSpec.groovy

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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\nword' || 'pass\\nword'
783+
'pass\rword' || '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\n2024')
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

Comments
 (0)