Skip to content

Commit d00deea

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 2b77959 commit d00deea

File tree

2 files changed

+276
-15
lines changed

2 files changed

+276
-15
lines changed

gssapi/names.py

+176-15
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
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+
712

813
class Name(rname.Name):
914
"""GSSAPI Name
@@ -20,9 +25,31 @@ class Name(rname.Name):
2025
text of the name.
2126
"""
2227

23-
__slots__ = ()
28+
__slots__ = ('_attr_obj')
29+
30+
def __new__(cls, base=None, name_type=None, token=None,
31+
composite=False):
32+
if token is not None:
33+
if composite:
34+
if rname_rfc6680 is None:
35+
raise NotImplementedError(
36+
"Your GSSAPI implementation does not support RFC 6680 "
37+
"(the GSSAPI naming extensions)")
38+
39+
base_name = rname.import_name(token, NameType.composite_export)
40+
else:
41+
base_name = rname.import_name(token, NameType.export)
42+
elif isinstance(base, rname.Name):
43+
base_name = base
44+
else:
45+
if isinstance(base, six.text_type):
46+
base = base.encode(_utils._get_encoding())
47+
48+
base_name = rname.import_name(base, name_type)
2449

25-
def __new__(cls, base=None, name_type=None, token=None):
50+
return super(Name, cls).__new__(cls, base_name)
51+
52+
def __init__(self, base=None, name_type=None, token=None, composite=False):
2653
"""Create or import a GSSAPI name
2754
2855
The constructor either creates or imports a GSSAPI name.
@@ -32,7 +59,8 @@ def __new__(cls, base=None, name_type=None, token=None):
3259
high-level object.
3360
3461
If the `token` argument is used, the name will be imported using
35-
the token.
62+
the token. If the token was exported as a composite token,
63+
pass `composite=True`.
3664
3765
Otherwise, a new name will be created, using the `base` argument as
3866
the string and the `name_type` argument to denote the name type.
@@ -43,17 +71,10 @@ def __new__(cls, base=None, name_type=None, token=None):
4371
BadMechanismError
4472
"""
4573

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
74+
if rname_rfc6680 is not None:
75+
self._attr_obj = _NameAttributeMapping(self)
5076
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)
77+
self._attr_obj = None
5778

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

95+
def display_as(self, name_type):
96+
"""
97+
Display the current name as the given name type.
98+
99+
This method attempts to display the current Name using
100+
the syntax of the given NameType, if possible.
101+
102+
Args:
103+
name_type (OID): the NameType to use to display the given name
104+
105+
Returns:
106+
str: the displayed name
107+
108+
Raises:
109+
OperationUnavailableError
110+
"""
111+
112+
if rname_rfc6680 is None:
113+
raise NotImplementedError("Your GSSAPI implementation does not "
114+
"support RFC 6680 (the GSSAPI naming "
115+
"extensions)")
116+
return rname_rfc6680.display_name_ext(self, name_type).encode(
117+
_utils.get_encoding())
118+
74119
@property
75120
def name_type(self):
76121
"""Get the name type of this name"""
@@ -92,7 +137,7 @@ def __repr__(self):
92137
return "Name({name}, {name_type})".format(name=disp_res.name,
93138
name_type=disp_res.name_type)
94139

95-
def export(self):
140+
def export(self, composite=False):
96141
"""Export the name
97142
98143
This method exports the name into a byte string which can then be
@@ -107,7 +152,15 @@ def export(self):
107152
BadNameError
108153
"""
109154

110-
return rname.export_name(self)
155+
if composite:
156+
if rname_rfc6680 is None:
157+
raise NotImplementedError("Your GSSAPI implementation does "
158+
"not support RFC 6680 (the GSSAPI "
159+
"naming extensions)")
160+
161+
return rname_rfc6680.export_name_composite(self)
162+
else:
163+
return rname.export_name(self)
111164

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

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

gssapi/tests/test_high_level.py

+100
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)