From 39b3677d622d7cf5f9af037325779bd7f096764b Mon Sep 17 00:00:00 2001 From: Jacob Henner Date: Thu, 18 Jun 2026 16:40:15 -0400 Subject: [PATCH 1/2] 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'], From 2359d720b2549f8d1fff3be62d0e576a0aab2752 Mon Sep 17 00:00:00 2001 From: Jacob Henner Date: Mon, 22 Jun 2026 15:18:43 -0400 Subject: [PATCH 2/2] Split pname_to_uid into its own module Not available on Windows. Include it conditionally to prevent build errors on Windows. Signed-off-by: Jacob Henner --- gssapi/raw/__init__.py | 6 ++++ gssapi/raw/ext_localname.pyi | 25 ---------------- gssapi/raw/ext_localname.pyx | 43 --------------------------- gssapi/raw/ext_pname_to_uid.pyi | 30 +++++++++++++++++++ gssapi/raw/ext_pname_to_uid.pyx | 51 +++++++++++++++++++++++++++++++++ gssapi/tests/test_raw.py | 2 +- setup.py | 1 + 7 files changed, 89 insertions(+), 69 deletions(-) create mode 100644 gssapi/raw/ext_pname_to_uid.pyi create mode 100644 gssapi/raw/ext_pname_to_uid.pyx diff --git a/gssapi/raw/__init__.py b/gssapi/raw/__init__.py index 88f19b8f..3d738b3e 100644 --- a/gssapi/raw/__init__.py +++ b/gssapi/raw/__init__.py @@ -156,3 +156,9 @@ from gssapi.raw.ext_localname_attr import * # noqa except ImportError: pass + +# optional pname_to_uid support (not available on Windows) +try: + from gssapi.raw.ext_pname_to_uid import * # noqa +except ImportError: + pass diff --git a/gssapi/raw/ext_localname.pyi b/gssapi/raw/ext_localname.pyi index 9c5c1935..b912939b 100644 --- a/gssapi/raw/ext_localname.pyi +++ b/gssapi/raw/ext_localname.pyi @@ -66,28 +66,3 @@ def authorize_localname( 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 index 81fec8c6..b1f59004 100644 --- a/gssapi/raw/ext_localname.pyx +++ b/gssapi/raw/ext_localname.pyx @@ -7,8 +7,6 @@ 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, @@ -22,11 +20,6 @@ cdef extern from "python_gssapi_ext.h": 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. @@ -118,39 +111,3 @@ def authorize_localname(Name name not None, Name user not None): 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_pname_to_uid.pyi b/gssapi/raw/ext_pname_to_uid.pyi new file mode 100644 index 00000000..6d1fef16 --- /dev/null +++ b/gssapi/raw/ext_pname_to_uid.pyi @@ -0,0 +1,30 @@ +import typing as t + +if t.TYPE_CHECKING: + from gssapi.raw.names import Name + from gssapi.raw.oids import OID + + +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_pname_to_uid.pyx b/gssapi/raw/ext_pname_to_uid.pyx new file mode 100644 index 00000000..f85f636f --- /dev/null +++ b/gssapi/raw/ext_pname_to_uid.pyx @@ -0,0 +1,51 @@ +GSSAPI="BASE" # This ensures that a full module is generated by Cython + +from posix.types cimport uid_t + +from gssapi.raw.cython_types cimport * +from gssapi.raw.names cimport Name +from gssapi.raw.oids cimport OID + +from gssapi.raw.misc import GSSError + +cdef extern from "python_gssapi_ext.h": + 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 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/tests/test_raw.py b/gssapi/tests/test_raw.py index 56571332..4bfc5b04 100644 --- a/gssapi/tests/test_raw.py +++ b/gssapi/tests/test_raw.py @@ -1376,7 +1376,7 @@ def test_authorize_localname_fails(self): self.assertRaises(gb.GSSError, gb.authorize_localname, canon_name, fake_local_name) - @ktu.gssapi_extension_test('localname', 'Local Name') + @ktu.gssapi_extension_test('pname_to_uid', 'pname_to_uid') def test_pname_to_uid(self): base_name = gb.import_name(self.USER_PRINC, gb.NameType.kerberos_principal) diff --git a/setup.py b/setup.py index 8e3e3423..aeb09647 100755 --- a/setup.py +++ b/setup.py @@ -379,6 +379,7 @@ def gssapi_modules(lst): extension_file('localname', 'gss_localname'), extension_file('localname_attr', 'GSS_C_ATTR_LOCAL_LOGIN_USER'), + extension_file('pname_to_uid', 'gss_pname_to_uid'), ]), options=setup_options, keywords=['gssapi', 'security'],