From de6abc7af2dac7994b0fff4396115320d1a9a54d Mon Sep 17 00:00:00 2001 From: Martin Babinsky Date: Thu, 16 Jun 2016 12:15:40 +0200 Subject: ipapython module for Kerberos principal manipulation and parsing This module implements a shared codebase to handle various types of Kerberos principal names encountered during management of users, hosts nad services. Common codebase aims to replace various ad-hoc functions and routines scattered along the management framework. https://fedorahosted.org/freeipa/ticket/3864 Reviewed-By: David Kupka Reviewed-By: Jan Cholasta --- ipapython/kerberos.py | 208 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 ipapython/kerberos.py (limited to 'ipapython/kerberos.py') diff --git a/ipapython/kerberos.py b/ipapython/kerberos.py new file mode 100644 index 000000000..298dbf186 --- /dev/null +++ b/ipapython/kerberos.py @@ -0,0 +1,208 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +""" +classes/utils for Kerberos principal name validation/manipulation +""" +import re +import six + +if six.PY3: + unicode = str + +REALM_SPLIT_RE = re.compile(r'(?, components + + :param principal: unicode representation of principal + :param realm: if not None, replace the parsed realm with the specified one + + :returns: tuple containing the principal name and realm + realm will be `None` if no realm was found in the input string + """ + realm_and_name = REALM_SPLIT_RE.split(principal) + if len(realm_and_name) > 2: + raise ValueError( + "Principal is not in @ format") + + principal_name = realm_and_name[0] + + try: + parsed_realm = realm_and_name[1] + except IndexError: + parsed_realm = None if realm is None else realm + + return principal_name, parsed_realm + + +def split_principal_name(principal_name): + """ + Split principal name (without realm) into the components + + NOTE: operates on the following RFC 1510 types: + * NT-PRINCIPAL + * NT-SRV-INST + * NT-SRV-HST + + Enterprise principals (NT-ENTERPRISE, see RFC 6806) are also handled + + :param principal_name: unicode representation of principal name + :returns: tuple of individual components (i.e. primary name for + NT-PRINCIPAL and NT-ENTERPRISE, primary name and instance for others) + """ + return tuple(COMPONENT_SPLIT_RE.split(principal_name)) + + +def unescape_seq(seq, *args): + """ + unescape (remove '\\') all occurences of sequence in input strings. + + :param seq: sequence to unescape + :param args: input string to process + + :returns: tuple of strings with unescaped sequences + """ + unescape_re = re.compile(r'\\{}'.format(seq)) + + return tuple(re.sub(unescape_re, seq, a) for a in args) + + +def escape_seq(seq, *args): + """ + escape (prepend '\\') all occurences of sequence in input strings + + :param seq: sequence to escape + :param args: input string to process + + :returns: tuple of strings with escaped sequences + """ + + return tuple(a.replace(seq, u'\\{}'.format(seq)) for a in args) + + +@six.python_2_unicode_compatible +class Principal(object): + """ + Container for the principal name and realm according to RFC 1510 + """ + def __init__(self, components, realm=None): + if isinstance(components, six.string_types): + # parse principal components from realm + self.components, self.realm = self._parse_from_text( + components, realm) + + elif isinstance(components, Principal): + self.components = components.components + self.realm = components.realm if realm is None else realm + else: + self.components = tuple(components) + self.realm = realm + + def __eq__(self, other): + if not isinstance(other, Principal): + return False + + return (self.components == other.components and + self.realm == other.realm) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.components + (self.realm,)) + + def _parse_from_text(self, principal, realm=None): + """ + parse individual principal name components from the string + representation of the principal. This is done in three steps: + 1.) split the string at the unescaped '@' + 2.) unescape any leftover '\@' sequences + 3.) split the primary at the unescaped '/' + 4.) unescape leftover '\/' + :param principal: unicode representation of the principal name + :param realm: if not None, this realm name will be used instead of the + one parsed from `principal` + + :returns: tuple containing the principal name components and realm + """ + principal_name, parsed_realm = parse_princ_name_and_realm( + principal, realm=realm) + + (principal_name,) = unescape_seq(u'@', principal_name) + + if parsed_realm is not None: + (parsed_realm,) = unescape_seq(u'@', parsed_realm) + + name_components = split_principal_name(principal_name) + name_components = unescape_seq(u'/', *name_components) + + return name_components, parsed_realm + + @property + def is_user(self): + return len(self.components) == 1 + + @property + def is_enterprise(self): + return self.is_user and u'@' in self.components[0] + + @property + def is_service(self): + return len(self.components) > 1 + + @property + def is_host(self): + return (self.is_service and len(self.components) == 2 and + self.components[0] == u'host') + + @property + def username(self): + if self.is_user: + return self.components[0] + else: + raise ValueError( + "User name is defined only for user and enterprise principals") + + @property + def upn_suffix(self): + if not self.is_enterprise: + raise ValueError("Only enterprise principals have UPN suffix") + + return self.components[0].split(u'@')[1] + + @property + def hostname(self): + if not (self.is_host or self.is_service): + raise ValueError( + "hostname is defined for host and service principals") + return self.components[-1] + + @property + def service_name(self): + if not self.is_service: + raise ValueError( + "Only service principals have meaningful service name") + + return u'/'.join(c for c in escape_seq('/', *self.components[:-1])) + + def __str__(self): + """ + return the unicode representation of principal + + works in reverse of the `from_text` class method + """ + name_components = escape_seq(u'/', *self.components) + name_components = escape_seq(u'@', *name_components) + + principal_string = u'/'.join(name_components) + + if self.realm is not None: + (realm,) = escape_seq(u'@', self.realm) + principal_string = u'@'.join([principal_string, realm]) + + return principal_string -- cgit