1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
|
#
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
#
"""
classes/utils for Kerberos principal name validation/manipulation
"""
import re
import six
from ipapython.ipautil import escape_seq, unescape_seq
if six.PY3:
unicode = str
REALM_SPLIT_RE = re.compile(r'(?<!\\)@')
COMPONENT_SPLIT_RE = re.compile(r'(?<!\\)/')
def parse_princ_name_and_realm(principal, realm=None):
"""
split principal to the <principal_name>, <realm> 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 <name>@<realm> 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))
@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.binary_type):
raise TypeError(
"Cannot create a principal object from bytes: {!r}".format(
components)
)
elif 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
def __repr__(self):
return "{0.__module__}.{0.__name__}('{1}')".format(
self.__class__, self)
|