From 39b3677d622d7cf5f9af037325779bd7f096764b Mon Sep 17 00:00:00 2001 From: Jacob Henner Date: Thu, 18 Jun 2026 16:40:15 -0400 Subject: [PATCH] Add gss_localname and friends * gss_authorize_localname * gss_localname * gss_pname_to_uid * gss_userok Fixes #49 Signed-off-by: Jacob Henner --- gssapi/raw/__init__.py | 7 ++ gssapi/raw/ext_localname.pyi | 93 ++++++++++++++++++ gssapi/raw/ext_localname.pyx | 156 ++++++++++++++++++++++++++++++ gssapi/raw/ext_localname_attr.pyi | 7 ++ gssapi/raw/ext_localname_attr.pyx | 13 +++ gssapi/tests/test_raw.py | 77 +++++++++++++++ setup.py | 3 + 7 files changed, 356 insertions(+) create mode 100644 gssapi/raw/ext_localname.pyi create mode 100644 gssapi/raw/ext_localname.pyx create mode 100644 gssapi/raw/ext_localname_attr.pyi create mode 100644 gssapi/raw/ext_localname_attr.pyx diff --git a/gssapi/raw/__init__.py b/gssapi/raw/__init__.py index 0699c419..88f19b8f 100644 --- a/gssapi/raw/__init__.py +++ b/gssapi/raw/__init__.py @@ -149,3 +149,10 @@ from gssapi.raw.ext_set_cred_opt import * # noqa except ImportError: pass + +# optional localname support +try: + from gssapi.raw.ext_localname import * # noqa + from gssapi.raw.ext_localname_attr import * # noqa +except ImportError: + pass diff --git a/gssapi/raw/ext_localname.pyi b/gssapi/raw/ext_localname.pyi new file mode 100644 index 00000000..9c5c1935 --- /dev/null +++ b/gssapi/raw/ext_localname.pyi @@ -0,0 +1,93 @@ +import typing as t + +if t.TYPE_CHECKING: + from gssapi.raw.names import Name + from gssapi.raw.oids import OID + + +def localname( + name: "Name", + mech: t.Optional["OID"] = None, +) -> bytes: + """Get the local name for a GSSAPI name. + + This method determines the local name associated with a GSSAPI + name, optionally for a given mechanism. + + Args: + name (Name): the GSSAPI name to map to a local name + mech (~gssapi.OID): the mechanism to use for the mapping + (or None for the default) + + Returns: + bytes: the local name + + Raises: + ~gssapi.exceptions.GSSError + """ + + +def userok( + name: "Name", + username: t.Union[bytes, str], +) -> bool: + """Determine whether a GSSAPI name is authorized to act as a local user. + + This method determines whether a given GSSAPI name is authorized + to act as the given local username. This is a simple wrapper + around :func:`authorize_localname` that only supports system + usernames as local names. + + Args: + name (Name): the GSSAPI name to check + username (Union[bytes, str]): the local username to check against + + Returns: + bool: whether or not the name is authorized to act as the user + """ + + +def authorize_localname( + name: "Name", + user: "Name", +) -> bool: + """Determine whether a GSSAPI name is authorized to act as a local name. + + This method determines whether a given GSSAPI name is authorized + to act as the given local name. + + Args: + name (Name): the mechanism name to check + user (Name): the local name to check against + + Returns: + bool: whether or not the name is authorized + + Raises: + ~gssapi.exceptions.GSSError + """ + + +def pname_to_uid( + name: "Name", + mech: t.Optional["OID"] = None, +) -> int: + """Get the local UID for a GSSAPI name. + + This method determines the local UID associated with a GSSAPI + name, optionally for a given mechanism. + + Note: + This function is not available on Windows. + + Args: + name (Name): the GSSAPI name to map to a local UID + mech (~gssapi.OID): the mechanism to use for the mapping + (or None for the default) + + Returns: + int: the local UID + + Raises: + ~gssapi.exceptions.GSSError + """ diff --git a/gssapi/raw/ext_localname.pyx b/gssapi/raw/ext_localname.pyx new file mode 100644 index 00000000..81fec8c6 --- /dev/null +++ b/gssapi/raw/ext_localname.pyx @@ -0,0 +1,156 @@ +GSSAPI="BASE" # This ensures that a full module is generated by Cython + +from gssapi.raw.cython_types cimport * +from gssapi.raw.names cimport Name +from gssapi.raw.oids cimport OID + +from gssapi.raw.misc import GSSError +from gssapi import _utils + +from posix.types cimport uid_t + +cdef extern from "python_gssapi_ext.h": + OM_uint32 gss_localname(OM_uint32 *minor, + const gss_name_t name, + const gss_OID mech_type, + gss_buffer_t localname) nogil + + int gss_userok(const gss_name_t name, + const char *username) nogil + + OM_uint32 gss_authorize_localname(OM_uint32 *minor, + const gss_name_t name, + const gss_name_t user) nogil + + OM_uint32 gss_pname_to_uid(OM_uint32 *minor, + const gss_name_t name, + const gss_OID mech_type, + uid_t *uid_out) nogil + + +def localname(Name name not None, OID mech=None): + """Get the local name for a GSSAPI name. + + This method determines the local name associated with a GSSAPI + name, optionally for a given mechanism. + + Args: + name (Name): the GSSAPI name to map to a local name + mech (~gssapi.OID): the mechanism to use for the mapping + (or None for the default) + + Returns: + bytes: the local name + + Raises: + ~gssapi.exceptions.GSSError + """ + cdef gss_OID m = GSS_C_NO_OID + if mech is not None: + m = &mech.raw_oid + + cdef gss_buffer_desc output = gss_buffer_desc(0, NULL) + + cdef OM_uint32 maj_stat, min_stat + + with nogil: + maj_stat = gss_localname(&min_stat, name.raw_name, m, &output) + + if maj_stat == GSS_S_COMPLETE: + py_output = (output.value)[:output.length] + gss_release_buffer(&min_stat, &output) + return py_output + else: + raise GSSError(maj_stat, min_stat) + + +def userok(Name name not None, username not None): + """Determine whether a GSSAPI name is authorized to act as a local user. + + This method determines whether a given GSSAPI name is authorized + to act as the given local username. This is a simple wrapper + around :func:`authorize_localname` that only supports system + usernames as local names. + + Args: + name (Name): the GSSAPI name to check + username (Union[bytes, str]): the local username to check against + + Returns: + bool: whether or not the name is authorized to act as the user + """ + cdef int res + + if isinstance(username, str): + username = username.encode(_utils._get_encoding()) + + cdef char *c_username = username + + with nogil: + res = gss_userok(name.raw_name, c_username) + + return res == 1 + + +def authorize_localname(Name name not None, Name user not None): + """Determine whether a GSSAPI name is authorized to act as a local name. + + This method determines whether a given GSSAPI name is authorized + to act as the given local name. + + Args: + name (Name): the mechanism name to check + user (Name): the local name to check against + + Returns: + bool: whether or not the name is authorized + + Raises: + ~gssapi.exceptions.GSSError + """ + cdef OM_uint32 maj_stat, min_stat + + with nogil: + maj_stat = gss_authorize_localname(&min_stat, name.raw_name, + user.raw_name) + + if maj_stat == GSS_S_COMPLETE: + return True + else: + raise GSSError(maj_stat, min_stat) + + +def pname_to_uid(Name name not None, OID mech=None): + """Get the local UID for a GSSAPI name. + + This method determines the local UID associated with a GSSAPI + name, optionally for a given mechanism. + + Note: + This function is not available on Windows. + + Args: + name (Name): the GSSAPI name to map to a local UID + mech (~gssapi.OID): the mechanism to use for the mapping + (or None for the default) + + Returns: + int: the local UID + + Raises: + ~gssapi.exceptions.GSSError + """ + cdef gss_OID m = GSS_C_NO_OID + if mech is not None: + m = &mech.raw_oid + + cdef uid_t uid_out + cdef OM_uint32 maj_stat, min_stat + + with nogil: + maj_stat = gss_pname_to_uid(&min_stat, name.raw_name, m, &uid_out) + + if maj_stat == GSS_S_COMPLETE: + return uid_out + else: + raise GSSError(maj_stat, min_stat) diff --git a/gssapi/raw/ext_localname_attr.pyi b/gssapi/raw/ext_localname_attr.pyi new file mode 100644 index 00000000..5e41309a --- /dev/null +++ b/gssapi/raw/ext_localname_attr.pyi @@ -0,0 +1,7 @@ +ATTR_LOCAL_LOGIN_USER: bytes +"""The attribute name for the local login username. + +This can be used with RFC 6680 :func:`~gssapi.raw.ext_rfc6680.get_name_attribute` +and :func:`~gssapi.raw.ext_rfc6680.set_name_attribute` to retrieve or set the +local login username for a GSSAPI name. +""" diff --git a/gssapi/raw/ext_localname_attr.pyx b/gssapi/raw/ext_localname_attr.pyx new file mode 100644 index 00000000..abf4fb29 --- /dev/null +++ b/gssapi/raw/ext_localname_attr.pyx @@ -0,0 +1,13 @@ +GSSAPI="BASE" # This ensures that a full module is generated by Cython + +from gssapi.raw.cython_types cimport gss_buffer_t + +cdef extern from "python_gssapi_ext.h": + gss_buffer_t GSS_C_ATTR_LOCAL_LOGIN_USER + + +# Export the attribute name constant as a Python bytes object. +# This can be used with RFC 6680 get_name_attribute/set_name_attribute +# to retrieve or set the local login username for a GSSAPI name. +ATTR_LOCAL_LOGIN_USER = (GSS_C_ATTR_LOCAL_LOGIN_USER.value)[ + :GSS_C_ATTR_LOCAL_LOGIN_USER.length] diff --git a/gssapi/tests/test_raw.py b/gssapi/tests/test_raw.py index 1ab7ab3a..56571332 100644 --- a/gssapi/tests/test_raw.py +++ b/gssapi/tests/test_raw.py @@ -1309,6 +1309,83 @@ def test_krb5_set_allowable_enctypes(self): self.assertEqual(acceptor_info.cfx_kd.ctx_key_type, acceptor_info.cfx_kd.acceptor_subkey_type) + @ktu.gssapi_extension_test('localname', 'Local Name') + def test_localname(self): + base_name = gb.import_name(self.USER_PRINC, + gb.NameType.kerberos_principal) + canon_name = gb.canonicalize_name(base_name, gb.MechType.kerberos) + + local = gb.localname(canon_name, gb.MechType.kerberos) + self.assertIsInstance(local, bytes) + self.assertGreater(len(local), 0) + + @ktu.gssapi_extension_test('localname', 'Local Name') + def test_localname_no_mech(self): + base_name = gb.import_name(self.USER_PRINC, + gb.NameType.kerberos_principal) + canon_name = gb.canonicalize_name(base_name, gb.MechType.kerberos) + + local = gb.localname(canon_name) + self.assertIsInstance(local, bytes) + self.assertGreater(len(local), 0) + + @ktu.gssapi_extension_test('localname', 'Local Name') + def test_userok(self): + base_name = gb.import_name(self.USER_PRINC, + gb.NameType.kerberos_principal) + canon_name = gb.canonicalize_name(base_name, gb.MechType.kerberos) + + local = gb.localname(canon_name, gb.MechType.kerberos) + # The user should be authorized as their own local name + self.assertTrue(gb.userok(canon_name, local)) + + # A made-up username should not be authorized + self.assertFalse(gb.userok(canon_name, b'not_a_real_user_name')) + + @ktu.gssapi_extension_test('localname', 'Local Name') + def test_userok_str(self): + base_name = gb.import_name(self.USER_PRINC, + gb.NameType.kerberos_principal) + canon_name = gb.canonicalize_name(base_name, gb.MechType.kerberos) + + local = gb.localname(canon_name, gb.MechType.kerberos) + # userok should also accept str input + self.assertTrue(gb.userok(canon_name, local.decode('UTF-8'))) + + # A made-up str username should not be authorized + self.assertFalse(gb.userok(canon_name, 'not_a_real_user_name')) + + @ktu.gssapi_extension_test('localname', 'Local Name') + def test_authorize_localname(self): + base_name = gb.import_name(self.USER_PRINC, + gb.NameType.kerberos_principal) + canon_name = gb.canonicalize_name(base_name, gb.MechType.kerberos) + + local = gb.localname(canon_name, gb.MechType.kerberos) + local_name = gb.import_name(local, gb.NameType.user) + self.assertTrue(gb.authorize_localname(canon_name, local_name)) + + @ktu.gssapi_extension_test('localname', 'Local Name') + def test_authorize_localname_fails(self): + base_name = gb.import_name(self.USER_PRINC, + gb.NameType.kerberos_principal) + canon_name = gb.canonicalize_name(base_name, gb.MechType.kerberos) + + fake_local_name = gb.import_name(b'not_a_real_user_name', + gb.NameType.user) + self.assertRaises(gb.GSSError, gb.authorize_localname, + canon_name, fake_local_name) + + @ktu.gssapi_extension_test('localname', 'Local Name') + def test_pname_to_uid(self): + base_name = gb.import_name(self.USER_PRINC, + gb.NameType.kerberos_principal) + canon_name = gb.canonicalize_name(base_name, gb.MechType.kerberos) + + uid = gb.pname_to_uid(canon_name, gb.MechType.kerberos) + self.assertIsInstance(uid, int) + self.assertGreaterEqual(uid, 0) + class TestIntEnumFlagSet(unittest.TestCase): def test_create_from_int(self): diff --git a/setup.py b/setup.py index af21380e..8e3e3423 100755 --- a/setup.py +++ b/setup.py @@ -376,6 +376,9 @@ def gssapi_modules(lst): extension_file('password_add', 'gss_add_cred_with_password'), extension_file('krb5', 'gss_krb5_ccache_name'), + + extension_file('localname', 'gss_localname'), + extension_file('localname_attr', 'GSS_C_ATTR_LOCAL_LOGIN_USER'), ]), options=setup_options, keywords=['gssapi', 'security'],