@@ -860,6 +860,11 @@ def __init__(self, host: MultihostHost) -> None:
860860 self .opts = "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
861861 """SSH CLI options."""
862862
863+ self .passwd : SSHPasswdUtils = SSHPasswdUtils (host )
864+ """
865+ Change password via SSH session using passwd command.
866+ """
867+
863868 def password_with_output (
864869 self , username : str , password : str , hostname : str = "localhost"
865870 ) -> tuple [int , int , str , str ]:
@@ -1076,6 +1081,177 @@ def password_expired(self, username: str, password: str, new_password: str, host
10761081 return rc == 0
10771082
10781083
1084+ class SSHPasswdUtils (MultihostUtility [MultihostHost ]):
1085+ """
1086+ Change password via SSH session using passwd command.
1087+
1088+ SSHs as user, logs in, runs ``passwd`` interactively, and
1089+ changes the password. Used when testing krb5_child
1090+ "Initial authentication for change password".
1091+ """
1092+
1093+ def __init__ (self , host : MultihostHost ) -> None :
1094+ """
1095+ :param host: Multihost host.
1096+ :type host: MultihostHost
1097+ """
1098+ super ().__init__ (host )
1099+
1100+ self .opts = (
1101+ "-o UserKnownHostsFile=/dev/null"
1102+ " -o StrictHostKeyChecking=no"
1103+ )
1104+
1105+ def password_with_output (
1106+ self ,
1107+ username : str ,
1108+ password : str ,
1109+ new_password : str ,
1110+ retyped : str | None = None ,
1111+ * ,
1112+ hostname : str = "localhost" ,
1113+ ) -> tuple [int , int , str , str ]:
1114+ """
1115+ SSH to host, run passwd, and change password.
1116+
1117+ :param username: Username.
1118+ :type username: str
1119+ :param password: Current password.
1120+ :type password: str
1121+ :param new_password: New password.
1122+ :type new_password: str
1123+ :param retyped: Retyped new password (defaults to new_password).
1124+ :type retyped: str | None, optional
1125+ :param hostname: SSH target host, defaults to "localhost".
1126+ :type hostname: str
1127+ :return: Tuple containing [return code, command code, stdout, stderr].
1128+ :rtype: Tuple[int, int, str, str]
1129+ """
1130+ if retyped is None :
1131+ retyped = new_password
1132+
1133+ result = self .host .conn .expect_nobody (
1134+ rf"""
1135+ exp_internal 0
1136+
1137+ proc exitmsg {{ msg code }} {{
1138+ catch close
1139+ lassign [wait] pid spawnid os_error_flag rc
1140+
1141+ puts ""
1142+ puts "expect result: $msg"
1143+ puts "expect exit code: $code"
1144+ puts "expect spawn exit code: $rc"
1145+ exit $code
1146+ }}
1147+
1148+ set timeout { DEFAULT_AUTHENTICATION_TIMEOUT }
1149+ set prompt "\n.*\[#\$>\] $"
1150+
1151+ spawn ssh { self .opts } \
1152+ -o PreferredAuthentications=password \
1153+ -o NumberOfPasswordPrompts=1 \
1154+ -l "{ username } " "{ hostname } "
1155+
1156+ expect {{
1157+ "password:" {{send "{ password } \n"}}
1158+ timeout {{exitmsg "Unexpected output" 201}}
1159+ eof {{exitmsg "Unexpected end of file" 202}}
1160+ }}
1161+
1162+ expect {{
1163+ -re $prompt {{}}
1164+ timeout {{exitmsg "Unexpected output" 201}}
1165+ eof {{exitmsg "Unexpected end of file" 202}}
1166+ }}
1167+
1168+ send "passwd\r"
1169+
1170+ expect {{
1171+ -nocase "Current Password:" {{send "{ password } \n"}}
1172+ timeout {{exitmsg "Unexpected output" 201}}
1173+ eof {{exitmsg "Unexpected end of file" 202}}
1174+ }}
1175+
1176+ expect {{
1177+ -nocase "New password:" {{send "{ new_password } \n"}}
1178+ timeout {{exitmsg "Unexpected output" 201}}
1179+ eof {{exitmsg "Unexpected end of file" 202}}
1180+ }}
1181+
1182+ expect {{
1183+ -nocase "Retype new password:" {{send "{ retyped } \n"}}
1184+ timeout {{exitmsg "Unexpected output" 201}}
1185+ eof {{exitmsg "Unexpected end of file" 202}}
1186+ }}
1187+
1188+ expect {{
1189+ -re "passwd: .+ updated successfully" {{
1190+ send "exit\r"
1191+ expect eof
1192+ exitmsg "Password change was successful" 0
1193+ }}
1194+ "Sorry, passwords do not match." {{
1195+ exitmsg "Passwords do not match" 1
1196+ }}
1197+ "Password change failed." {{
1198+ exitmsg "Password change failed" 1
1199+ }}
1200+ timeout {{exitmsg "Unexpected output" 201}}
1201+ eof {{exitmsg "Unexpected end of file" 202}}
1202+ }}
1203+
1204+ exitmsg "Unexpected code path" 203
1205+ """ ,
1206+ verbose = False ,
1207+ )
1208+
1209+ if result .rc > 200 :
1210+ raise ExpectScriptError (result .rc )
1211+
1212+ expect_data = result .stdout_lines [- 3 :]
1213+
1214+ # Get command exit code.
1215+ cmdrc = int (expect_data [2 ].split (":" )[1 ].strip ())
1216+
1217+ # Alter stdout, first line is spawned command,
1218+ # the last three are our expect output.
1219+ stdout = "\n " .join (result .stdout_lines [1 :- 3 ])
1220+
1221+ return result .rc , cmdrc , stdout , result .stderr
1222+
1223+ def password (
1224+ self ,
1225+ username : str ,
1226+ password : str ,
1227+ new_password : str ,
1228+ retyped : str | None = None ,
1229+ * ,
1230+ hostname : str = "localhost" ,
1231+ ) -> bool :
1232+ """
1233+ SSH to host, run passwd, and change password.
1234+
1235+ :param username: Username.
1236+ :type username: str
1237+ :param password: Current password.
1238+ :type password: str
1239+ :param new_password: New password.
1240+ :type new_password: str
1241+ :param retyped: Retyped new password (defaults to new_password).
1242+ :type retyped: str | None, optional
1243+ :param hostname: SSH target host, defaults to "localhost".
1244+ :type hostname: str
1245+ :return: True if password change succeeded, False otherwise.
1246+ :rtype: bool
1247+ """
1248+ rc , _ , _ , _ = self .password_with_output (
1249+ username , password , new_password , retyped ,
1250+ hostname = hostname ,
1251+ )
1252+ return rc == 0
1253+
1254+
10791255class SudoAuthenticationUtils (MultihostUtility [MultihostHost ]):
10801256 """
10811257 Methods for testing authentication and authorization via sudo.
@@ -1510,6 +1686,83 @@ def list_tgt_times(self, realm: str) -> tuple[datetime, datetime]:
15101686
15111687 raise Exception ("TGT was not found" )
15121688
1689+ def ktutil_create_mixed_keytab (
1690+ self ,
1691+ wrong_principal : str ,
1692+ valid_keytab : str ,
1693+ output_keytab : str ,
1694+ password : str = "Secret123" ,
1695+ * ,
1696+ raise_on_error : bool = True ,
1697+ ) -> ProcessResult :
1698+ """
1699+ Create keytab with wrong principal first, then entries from valid keytab.
1700+
1701+ BZ 805281: Uses ktutil to add a password-based entry (wrong realm) first,
1702+ then merge with an existing keytab. Tests that SSSD selects the correct
1703+ principal when multiple realms exist in one keytab.
1704+
1705+ :param wrong_principal: Principal to add first (e.g. nfs/host@TEST.EXAMPLE.COM)
1706+ :param valid_keytab: Path to keytab with correct principal
1707+ :param output_keytab: Path for the combined keytab output
1708+ :param password: Password for addent -password, defaults to "Secret123"
1709+ :param raise_on_error: Raise on failure, defaults to True
1710+ :return: Process result from expect
1711+ """
1712+ return self .host .conn .expect (
1713+ f"""
1714+ spawn ktutil
1715+ expect "ktutil: "
1716+ send "addent -password -p { wrong_principal } -k 3 -e rc4-hmac\\ r"
1717+ expect "Password: *"
1718+ send "{ password } \\ r"
1719+ send "rkt { valid_keytab } \\ r"
1720+ send "wkt { output_keytab } \\ r"
1721+ expect eof
1722+ """ ,
1723+ raise_on_error = raise_on_error ,
1724+ )
1725+
1726+ def ktutil_create_keytab (
1727+ self ,
1728+ principal : str ,
1729+ output_keytab : str ,
1730+ password : str = "Secret123" ,
1731+ enctype : str = "aes256-cts-hmac-sha1-96" ,
1732+ kvno : int = 1 ,
1733+ * ,
1734+ raise_on_error : bool = True ,
1735+ ) -> ProcessResult :
1736+ """
1737+ Create keytab with single password-based entry (BZ 1198478).
1738+
1739+ Uses ktutil to add a principal with password and write to keytab file.
1740+ Useful for dummy keytabs (principal in keytab but not on KDC).
1741+
1742+ :param principal: Principal name (e.g. bla@EXAMPLE.COM)
1743+ :param output_keytab: Path for the keytab output
1744+ :param password: Password for addent -password, defaults to "Secret123"
1745+ :param enctype: Encryption type (default: aes256-cts-hmac-sha1-96)
1746+ :param kvno: Key version number, defaults to 1
1747+ :param raise_on_error: Raise on failure, defaults to True
1748+ :return: Process result from expect
1749+ """
1750+ return self .host .conn .expect (
1751+ f"""
1752+ spawn ktutil
1753+ expect "ktutil: "
1754+ send "addent -password -p { principal } -k { kvno } -e { enctype } \\ r"
1755+ expect "Password: *"
1756+ send "{ password } \\ r"
1757+ expect "ktutil: "
1758+ send "wkt { output_keytab } \\ r"
1759+ expect "ktutil: "
1760+ send "q\\ r"
1761+ expect eof
1762+ """ ,
1763+ raise_on_error = raise_on_error ,
1764+ )
1765+
15131766 def __enter__ (self ) -> KerberosAuthenticationUtils :
15141767 """
15151768 Connect to the host over ssh if not already connected.
0 commit comments