Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

password change functionality #50

Merged
merged 19 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = krb5
version = 0.6.0
version = 0.6.0.post50
zarganum marked this conversation as resolved.
Show resolved Hide resolved
url = https://github.com/jborean93/pykrb5
author = Jordan Borean
author_email = [email protected]
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ def get_krb5_lib_path(
("kt_have_content", "krb5_kt_have_content"),
"principal",
("principal_heimdal", "krb5_principal_get_realm"),
"set_password",
"string",
("string_mit", "krb5_enctype_to_name"),
]:
Expand Down
8 changes: 8 additions & 0 deletions src/krb5/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@
parse_name_flags,
unparse_name_flags,
)
from krb5._set_password import (
SetPasswordResult,
set_password,
set_password_using_ccache,
)
from krb5._string import enctype_to_string, string_to_enctype

__all__ = [
Expand All @@ -101,6 +106,7 @@
"Principal",
"PrincipalParseFlags",
"PrincipalUnparseFlags",
"SetPasswordResult",
"TicketFlags",
"TicketTimes",
"build_principal",
Expand Down Expand Up @@ -155,6 +161,8 @@
"kt_resolve",
"parse_name_flags",
"set_default_realm",
"set_password",
"set_password_using_ccache",
"set_real_time",
"string_to_enctype",
"timeofday",
Expand Down
23 changes: 23 additions & 0 deletions src/krb5/_krb5_types.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ from libc.stdint cimport int32_t, uint8_t, uint32_t

cdef extern from "python_krb5.h":
"""
// The Heimdal and MIT krb5 libraries have different implementations of the krb5_data struct.
// MIT uses a struct with a member magic of -1760647422L
// Heimdal uses a struct without a magic
void pykrb5_init_krb5_data(
krb5_data *data
)
{
#if defined(HEIMDAL_XFREE)
krb5_data_zero(data);
#else
data->magic = KV5M_DATA;
data->length = 0;
data->data = NULL;
#endif
}

// The structures are different so cannot be explicitly defined in Cython. Use inline C to set the structs elements
// by name.
void pykrb5_set_krb5_data(
Expand All @@ -14,6 +30,9 @@ cdef extern from "python_krb5.h":
char *value
)
{
#if !defined(HEIMDAL_XFREE)
data->magic = KV5M_DATA;
#endif
data->length = length;
data->data = value;
}
Expand Down Expand Up @@ -121,6 +140,10 @@ cdef extern from "python_krb5.h":
krb5_prompt prompts[],
)

void pykrb5_init_krb5_data(
krb5_data *data,
) nogil

void pykrb5_set_krb5_data(
krb5_data *data,
size_t length,
Expand Down
99 changes: 99 additions & 0 deletions src/krb5/_set_password.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import typing

from krb5._ccache import CCache
from krb5._context import Context
from krb5._creds import Creds
from krb5._principal import Principal

class SetPasswordResult(typing.NamedTuple):
result_code: int
zarganum marked this conversation as resolved.
Show resolved Hide resolved
result_code_string: bytes
result_string: str

def set_password(
context: Context,
creds: Creds,
newpw: bytes,
change_password_for: typing.Optional[Principal] = None,
) -> SetPasswordResult:
"""Set a password for a principal using specified credentials.


This function implements the set password operation of ``RFC 3244``,
for interoperability with Microsoft Windows implementations.
It uses the credentials `creds` to set the password `newpw` for the
principal `change_password_for`.
If `change_password_for` is `None`, the password is set for the principal
owning creds. If `change_password_for` is not `None`, the change is
performed on the specified principal, assuming enough privileges.

Note: the `creds` can be obtained using `get_init_creds_password()` with
`in_tkt_service` set to ``kadmin/changepw``.

Args:
context: Krb5 context.
creds: Credentials for kadmin/changepw service.
newpw: New password.
change_password_for: `None` or the principal to set the password for.

Returns:
A named tuple containing the `result_code`, `result_code_string`, and an
optional `result_string`.
The non-zero `result_code` means error with a corresponding readable
representation in `result_code_string`. It is a `bytes` object.
The `result_string` is a server response that may contain useful
information about password policy violations or other errors. It is
decoded as a `string` according to ``RFC 3244``.

The possible values of the output `result_code` are:

`KRB5_KPASSWD_SUCCESS` (0) - Success
`KRB5_KPASSWD_MALFORMED` (1) - Malformed request error
`KRB5_KPASSWD_HARDERROR` (2) - Server error
`KRB5_KPASSWD_AUTHERROR` (3) - Authentication error
`KRB5_KPASSWD_SOFTERROR` (4) - Password change rejected
"""

def set_password_using_ccache(
context: Context,
ccache: CCache,
newpw: bytes,
change_password_for: typing.Optional[Principal] = None,
) -> SetPasswordResult:
"""Set a password for a principal using cached credentials.


This function implements the set password operation of ``RFC 3244``,
for interoperability with Microsoft Windows implementations.
It uses the cached credentials from `ccache` to set the password `newpw` for
the principal `change_password_for`.
If `change_password_for` is `None`, the change is performed on the default
principal in ccache. If `change_password_for` is not `None`, the change is
performed on the specified principal.

Note: the credentials can be obtained using `get_init_creds_password()` with
`in_tkt_service` set to ``kadmin/changepw`` and stored to `ccache`.

Args:
context: Krb5 context.
ccache: Credential cache.
newpw: The new password.
change_password_for: `None` or the principal to set the password for.

Returns:
A named tuple containing the `result_code`, `result_code_string`, and an
optional `result_string`.
The non-zero `result_code` means error with a corresponding readable
representation in `result_code_string`. It is a `bytes` object.
The `result_string` is a server response that may contain useful
information about password policy violations or other errors. It is
decoded as a `string` according to ``RFC 3244``.

The possible values of the output `result_code` are:

`KRB5_KPASSWD_SUCCESS` (0) - Success
`KRB5_KPASSWD_MALFORMED` (1) - Malformed request error
`KRB5_KPASSWD_HARDERROR` (2) - Server error
`KRB5_KPASSWD_AUTHERROR` (3) - Authentication error
`KRB5_KPASSWD_SOFTERROR` (4) - Password change rejected
"""
164 changes: 164 additions & 0 deletions src/krb5/_set_password.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Support for Microsoft set/change password was added in MIT 1.7

import collections
import typing

from krb5._exceptions import Krb5Error

from krb5._ccache cimport CCache
from krb5._context cimport Context
from krb5._creds cimport Creds
from krb5._krb5_types cimport *
from krb5._principal cimport Principal


cdef extern from "python_krb5.h":
krb5_error_code krb5_set_password(
krb5_context context,
krb5_creds *creds,
const char *newpw,
krb5_principal change_password_for,
int *result_code,
krb5_data *result_code_string,
krb5_data *result_string
) nogil

krb5_error_code krb5_set_password_using_ccache(
krb5_context context,
krb5_ccache ccache,
const char *newpw,
krb5_principal change_password_for,
int *result_code,
krb5_data *result_code_string,
krb5_data *result_string
) nogil

SetPasswordResult = collections.namedtuple(
'SetPasswordResult',
[
'result_code',
'result_code_string',
'result_string',
],
)

def set_password(
Context context not None,
Creds creds not None,
const unsigned char[:] newpw not None,
change_password_for: typing.Optional[Principal] = None,
) -> SetPasswordResult:
cdef krb5_error_code err = 0
cdef int result_code
cdef krb5_data result_code_string
cdef krb5_data result_string
cdef char *newpw_ptr
cdef krb5_principal change_password_for_ptr = NULL
cdef size_t length
cdef char *value

if len(newpw) > 0:
newpw_ptr = <char *>&newpw[0]
else:
newpw_ptr = <char *>b""

pykrb5_init_krb5_data(&result_code_string)
pykrb5_init_krb5_data(&result_string)

if change_password_for is not None:
change_password_for_ptr = change_password_for.raw

try:
err = krb5_set_password(
context.raw,
creds.get_pointer(),
newpw_ptr,
change_password_for_ptr,
&result_code,
&result_code_string,
&result_string
)

if err:
raise Krb5Error(context, err)

pykrb5_get_krb5_data(&result_code_string, &length, &value)

if length == 0:
result_code_bytes = b""
else:
result_code_bytes = value[:length]

pykrb5_get_krb5_data(&result_string, &length, &value)

if length == 0:
result_string_bytes = b""
else:
result_string_bytes = value[:length]

return SetPasswordResult(result_code, result_code_bytes, result_string_bytes.decode("utf-8"))

finally:
pykrb5_free_data_contents(context.raw, &result_code_string)
pykrb5_free_data_contents(context.raw, &result_string)

def set_password_using_ccache(
Context context not None,
CCache ccache not None,
const unsigned char[:] newpw not None,
change_password_for: typing.Optional[Principal] = None,
) -> SetPasswordResult:
cdef krb5_error_code err = 0
cdef int result_code
cdef krb5_data result_code_string
cdef krb5_data result_string
cdef char *newpw_ptr
cdef krb5_principal change_password_for_ptr = NULL
cdef size_t length
cdef char *value

if len(newpw) > 0:
newpw_ptr = <char *>&newpw[0]
else:
newpw_ptr = <char *>b""

pykrb5_init_krb5_data(&result_code_string)
pykrb5_init_krb5_data(&result_string)

if change_password_for is not None:
change_password_for_ptr = change_password_for.raw

try:
err = krb5_set_password_using_ccache(
context.raw,
ccache.raw,
newpw_ptr,
change_password_for_ptr,
&result_code,
&result_code_string,
&result_string
)

if err:
raise Krb5Error(context, err)

pykrb5_get_krb5_data(&result_code_string, &length, &value)

if length == 0:
result_code_bytes = b""
else:
result_code_bytes = value[:length]

pykrb5_get_krb5_data(&result_string, &length, &value)

if length == 0:
result_string_bytes = b""
else:
result_string_bytes = value[:length]

return SetPasswordResult(result_code, result_code_bytes, result_string_bytes.decode("utf-8"))

finally:
pykrb5_free_data_contents(context.raw, &result_code_string)
pykrb5_free_data_contents(context.raw, &result_string)

1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
@pytest.fixture(scope="session")
def realm() -> typing.Iterator[k5test.K5Realm]:
test_realm = k5test.K5Realm()

try:
original_env: typing.Dict[str, typing.Optional[str]] = {}
for k in test_realm.env.keys():
Expand Down
Loading