Skip to content

Commit 16f0102

Browse files
committed
Implement RFC 6680 (High-Level)
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
1 parent 03863fd commit 16f0102

File tree

2 files changed

+291
-15
lines changed

2 files changed

+291
-15
lines changed

gssapi/names.py

Lines changed: 191 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
import collections
2+
13
import six
24

35
from gssapi.raw import names as rname
46
from gssapi.raw import NameType
7+
from gssapi.raw import named_tuples as tuples
58
from gssapi import _utils
69

10+
rname_rfc6680 = _utils.import_gssapi_extension('rfc6680')
11+
rname_rfc6680_comp_oid = _utils.import_gssapi_extension('rfc6680_comp_oid')
12+
713

814
class Name(rname.Name):
915
"""GSSAPI Name
@@ -20,9 +26,45 @@ class Name(rname.Name):
2026
text of the name.
2127
"""
2228

23-
__slots__ = ()
29+
__slots__ = ('_attr_obj')
30+
31+
def __new__(cls, base=None, name_type=None, token=None,
32+
composite=False):
33+
if token is not None:
34+
if composite:
35+
if rname_rfc6680 is None:
36+
raise NotImplementedError(
37+
"Your GSSAPI implementation does not support RFC 6680 "
38+
"(the GSSAPI naming extensions)")
39+
40+
if rname_rfc6680_comp_oid is not None:
41+
base_name = rname.import_name(token,
42+
NameType.composite_export)
43+
displ_name = rname.display_name(base_name, name_type=True)
44+
if displ_name.name_type == NameType.composite_export:
45+
# NB(directxman12): there's a bug in MIT krb5 <= 1.13
46+
# where GSS_C_NT_COMPOSITE_EXPORT doesn't trigger
47+
# immediate import logic. However, we can just use
48+
# the normal GSS_C_NT_EXPORT_NAME in this case.
49+
base_name = rname.import_name(token, NameType.export)
50+
else:
51+
# NB(directxman12): some older versions of MIT krb5 don't
52+
# have support for the GSS_C_NT_COMPOSITE_EXPORT, but do
53+
# support composite tokens via GSS_C_NT_EXPORT_NAME.
54+
base_name = rname.import_name(token, NameType.export)
55+
else:
56+
base_name = rname.import_name(token, NameType.export)
57+
elif isinstance(base, rname.Name):
58+
base_name = base
59+
else:
60+
if isinstance(base, six.text_type):
61+
base = base.encode(_utils._get_encoding())
62+
63+
base_name = rname.import_name(base, name_type)
2464

25-
def __new__(cls, base=None, name_type=None, token=None):
65+
return super(Name, cls).__new__(cls, base_name)
66+
67+
def __init__(self, base=None, name_type=None, token=None, composite=False):
2668
"""Create or import a GSSAPI name
2769
2870
The constructor either creates or imports a GSSAPI name.
@@ -32,7 +74,8 @@ def __new__(cls, base=None, name_type=None, token=None):
3274
high-level object.
3375
3476
If the `token` argument is used, the name will be imported using
35-
the token.
77+
the token. If the token was exported as a composite token,
78+
pass `composite=True`.
3679
3780
Otherwise, a new name will be created, using the `base` argument as
3881
the string and the `name_type` argument to denote the name type.
@@ -43,17 +86,10 @@ def __new__(cls, base=None, name_type=None, token=None):
4386
BadMechanismError
4487
"""
4588

46-
if token is not None:
47-
base_name = rname.import_name(token, NameType.export)
48-
elif isinstance(base, rname.Name):
49-
base_name = base
89+
if rname_rfc6680 is not None:
90+
self._attr_obj = _NameAttributeMapping(self)
5091
else:
51-
if isinstance(base, six.text_type):
52-
base = base.encode(_utils._get_encoding())
53-
54-
base_name = rname.import_name(base, name_type)
55-
56-
return super(Name, cls).__new__(cls, base_name)
92+
self._attr_obj = None
5793

5894
def __str__(self):
5995
if issubclass(str, six.text_type):
@@ -71,6 +107,30 @@ def __bytes__(self):
71107
# Python 3 -- someone asked for bytes
72108
return rname.display_name(self, name_type=False).name
73109

110+
def display_as(self, name_type):
111+
"""
112+
Display the current name as the given name type.
113+
114+
This method attempts to display the current Name using
115+
the syntax of the given NameType, if possible.
116+
117+
Args:
118+
name_type (OID): the NameType to use to display the given name
119+
120+
Returns:
121+
str: the displayed name
122+
123+
Raises:
124+
OperationUnavailableError
125+
"""
126+
127+
if rname_rfc6680 is None:
128+
raise NotImplementedError("Your GSSAPI implementation does not "
129+
"support RFC 6680 (the GSSAPI naming "
130+
"extensions)")
131+
return rname_rfc6680.display_name_ext(self, name_type).encode(
132+
_utils.get_encoding())
133+
74134
@property
75135
def name_type(self):
76136
"""Get the name type of this name"""
@@ -92,7 +152,7 @@ def __repr__(self):
92152
return "Name({name}, {name_type})".format(name=disp_res.name,
93153
name_type=disp_res.name_type)
94154

95-
def export(self):
155+
def export(self, composite=False):
96156
"""Export the name
97157
98158
This method exports the name into a byte string which can then be
@@ -107,7 +167,15 @@ def export(self):
107167
BadNameError
108168
"""
109169

110-
return rname.export_name(self)
170+
if composite:
171+
if rname_rfc6680 is None:
172+
raise NotImplementedError("Your GSSAPI implementation does "
173+
"not support RFC 6680 (the GSSAPI "
174+
"naming extensions)")
175+
176+
return rname_rfc6680.export_name_composite(self)
177+
else:
178+
return rname.export_name(self)
111179

112180
def canonicalize(self, mech):
113181
"""Canonicalize a name with respect to a mechanism
@@ -134,3 +202,111 @@ def __copy__(self):
134202

135203
def __deepcopy__(self, memo):
136204
return type(self)(rname.duplicate_name(self))
205+
206+
def _inquire(self, **kwargs):
207+
"""Inspect the name for information
208+
209+
This method inspects the name for information.
210+
211+
If no keyword arguments are passed, all available information
212+
is returned. Otherwise, only the keyword arguments that
213+
are passed and set to `True` are returned.
214+
215+
Args:
216+
mech_name (bool): get whether this is a mechanism name,
217+
and, if so, the associated mechanism
218+
attrs (bool): get the attributes names for this name
219+
220+
Returns:
221+
InquireNameResult: the results of the inquiry, with unused
222+
fields set to None
223+
224+
Raises:
225+
GSSError
226+
"""
227+
228+
if rname_rfc6680 is None:
229+
raise NotImplementedError("Your GSSAPI implementation does not "
230+
"support RFC 6680 (the GSSAPI naming "
231+
"extensions)")
232+
233+
if not kwargs:
234+
default_val = True
235+
else:
236+
default_val = False
237+
238+
attrs = kwargs.get('attrs', default_val)
239+
mech_name = kwargs.get('mech_name', default_val)
240+
241+
return rname_rfc6680.inquire_name(self, mech_name=mech_name,
242+
attrs=attrs)
243+
244+
@property
245+
def is_mech_name(self):
246+
return self._inquire(mech_name=True).is_mech_name
247+
248+
@property
249+
def mech(self):
250+
return self._inquire(mech_name=True).mech
251+
252+
@property
253+
def attributes(self):
254+
if self._attr_obj is None:
255+
raise NotImplementedError("Your GSSAPI implementation does not "
256+
"support RFC 6680 (the GSSAPI naming "
257+
"extensions)")
258+
259+
return self._attr_obj
260+
261+
262+
class _NameAttributeMapping(collections.MutableMapping):
263+
264+
"""Provides dict-like access to RFC 6680 Name attributes."""
265+
def __init__(self, name):
266+
self._name = name
267+
268+
def __getitem__(self, key):
269+
if isinstance(key, six.text_type):
270+
key = key.encode(_utils._get_encoding())
271+
272+
res = rname_rfc6680.get_name_attribute(self._name, key)
273+
return tuples.GetNameAttributeResult(frozenset(res.values),
274+
frozenset(res.display_values),
275+
res.authenticated,
276+
res.complete)
277+
278+
def __setitem__(self, key, value):
279+
if isinstance(key, six.text_type):
280+
key = key.encode(_utils._get_encoding())
281+
282+
rname_rfc6680.delete_name_attribute(self._name, key)
283+
284+
if isinstance(value, tuples.GetNameAttributeResult):
285+
complete = value.complete
286+
value = value.values
287+
elif isinstance(value, tuple) and len(value) == 2:
288+
complete = value[1]
289+
value = value[0]
290+
else:
291+
complete = False
292+
293+
if (isinstance(value, (six.string_types, bytes)) or
294+
not isinstance(value, collections.Iterable)):
295+
# NB(directxman12): this allows us to easily assign a single
296+
# value, since that's a common case
297+
value = [value]
298+
299+
rname_rfc6680.set_name_attribute(self._name, key, value,
300+
complete=complete)
301+
302+
def __delitem__(self, key):
303+
if isinstance(key, six.text_type):
304+
key = key.encode(_utils._get_encoding())
305+
306+
rname_rfc6680.delete_name_attribute(self._name, key)
307+
308+
def __iter__(self):
309+
return iter(self._name._inquire(attrs=True).attrs)
310+
311+
def __len__(self):
312+
return len(self._name._inquire(attrs=True).attrs)

gssapi/tests/test_high_level.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from gssapi import _utils as gssutils
1616
from gssapi import exceptions as excs
1717
from gssapi.tests._utils import _extension_test, _minversion_test
18+
from gssapi.tests._utils import _requires_krb_plugin
1819
from gssapi.tests import k5test as kt
1920

2021

@@ -395,6 +396,45 @@ def test_create_from_token(self):
395396
name2.shouldnt_be_none()
396397
name2.name_type.should_be(gb.NameType.kerberos_principal)
397398

399+
@_extension_test('rfc6680', 'RFC 6680')
400+
def test_create_from_composite_token_no_attrs(self):
401+
name1 = gssnames.Name(TARGET_SERVICE_NAME,
402+
gb.NameType.hostbased_service)
403+
exported_name = name1.canonicalize(
404+
gb.MechType.kerberos).export(composite=True)
405+
name2 = gssnames.Name(token=exported_name, composite=True)
406+
407+
name2.shouldnt_be_none()
408+
409+
@_extension_test('rfc6680', 'RFC 6680')
410+
@_requires_krb_plugin('authdata', 'greet_client')
411+
def test_create_from_composite_token_with_attrs(self):
412+
name1 = gssnames.Name(TARGET_SERVICE_NAME,
413+
gb.NameType.hostbased_service)
414+
415+
canon_name = name1.canonicalize(gb.MechType.kerberos)
416+
canon_name.attributes['urn:greet:greeting'] = b'some val'
417+
418+
exported_name = canon_name.export(composite=True)
419+
420+
# TODO(directxman12): when you just import a token as composite,
421+
# appears as this name whose text is all garbled, since it contains
422+
# all of the attributes, etc, but doesn't properly have the attributes.
423+
# Once it's canonicalized, the attributes reappear. However, if you
424+
# just import it as normal export, the attributes appear directly.
425+
# It is thus unclear as to what is going on
426+
# name2_raw = gssnames.Name(token=exported_name, composite=True)
427+
# name2 = name2_raw.canonicalize(gb.MechType.kerberos)
428+
429+
name2 = gssnames.Name(token=exported_name)
430+
431+
name2.shouldnt_be_none()
432+
433+
name2.attributes['urn:greet:greeting'].values.should_be(
434+
set([b'some val']))
435+
name2.attributes['urn:greet:greeting'].complete.should_be_true()
436+
name2.attributes['urn:greet:greeting'].authenticated.should_be_false()
437+
398438
def test_to_str(self):
399439
name = gssnames.Name(SERVICE_PRINCIPAL, gb.NameType.kerberos_principal)
400440

@@ -455,6 +495,66 @@ def test_copy(self):
455495

456496
name1.should_be(name2)
457497

498+
# NB(directxman12): we don't test display_name_ext because the krb5 mech
499+
# doesn't actually implement it
500+
501+
@_extension_test('rfc6680', 'RFC 6680')
502+
def test_is_mech_name(self):
503+
name = gssnames.Name(TARGET_SERVICE_NAME,
504+
gb.NameType.hostbased_service)
505+
506+
name.is_mech_name.should_be_false()
507+
508+
canon_name = name.canonicalize(gb.MechType.kerberos)
509+
510+
canon_name.is_mech_name.should_be_true()
511+
canon_name.mech.should_be_a(gb.OID)
512+
canon_name.mech.should_be(gb.MechType.kerberos)
513+
514+
@_extension_test('rfc6680', 'RFC 6680')
515+
def test_export_name_composite_no_attrs(self):
516+
name = gssnames.Name(TARGET_SERVICE_NAME,
517+
gb.NameType.hostbased_service)
518+
canon_name = name.canonicalize(gb.MechType.kerberos)
519+
exported_name = canon_name.export(composite=True)
520+
521+
exported_name.should_be_a(bytes)
522+
523+
@_extension_test('rfc6680', 'RFC 6680')
524+
@_requires_krb_plugin('authdata', 'greet_client')
525+
def test_export_name_composite_with_attrs(self):
526+
name = gssnames.Name(TARGET_SERVICE_NAME,
527+
gb.NameType.hostbased_service)
528+
canon_name = name.canonicalize(gb.MechType.kerberos)
529+
canon_name.attributes['urn:greet:greeting'] = b'some val'
530+
exported_name = canon_name.export(composite=True)
531+
532+
exported_name.should_be_a(bytes)
533+
534+
@_extension_test('rfc6680', 'RFC 6680')
535+
@_requires_krb_plugin('authdata', 'greet_client')
536+
def test_basic_get_set_del_name_attribute_no_auth(self):
537+
name = gssnames.Name(TARGET_SERVICE_NAME,
538+
gb.NameType.hostbased_service)
539+
canon_name = name.canonicalize(gb.MechType.kerberos)
540+
541+
canon_name.attributes['urn:greet:greeting'] = (b'some val', True)
542+
canon_name.attributes['urn:greet:greeting'].values.should_be(
543+
set([b'some val']))
544+
canon_name.attributes['urn:greet:greeting'].complete.should_be_true()
545+
(canon_name.attributes['urn:greet:greeting'].authenticated
546+
.should_be_false())
547+
548+
del canon_name.attributes['urn:greet:greeting']
549+
550+
# NB(directxman12): for some reason, the greet:greeting handler plugin
551+
# doesn't properly delete itself -- it just clears the value
552+
# If we try to get its value now, we segfault (due to an issue with
553+
# greet:greeting's delete). Instead, just try setting the value again
554+
# canon_name.attributes.should_be_empty(), which would normally give
555+
# an error.
556+
canon_name.attributes['urn:greet:greeting'] = b'some other val'
557+
458558

459559
class SecurityContextTestCase(_GSSAPIKerberosTestCase):
460560
def setUp(self):

0 commit comments

Comments
 (0)