Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions gssapi/raw/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,16 @@
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

# optional pname_to_uid support (not available on Windows)
try:
from gssapi.raw.ext_pname_to_uid import * # noqa
except ImportError:
pass
68 changes: 68 additions & 0 deletions gssapi/raw/ext_localname.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import typing as t

if t.TYPE_CHECKING:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this check, I don't think the names and oids files will need to import this ext file and we shouldn't need to use the t.TYPE_CHECKING conditional import.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was following the pattern I observed in the other ext_*.pyi files within the same directory, but perhaps I've overlooked some other aspect that establishes the necessity for this block. What's the criteria for including/not including?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fairly certain it's only there when there are circular imports, e.g. names or oids import this file. The __init__.py is somewhat different enough that maybe it is required here but I wouldn't have thought so.

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
"""
113 changes: 113 additions & 0 deletions gssapi/raw/ext_localname.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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

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


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 = (<char*>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())
Comment on lines +77 to +78

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a benefit to accepting both bytes and str here? I would have thought we would just use bytes but it's been a while since I've looked at what some of the other functions are doing.


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)
7 changes: 7 additions & 0 deletions gssapi/raw/ext_localname_attr.pyi
Original file line number Diff line number Diff line change
@@ -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.
"""
13 changes: 13 additions & 0 deletions gssapi/raw/ext_localname_attr.pyx
Original file line number Diff line number Diff line change
@@ -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 = (<char*>GSS_C_ATTR_LOCAL_LOGIN_USER.value)[
:GSS_C_ATTR_LOCAL_LOGIN_USER.length]
30 changes: 30 additions & 0 deletions gssapi/raw/ext_pname_to_uid.pyi
Original file line number Diff line number Diff line change
@@ -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
"""
51 changes: 51 additions & 0 deletions gssapi/raw/ext_pname_to_uid.pyx
Original file line number Diff line number Diff line change
@@ -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)
77 changes: 77 additions & 0 deletions gssapi/tests/test_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('pname_to_uid', 'pname_to_uid')
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):
Expand Down
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@ 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'),
extension_file('pname_to_uid', 'gss_pname_to_uid'),
]),
options=setup_options,
keywords=['gssapi', 'security'],
Expand Down
Loading