From a3fffd7f4ef1c5f79a2387380b10000157d78cb7 Mon Sep 17 00:00:00 2001 From: zarganum <116151805+zarganum@users.noreply.github.com> Date: Tue, 17 Sep 2024 22:47:20 +0400 Subject: [PATCH] set_password extended server response processing (#53) * set_password bytes result_string * CI bump version * CI revert version * set_password response ADPolicyInfo and MIT chpw_message * isort * result structures, docs and code enums * black format * remove nesting from named tuples * chpw_message locale encoding * revert over-decoding and fixed enum * removed ADPolicyInfo.to_bytes() etc --------- Co-authored-by: zarganum --- setup.py | 1 + src/krb5/__init__.py | 12 +++ src/krb5/_adpi.py | 71 ++++++++++++++++ src/krb5/_chpw_message_mit.pyi | 21 +++++ src/krb5/_chpw_message_mit.pyx | 49 +++++++++++ src/krb5/_set_password.pyi | 52 ++++++++---- src/krb5/_set_password.pyx | 106 +++++++++++++++--------- tests/test_changepw.py | 147 ++++++++++++++++++++++++++++----- 8 files changed, 386 insertions(+), 73 deletions(-) create mode 100644 src/krb5/_adpi.py create mode 100644 src/krb5/_chpw_message_mit.pyi create mode 100644 src/krb5/_chpw_message_mit.pyx diff --git a/setup.py b/setup.py index e6fabdf..be524cb 100755 --- a/setup.py +++ b/setup.py @@ -217,6 +217,7 @@ def get_krb5_lib_path( ("ccache_match", "krb5_cc_cache_match"), ("ccache_support_switch", "krb5_cc_support_switch"), "cccol", + ("chpw_message_mit", "krb5_chpw_message"), "context", ("context_mit", "krb5_init_secure_context"), "creds", diff --git a/src/krb5/__init__.py b/src/krb5/__init__.py index 9056edd..43fcbd9 100644 --- a/src/krb5/__init__.py +++ b/src/krb5/__init__.py @@ -1,6 +1,7 @@ # Copyright: (c) 2021 Jordan Borean (@jborean93) # MIT License (see LICENSE or https://opensource.org/licenses/MIT) +from krb5._adpi import ADPolicyInfo, ADPolicyInfoProp from krb5._ccache import ( CCache, CredentialsRetrieveFlags, @@ -85,12 +86,15 @@ ) from krb5._set_password import ( SetPasswordResult, + SetPasswordResultCode, set_password, set_password_using_ccache, ) from krb5._string import enctype_to_string, string_to_enctype __all__ = [ + "ADPolicyInfo", + "ADPolicyInfoProp", "CCache", "Context", "CredentialsRetrieveFlags", @@ -107,6 +111,7 @@ "PrincipalParseFlags", "PrincipalUnparseFlags", "SetPasswordResult", + "SetPasswordResultCode", "TicketFlags", "TicketTimes", "build_principal", @@ -195,6 +200,13 @@ __all__.append("marshal_credentials") __all__.append("unmarshal_credentials") +try: + from krb5._chpw_message_mit import chpw_message +except ImportError: + pass +else: + __all__.append("chpw_message") + try: from krb5._ccache_match import cc_cache_match diff --git a/src/krb5/_adpi.py b/src/krb5/_adpi.py new file mode 100644 index 0000000..aea5189 --- /dev/null +++ b/src/krb5/_adpi.py @@ -0,0 +1,71 @@ +import enum +import struct +import typing + +FORMAT = "!HIIIQQ" + + +class ADPolicyInfoProp(enum.IntFlag): + COMPLEX = 0x00000001 + NO_ANON_CHANGEv = 0x00000002 + NO_CLEAR_CHANGE = 0x00000004 + LOCKOUT_ADMINS = 0x00000008 + STORE_CLEARTEXT = 0x00000010 + REFUSE_CHANGE = 0x00000020 + + +class ADPolicyInfo(typing.NamedTuple): + """The structure containing the reasons for failed password change attempt. + Should be used to inform the end user how to meet the policy requirements. + This is specific to Active Directory and is returned as the + `server_response` by :meth:`set_password()` and + :meth:`set_password_using_ccache()`. + + When using MIT library, this structure may be encoded back to bytes and + passed to :meth:`chpw_message()` to obtain a human readable response. + With Heimdal, it is required to provide a custom implementation based + on the known fields below. + + The structure contains the following fields:\n + - `properties` - Password policy bit flags (see below) + - `min_length` - Minimal password length + - `history` - Number of passwords that this system remembers + - `max_age` - Maximum password age in 100 nanosecond units + - `min_age` - Minimum password age in 100 nanosecond units + + The only known property flag is `COMPLEX` which means that the password must + meet certain character variety and not contain the user's name. + To convert `max_age` and `min_age` to seconds, divide them by 10,000,000. + """ + + properties: ADPolicyInfoProp + min_length: int + history: int + max_age: int + min_age: int + + @classmethod + def from_bytes(cls, data: bytes) -> "ADPolicyInfo": + """Decode AD policy result from byte string + + Args: + data: Serialized AD policy `server_response` + + Returns: + ADPolicyInfo: Decoded AD policy result strcture + + Raises: + ValueError: Invalid data length or wrong signature + """ + if len(data) != struct.calcsize(FORMAT): + raise ValueError("Invalid data length") + signature, min_length, history, flags, max_age, min_age = struct.unpack(FORMAT, data) + if signature != 0x0000: + raise ValueError("Invalid signature") + return cls( + min_length=min_length, + history=history, + max_age=max_age, + min_age=min_age, + properties=ADPolicyInfoProp(flags), + ) diff --git a/src/krb5/_chpw_message_mit.pyi b/src/krb5/_chpw_message_mit.pyi new file mode 100644 index 0000000..0532c77 --- /dev/null +++ b/src/krb5/_chpw_message_mit.pyi @@ -0,0 +1,21 @@ +from krb5._context import Context + +def chpw_message(context: Context, server_response: bytes) -> bytes: + """This function processes the byte sequence returned as the + `server_response` by :meth:`set_password()` and + :meth:`set_password_using_ccache()` functions, and returns a human readable + byte string. + + Note that `gettext` library is used to translate the strings according + to locale settings. For the list of existing translations, pls. refer + to MIT krb5 source code. Not all translations may be available on your + system. Caller is responsible for decoding the string according to + locale settings. + + Args: + context: Krb5 context. + server_response: The `server_response` bytes received from the KDC. + + Returns: + bytes: The human readable bytes string. + """ diff --git a/src/krb5/_chpw_message_mit.pyx b/src/krb5/_chpw_message_mit.pyx new file mode 100644 index 0000000..0b7f857 --- /dev/null +++ b/src/krb5/_chpw_message_mit.pyx @@ -0,0 +1,49 @@ +import typing + +from krb5._exceptions import Krb5Error + +from libc.string cimport strlen + +from krb5._context cimport Context +from krb5._krb5_types cimport * + + +cdef extern from "python_krb5.h": + krb5_error_code krb5_chpw_message( + krb5_context context, + const krb5_data *server_string, + char **message_out + ) nogil + + void krb5_free_string( + krb5_context context, + char *string + ) nogil + +def chpw_message( + Context context not None, + const unsigned char[:] server_string not None, +) -> bytes: + cdef krb5_error_code err = 0 + cdef krb5_data server_string_raw + cdef char *message_out = NULL + + try: + if len(server_string) == 0: + pykrb5_set_krb5_data(&server_string_raw, 0, "") + else: + pykrb5_set_krb5_data(&server_string_raw, len(server_string), &server_string[0]) + + err = krb5_chpw_message(context.raw, &server_string_raw, &message_out) + + if err: + raise Krb5Error(context, err) + + if message_out is NULL: + return b"" + else: + message_len = strlen(message_out) + return message_out[:message_len] + + finally: + krb5_free_string(context.raw, message_out) \ No newline at end of file diff --git a/src/krb5/_set_password.pyi b/src/krb5/_set_password.pyi index d51f45a..3c02870 100644 --- a/src/krb5/_set_password.pyi +++ b/src/krb5/_set_password.pyi @@ -1,3 +1,4 @@ +import enum import typing from krb5._ccache import CCache @@ -5,27 +6,52 @@ from krb5._context import Context from krb5._creds import Creds from krb5._principal import Principal +class SetPasswordResultCode(enum.IntEnum): + """Password change result constants returned by :meth:`set_password()` + Follow RFC 3244 with additional Microsoft extensions. + """ + + SUCCESS = ... # Success + MALFORMED = ... # Malformed request + HARDERROR = ... # Server error + AUTHERROR = ... # Authentication error + SOFTERROR = ... # Password change rejected + ACCESSDENIED = ... # Microsoft extension: Not authorized + BAD_VERSION = ... # Microsoft extension: Unknown RPC version + INITIAL_FLAG_NEEDED = ... # Microsoft extension: + # The presented credentials were not obtained using a password directly + class SetPasswordResult(typing.NamedTuple): """The result returned by :meth:`set_password()` and :meth:`set_password_using_ccache()`. - The `result_code` and `result_code_string` is the library response:\n - KRB5_KPASSWD_SUCCESS (0) - Success\n - KRB5_KPASSWD_MALFORMED (1) - Malformed request error\n - KRB5_KPASSWD_HARDERROR (2) - Server error\n - KRB5_KPASSWD_AUTHERROR (3) - Authentication error\n - KRB5_KPASSWD_SOFTERROR (4) - Password change rejected\n + The `result_code` and `result_code_string` are the pure library responses. + See `SetPasswordResultCode` for more information. - The `result_string` is a server protocol response that may contain useful + The `server_response` is a server protocol message that may contain useful information about password policy violations or other errors. + Despite RFC 3244, the server response is not standardized and may vary. + Depending on `kpasswd` implementation, it may be returned as:\n + - 30-byte binary Active Directory Policy Information + - UTF-8 byte string (MIT KDC, potentially Heimdal KDC) + - raw bytes (unknown or custom implementation) + + The trick is that Active Directory Policy Information always starts with + `0x0000` signature to distinguish from UTF-8. + So the client may try decoding the server response with either + :meth:`ADPolicyInfo.from_bytes()` or :meth:`bytes.decode()`. + And if the decoding fails with corresponding `ValueError` or + `UnicodeDecodeError`, the raw bytes should be analyzed. + + See `ADPolicyInfo` for more information. """ - result_code: int + result_code: SetPasswordResultCode """The library result code of the password change operation.""" result_code_string: bytes """The byte string representation of the result code.""" - result_string: bytes - """Server response string""" + server_response: bytes + """Implementation-specific server response.""" def set_password( context: Context, @@ -54,8 +80,7 @@ def set_password( change_password_for: `None` or the principal to set the password for. Returns: - SetPasswordResult: See `SetPasswordResult` for more information about - the return result. + SetPasswordResult: See `SetPasswordResult` for more information. """ def set_password_using_ccache( @@ -85,6 +110,5 @@ def set_password_using_ccache( change_password_for: `None` or the principal to set the password for. Returns: - SetPasswordResult: See `SetPasswordResult` for more information about - the return result. + SetPasswordResult: See `SetPasswordResult` for more information. """ diff --git a/src/krb5/_set_password.pyx b/src/krb5/_set_password.pyx index 722d4c6..0d7c4bf 100644 --- a/src/krb5/_set_password.pyx +++ b/src/krb5/_set_password.pyx @@ -1,6 +1,6 @@ # Support for Microsoft set/change password was added in MIT 1.7 -import collections +import enum import typing from krb5._exceptions import Krb5Error @@ -13,6 +13,15 @@ from krb5._principal cimport Principal cdef extern from "python_krb5.h": + int32_t KRB5_KPASSWD_SUCCESS + int32_t KRB5_KPASSWD_MALFORMED + int32_t KRB5_KPASSWD_HARDERROR + int32_t KRB5_KPASSWD_AUTHERROR + int32_t KRB5_KPASSWD_SOFTERROR + int32_t KRB5_KPASSWD_ACCESSDENIED + int32_t KRB5_KPASSWD_BAD_VERSION + int32_t KRB5_KPASSWD_INITIAL_FLAG_NEEDED + krb5_error_code krb5_set_password( krb5_context context, krb5_creds *creds, @@ -33,14 +42,33 @@ cdef extern from "python_krb5.h": krb5_data *result_string ) nogil -SetPasswordResult = collections.namedtuple( - 'SetPasswordResult', - [ - 'result_code', - 'result_code_string', - 'result_string', - ], -) + + +class SetPasswordResultCode(enum.IntEnum): + SUCCESS = KRB5_KPASSWD_SUCCESS + MALFORMED = KRB5_KPASSWD_MALFORMED + HARDERROR = KRB5_KPASSWD_HARDERROR + AUTHERROR = KRB5_KPASSWD_AUTHERROR + SOFTERROR = KRB5_KPASSWD_SOFTERROR + ACCESSDENIED = KRB5_KPASSWD_ACCESSDENIED + BAD_VERSION = KRB5_KPASSWD_BAD_VERSION + INITIAL_FLAG_NEEDED = KRB5_KPASSWD_INITIAL_FLAG_NEEDED + + @classmethod + def _missing_(cls, value: object) -> typing.Optional[enum.Enum]: + if not isinstance(value, int): + return None + value = int(value) + + new_member = int.__new__(cls, value) + new_member._name_ = f"Unknown_SetPasswordResultCode_{str(value).replace('-', 'm')}" + new_member._value_ = value + return cls._value2member_map_.setdefault(value, new_member) + +class SetPasswordResult(typing.NamedTuple): + result_code: SetPasswordResultCode + result_code_string: bytes + server_response: bytes def set_password( Context context not None, @@ -50,8 +78,8 @@ def set_password( ) -> SetPasswordResult: cdef krb5_error_code err = 0 cdef int result_code - cdef krb5_data result_code_string - cdef krb5_data result_string + cdef krb5_data krb5_result_code_string + cdef krb5_data krb5_server_response cdef char *newpw_ptr cdef krb5_principal change_password_for_ptr = NULL cdef size_t length @@ -62,8 +90,8 @@ def set_password( else: newpw_ptr = b"" - pykrb5_init_krb5_data(&result_code_string) - pykrb5_init_krb5_data(&result_string) + pykrb5_init_krb5_data(&krb5_result_code_string) + pykrb5_init_krb5_data(&krb5_server_response) if change_password_for is not None: change_password_for_ptr = change_password_for.raw @@ -75,32 +103,32 @@ def set_password( newpw_ptr, change_password_for_ptr, &result_code, - &result_code_string, - &result_string + &krb5_result_code_string, + &krb5_server_response ) if err: raise Krb5Error(context, err) - pykrb5_get_krb5_data(&result_code_string, &length, &value) + pykrb5_get_krb5_data(&krb5_result_code_string, &length, &value) if length == 0: - result_code_bytes = b"" + result_code_string = b"" else: - result_code_bytes = value[:length] + result_code_string = value[:length] - pykrb5_get_krb5_data(&result_string, &length, &value) + pykrb5_get_krb5_data(&krb5_server_response, &length, &value) if length == 0: - result_string_bytes = b"" + server_response = b"" else: - result_string_bytes = value[:length] + server_response = value[:length] - return SetPasswordResult(result_code, result_code_bytes, result_string_bytes) + return SetPasswordResult(result_code, result_code_string, server_response) finally: - pykrb5_free_data_contents(context.raw, &result_code_string) - pykrb5_free_data_contents(context.raw, &result_string) + pykrb5_free_data_contents(context.raw, &krb5_result_code_string) + pykrb5_free_data_contents(context.raw, &krb5_server_response) def set_password_using_ccache( Context context not None, @@ -110,8 +138,8 @@ def set_password_using_ccache( ) -> SetPasswordResult: cdef krb5_error_code err = 0 cdef int result_code - cdef krb5_data result_code_string - cdef krb5_data result_string + cdef krb5_data krb5_result_code_string + cdef krb5_data krb5_server_response cdef char *newpw_ptr cdef krb5_principal change_password_for_ptr = NULL cdef size_t length @@ -122,8 +150,8 @@ def set_password_using_ccache( else: newpw_ptr = b"" - pykrb5_init_krb5_data(&result_code_string) - pykrb5_init_krb5_data(&result_string) + pykrb5_init_krb5_data(&krb5_result_code_string) + pykrb5_init_krb5_data(&krb5_server_response) if change_password_for is not None: change_password_for_ptr = change_password_for.raw @@ -135,30 +163,30 @@ def set_password_using_ccache( newpw_ptr, change_password_for_ptr, &result_code, - &result_code_string, - &result_string + &krb5_result_code_string, + &krb5_server_response ) if err: raise Krb5Error(context, err) - pykrb5_get_krb5_data(&result_code_string, &length, &value) + pykrb5_get_krb5_data(&krb5_result_code_string, &length, &value) if length == 0: - result_code_bytes = b"" + result_code_string = b"" else: - result_code_bytes = value[:length] + result_code_string = value[:length] - pykrb5_get_krb5_data(&result_string, &length, &value) + pykrb5_get_krb5_data(&krb5_server_response, &length, &value) if length == 0: - result_string_bytes = b"" + server_response = b"" else: - result_string_bytes = value[:length] + server_response = value[:length] - return SetPasswordResult(result_code, result_code_bytes, result_string_bytes) + return SetPasswordResult(result_code, result_code_string, server_response) finally: - pykrb5_free_data_contents(context.raw, &result_code_string) - pykrb5_free_data_contents(context.raw, &result_string) + pykrb5_free_data_contents(context.raw, &krb5_result_code_string) + pykrb5_free_data_contents(context.raw, &krb5_server_response) diff --git a/tests/test_changepw.py b/tests/test_changepw.py index 5bf47d8..9dc2135 100644 --- a/tests/test_changepw.py +++ b/tests/test_changepw.py @@ -1,9 +1,116 @@ +import locale + import k5test import pytest import krb5 +@pytest.mark.requires_api("chpw_message") +def test_chpw_message() -> None: + + # MIT Kerberos test samples, https://github.com/krb5/krb5, human readable + adpi_tests = { + "complex": [ + ( + b"\0\0", + b"\0\0\0\0", + b"\0\0\0\0", + b"\0\0\0\1", + b"\0\0\0\0\0\0\0\0", + b"\0\0\0\0\0\0\0\0", + ), + ( + "The password must include numbers or symbols.", + "Don't include any part of your name in the password.", + ), + ], + "length": [ + ( + b"\0\0", + b"\0\0\0\x0d", + b"\0\0\0\0", + b"\0\0\0\0", + b"\0\0\0\0\0\0\0\0", + b"\0\0\0\0\0\0\0\0", + ), + ("The password must contain at least 13 characters.",), + ], + "history": [ + ( + b"\0\0", + b"\0\0\0\0", + b"\0\0\0\x09", + b"\0\0\0\0", + b"\0\0\0\0\0\0\0\0", + b"\0\0\0\0\0\0\0\0", + ), + ("The password must be different from the previous 9 passwords."), + ], + "age": [ + ( + b"\0\0", + b"\0\0\0\0", + b"\0\0\0\0", + b"\0\0\0\0", + b"\0\0\0\0\0\0\0\0", + b"\0\0\x01\x92\x54\xd3\x80\0", + ), + ("The password can only be changed every 2 days."), + ], + "combined": [ + ( + b"\0\0", + b"\0\0\0\x05", + b"\0\0\0\x0d", + b"\0\0\0\x01", + b"\0\0\0\0\0\0\0\0", + b"\0\0\0\xc9\x2a\x69\xc0\0", + ), + ( + "The password can only be changed once a day.", + "The password must be different from the previous 13 passwords.", + "The password must contain at least 5 characters.", + "The password must include numbers or symbols.", + "Don't include any part of your name in the password.", + ), + ], + "unknown": [ + ( + b"\0\0", + b"\0\0\0\0", + b"\0\0\0\0", + b"\x80\0\0\1", + b"\0\0\0\0\0\0\0\0", + b"\0\0\0\0\0\0\0\0", + ) + ], + } + + locale.setlocale(locale.LC_ALL, "C") + + ctx = krb5.init_context() + + samples = {k: b"".join(v[0]) if isinstance(v, list) else b"" for k, v in adpi_tests.items()} + + for k, test in adpi_tests.items(): + if isinstance(test, list) and len(test) > 1: + phrases = test[1] + for phrase in phrases: + message = krb5.chpw_message(ctx, samples[k]) + assert message.decode().find(phrase) >= 0 + + assert krb5.ADPolicyInfoProp.COMPLEX in krb5.ADPolicyInfo.from_bytes(samples["complex"]).properties + assert krb5.ADPolicyInfo.from_bytes(samples["length"]).min_length == 13 + assert krb5.ADPolicyInfo.from_bytes(samples["history"]).history == 9 + assert krb5.ADPolicyInfo.from_bytes(samples["age"]).min_age == 2 * 86400 * 10_000_000 + assert krb5.ADPolicyInfo.from_bytes(samples["combined"]).min_length == 5 + assert krb5.ADPolicyInfo.from_bytes(samples["combined"]).history == 13 + assert krb5.ADPolicyInfo.from_bytes(samples["combined"]).min_age == 1 * 86400 * 10_000_000 + assert krb5.ADPolicyInfoProp.COMPLEX in krb5.ADPolicyInfo.from_bytes(samples["combined"]).properties + assert 0x80000000 & krb5.ADPolicyInfo.from_bytes(samples["unknown"]).properties != 0 + + def test_set_password(realm: k5test.K5Realm) -> None: if realm.provider != "mit": @@ -42,28 +149,28 @@ def test_set_password(realm: k5test.K5Realm) -> None: creds = krb5.get_init_creds_password(ctx, princ, opt, old_password.encode(), in_tkt_service=b"kadmin/changepw") assert isinstance(creds, krb5.Creds) - (result_code, result_code_string, result_string) = krb5.set_password(ctx, creds, empty_password.encode()) - assert result_code != 0 + (result_code, result_code_string, server_response) = krb5.set_password(ctx, creds, empty_password.encode()) + assert result_code == krb5.SetPasswordResultCode.SOFTERROR assert result_code_string.find(b"rejected") > 0 - assert result_string.find(b"too short") > 0 + assert server_response.find(b"too short") > 0 - (result_code, result_code_string, result_string) = krb5.set_password(ctx, creds, weak_password.encode()) - assert result_code != 0 + (result_code, result_code_string, server_response) = krb5.set_password(ctx, creds, weak_password.encode()) + assert result_code == krb5.SetPasswordResultCode.SOFTERROR assert result_code_string.find(b"rejected") > 0 - assert result_string.find(b"too short") > 0 + assert server_response.find(b"too short") > 0 - (result_code, result_code_string, result_string) = krb5.set_password(ctx, creds, new_password.encode()) - assert result_code == 0 + (result_code, result_code_string, server_response) = krb5.set_password(ctx, creds, new_password.encode()) + assert result_code == krb5.SetPasswordResultCode.SUCCESS creds = krb5.get_init_creds_password(ctx, princ, opt, new_password.encode()) assert isinstance(creds, krb5.Creds) assert creds.client.name == princ.name # set_password for other principal using admin creds - (result_code, result_code_string, result_string) = krb5.set_password( + (result_code, result_code_string, server_response) = krb5.set_password( ctx, admin_creds, new_password2.encode(), change_password_for=princ ) - assert result_code == 0 + assert result_code == krb5.SetPasswordResultCode.SUCCESS creds = krb5.get_init_creds_password(ctx, princ, opt, new_password2.encode()) assert isinstance(creds, krb5.Creds) @@ -86,24 +193,24 @@ def test_set_password(realm: k5test.K5Realm) -> None: krb5.cc_initialize(ctx, cc, princ) krb5.cc_store_cred(ctx, cc, creds) - (result_code, result_code_string, result_string) = krb5.set_password_using_ccache( + (result_code, result_code_string, server_response) = krb5.set_password_using_ccache( ctx, cc, empty_password.encode(), princ ) - assert result_code != 0 + assert result_code == krb5.SetPasswordResultCode.SOFTERROR assert result_code_string.find(b"rejected") > 0 - assert result_string.find(b"too short") > 0 + assert server_response.find(b"too short") > 0 - (result_code, result_code_string, result_string) = krb5.set_password_using_ccache( + (result_code, result_code_string, server_response) = krb5.set_password_using_ccache( ctx, cc, weak_password.encode(), princ ) - assert result_code != 0 + assert result_code == krb5.SetPasswordResultCode.SOFTERROR assert result_code_string.find(b"rejected") > 0 - assert result_string.find(b"too short") > 0 + assert server_response.find(b"too short") > 0 - (result_code, result_code_string, result_string) = krb5.set_password_using_ccache( + (result_code, result_code_string, server_response) = krb5.set_password_using_ccache( ctx, cc, new_password.encode(), princ ) - assert result_code == 0 + assert result_code == krb5.SetPasswordResultCode.SUCCESS creds = krb5.get_init_creds_password(ctx, princ, opt, new_password.encode()) assert isinstance(creds, krb5.Creds) @@ -116,10 +223,10 @@ def test_set_password(realm: k5test.K5Realm) -> None: krb5.cc_store_cred(ctx, admin_cc, admin_creds) # set_password for other principal using admin ccache - (result_code, result_code_string, result_string) = krb5.set_password_using_ccache( + (result_code, result_code_string, server_response) = krb5.set_password_using_ccache( ctx, admin_cc, new_password2.encode(), change_password_for=princ ) - assert result_code == 0 + assert result_code == krb5.SetPasswordResultCode.SUCCESS creds = krb5.get_init_creds_password(ctx, princ, opt, new_password2.encode()) assert isinstance(creds, krb5.Creds)