Skip to content

Commit

Permalink
Implement RFC 6680 (High-Level)
Browse files Browse the repository at this point in the history
This commit introduces optional support for RFC 6680 to
the high-level API.

For attribute access, a new property named `attributes` was
introduced to the Name class.  This presents a `MutableMapping`
interface to the Name's attributes.  When iterables are assigned
to attributes (not including strings and bytes), they are considered
to be multiple values to be assigned to the attribute.  Additionally,
attribute names (but not values) are automatically encoded if they
are in text (and not bytes) form

For inquiry, appropriate properties were added to the `Name` class
(`is_mech_name` and `mech`).  `display_as` may be used to call
`display_name_ext`, and a `composite` argument was introduced to
both the `export` method and the constructor.

Closes #4
  • Loading branch information
DirectXMan12 committed Mar 5, 2015
1 parent 6c3820b commit 28b897f
Show file tree
Hide file tree
Showing 2 changed files with 276 additions and 15 deletions.
191 changes: 176 additions & 15 deletions gssapi/names.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import collections

import six

from gssapi.raw import names as rname
from gssapi.raw import NameType
from gssapi.raw import named_tuples as tuples
from gssapi import _utils

rname_rfc6680 = _utils.import_gssapi_extension('rfc6680')


class Name(rname.Name):
"""GSSAPI Name
Expand All @@ -20,9 +25,31 @@ class Name(rname.Name):
text of the name.
"""

__slots__ = ()
__slots__ = ('_attr_obj')

def __new__(cls, base=None, name_type=None, token=None,
composite=False):
if token is not None:
if composite:
if rname_rfc6680 is None:
raise NotImplementedError(
"Your GSSAPI implementation does not support RFC 6680 "
"(the GSSAPI naming extensions)")

base_name = rname.import_name(token, NameType.composite_export)
else:
base_name = rname.import_name(token, NameType.export)
elif isinstance(base, rname.Name):
base_name = base
else:
if isinstance(base, six.text_type):
base = base.encode(_utils._get_encoding())

base_name = rname.import_name(base, name_type)

def __new__(cls, base=None, name_type=None, token=None):
return super(Name, cls).__new__(cls, base_name)

def __init__(self, base=None, name_type=None, token=None, composite=False):
"""Create or import a GSSAPI name
The constructor either creates or imports a GSSAPI name.
Expand All @@ -32,7 +59,8 @@ def __new__(cls, base=None, name_type=None, token=None):
high-level object.
If the `token` argument is used, the name will be imported using
the token.
the token. If the token was exported as a composite token,
pass `composite=True`.
Otherwise, a new name will be created, using the `base` argument as
the string and the `name_type` argument to denote the name type.
Expand All @@ -43,17 +71,10 @@ def __new__(cls, base=None, name_type=None, token=None):
BadMechanismError
"""

if token is not None:
base_name = rname.import_name(token, NameType.export)
elif isinstance(base, rname.Name):
base_name = base
if rname_rfc6680 is not None:
self._attr_obj = _NameAttributeMapping(self)
else:
if isinstance(base, six.text_type):
base = base.encode(_utils._get_encoding())

base_name = rname.import_name(base, name_type)

return super(Name, cls).__new__(cls, base_name)
self._attr_obj = None

def __str__(self):
if issubclass(str, six.text_type):
Expand All @@ -71,6 +92,30 @@ def __bytes__(self):
# Python 3 -- someone asked for bytes
return rname.display_name(self, name_type=False).name

def display_as(self, name_type):
"""
Display the current name as the given name type.
This method attempts to display the current Name using
the syntax of the given NameType, if possible.
Args:
name_type (OID): the NameType to use to display the given name
Returns:
str: the displayed name
Raises:
OperationUnavailableError
"""

if rname_rfc6680 is None:
raise NotImplementedError("Your GSSAPI implementation does not "
"support RFC 6680 (the GSSAPI naming "
"extensions)")
return rname_rfc6680.display_name_ext(self, name_type).encode(
_utils.get_encoding())

@property
def name_type(self):
"""Get the name type of this name"""
Expand All @@ -92,7 +137,7 @@ def __repr__(self):
return "Name({name}, {name_type})".format(name=disp_res.name,
name_type=disp_res.name_type)

def export(self):
def export(self, composite=False):
"""Export the name
This method exports the name into a byte string which can then be
Expand All @@ -107,7 +152,15 @@ def export(self):
BadNameError
"""

return rname.export_name(self)
if composite:
if rname_rfc6680 is None:
raise NotImplementedError("Your GSSAPI implementation does "
"not support RFC 6680 (the GSSAPI "
"naming extensions)")

return rname_rfc6680.export_name_composite(self)
else:
return rname.export_name(self)

def canonicalize(self, mech):
"""Canonicalize a name with respect to a mechanism
Expand All @@ -134,3 +187,111 @@ def __copy__(self):

def __deepcopy__(self, memo):
return type(self)(rname.duplicate_name(self))

def _inquire(self, **kwargs):
"""Inspect the name for information
This method inspects the name for information.
If no keyword arguments are passed, all available information
is returned. Otherwise, only the keyword arguments that
are passed and set to `True` are returned.
Args:
mech_name (bool): get whether this is a mechanism name,
and, if so, the associated mechanism
attrs (bool): get the attributes names for this name
Returns:
InquireNameResult: the results of the inquiry, with unused
fields set to None
Raises:
GSSError
"""

if rname_rfc6680 is None:
raise NotImplementedError("Your GSSAPI implementation does not "
"support RFC 6680 (the GSSAPI naming "
"extensions)")

if not kwargs:
default_val = True
else:
default_val = False

attrs = kwargs.get('attrs', default_val)
mech_name = kwargs.get('mech_name', default_val)

return rname_rfc6680.inquire_name(self, mech_name=mech_name,
attrs=attrs)

@property
def is_mech_name(self):
return self._inquire(mech_name=True).is_mech_name

@property
def mech(self):
return self._inquire(mech_name=True).mech

@property
def attributes(self):
if self._attr_obj is None:
raise NotImplementedError("Your GSSAPI implementation does not "
"support RFC 6680 (the GSSAPI naming "
"extensions)")

return self._attr_obj


class _NameAttributeMapping(collections.MutableMapping):

"""Provides dict-like access to RFC 6680 Name attributes."""
def __init__(self, name):
self._name = name

def __getitem__(self, key):
if isinstance(key, six.text_type):
key = key.encode(_utils._get_encoding())

res = rname_rfc6680.get_name_attribute(self._name, key)
return tuples.GetNameAttributeResult(frozenset(res.values),
frozenset(res.display_values),
res.authenticated,
res.complete)

def __setitem__(self, key, value):
if isinstance(key, six.text_type):
key = key.encode(_utils._get_encoding())

rname_rfc6680.delete_name_attribute(self._name, key)

if isinstance(value, tuples.GetNameAttributeResult):
complete = value.complete
value = value.values
elif isinstance(value, tuple) and len(value) == 2:
complete = value[1]
value = value[0]
else:
complete = False

if (isinstance(value, (six.string_types, bytes)) or
not isinstance(value, collections.Iterable)):
# NB(directxman12): this allows us to easily assign a single
# value, since that's a common case
value = [value]

rname_rfc6680.set_name_attribute(self._name, key, value,
complete=complete)

def __delitem__(self, key):
if isinstance(key, six.text_type):
key = key.encode(_utils._get_encoding())

rname_rfc6680.delete_name_attribute(self._name, key)

def __iter__(self):
return iter(self._name._inquire(attrs=True).attrs)

def __len__(self):
return len(self._name._inquire(attrs=True).attrs)
100 changes: 100 additions & 0 deletions gssapi/tests/test_high_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from gssapi import _utils as gssutils
from gssapi import exceptions as excs
from gssapi.tests._utils import _extension_test, _minversion_test
from gssapi.tests._utils import _requires_krb_plugin
from gssapi.tests import k5test as kt


Expand Down Expand Up @@ -395,6 +396,45 @@ def test_create_from_token(self):
name2.shouldnt_be_none()
name2.name_type.should_be(gb.NameType.kerberos_principal)

@_extension_test('rfc6680', 'RFC 6680')
def test_create_from_composite_token_no_attrs(self):
name1 = gssnames.Name(TARGET_SERVICE_NAME,
gb.NameType.hostbased_service)
exported_name = name1.canonicalize(
gb.MechType.kerberos).export(composite=True)
name2 = gssnames.Name(token=exported_name, composite=True)

name2.shouldnt_be_none()

@_extension_test('rfc6680', 'RFC 6680')
@_requires_krb_plugin('authdata', 'greet_client')
def test_create_from_composite_token_with_attrs(self):
name1 = gssnames.Name(TARGET_SERVICE_NAME,
gb.NameType.hostbased_service)

canon_name = name1.canonicalize(gb.MechType.kerberos)
canon_name.attributes['urn:greet:greeting'] = b'some val'

exported_name = canon_name.export(composite=True)

# TODO(directxman12): when you just import a token as composite,
# appears as this name whose text is all garbled, since it contains
# all of the attributes, etc, but doesn't properly have the attributes.
# Once it's canonicalized, the attributes reappear. However, if you
# just import it as normal export, the attributes appear directly.
# It is thus unclear as to what is going on
# name2_raw = gssnames.Name(token=exported_name, composite=True)
# name2 = name2_raw.canonicalize(gb.MechType.kerberos)

name2 = gssnames.Name(token=exported_name)

name2.shouldnt_be_none()

name2.attributes['urn:greet:greeting'].values.should_be(
set([b'some val']))
name2.attributes['urn:greet:greeting'].complete.should_be_true()
name2.attributes['urn:greet:greeting'].authenticated.should_be_false()

def test_to_str(self):
name = gssnames.Name(SERVICE_PRINCIPAL, gb.NameType.kerberos_principal)

Expand Down Expand Up @@ -455,6 +495,66 @@ def test_copy(self):

name1.should_be(name2)

# NB(directxman12): we don't test display_name_ext because the krb5 mech
# doesn't actually implement it

@_extension_test('rfc6680', 'RFC 6680')
def test_is_mech_name(self):
name = gssnames.Name(TARGET_SERVICE_NAME,
gb.NameType.hostbased_service)

name.is_mech_name.should_be_false()

canon_name = name.canonicalize(gb.MechType.kerberos)

canon_name.is_mech_name.should_be_true()
canon_name.mech.should_be_a(gb.OID)
canon_name.mech.should_be(gb.MechType.kerberos)

@_extension_test('rfc6680', 'RFC 6680')
def test_export_name_composite_no_attrs(self):
name = gssnames.Name(TARGET_SERVICE_NAME,
gb.NameType.hostbased_service)
canon_name = name.canonicalize(gb.MechType.kerberos)
exported_name = canon_name.export(composite=True)

exported_name.should_be_a(bytes)

@_extension_test('rfc6680', 'RFC 6680')
@_requires_krb_plugin('authdata', 'greet_client')
def test_export_name_composite_with_attrs(self):
name = gssnames.Name(TARGET_SERVICE_NAME,
gb.NameType.hostbased_service)
canon_name = name.canonicalize(gb.MechType.kerberos)
canon_name.attributes['urn:greet:greeting'] = b'some val'
exported_name = canon_name.export(composite=True)

exported_name.should_be_a(bytes)

@_extension_test('rfc6680', 'RFC 6680')
@_requires_krb_plugin('authdata', 'greet_client')
def test_basic_get_set_del_name_attribute_no_auth(self):
name = gssnames.Name(TARGET_SERVICE_NAME,
gb.NameType.hostbased_service)
canon_name = name.canonicalize(gb.MechType.kerberos)

canon_name.attributes['urn:greet:greeting'] = (b'some val', True)
canon_name.attributes['urn:greet:greeting'].values.should_be(
set([b'some val']))
canon_name.attributes['urn:greet:greeting'].complete.should_be_true()
(canon_name.attributes['urn:greet:greeting'].authenticated
.should_be_false())

del canon_name.attributes['urn:greet:greeting']

# NB(directxman12): for some reason, the greet:greeting handler plugin
# doesn't properly delete itself -- it just clears the value
# If we try to get its value now, we segfault (due to an issue with
# greet:greeting's delete). Instead, just try setting the value again
# canon_name.attributes.should_be_empty(), which would normally give
# an error.
canon_name.attributes['urn:greet:greeting'] = b'some other val'


class SecurityContextTestCase(_GSSAPIKerberosTestCase):
def setUp(self):
Expand Down

0 comments on commit 28b897f

Please sign in to comment.