|
1 # |
|
2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net> |
|
3 # All Rights Reserved. |
|
4 # |
|
5 # This software is subject to the provisions of the Zope Public License, |
|
6 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. |
|
7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED |
|
8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS |
|
10 # FOR A PARTICULAR PURPOSE. |
|
11 # |
|
12 |
|
13 __docformat__ = 'restructuredtext' |
|
14 |
|
15 |
|
16 # import standard library |
|
17 import ldap3 |
|
18 import logging |
|
19 logger = logging.getLogger('pyams_ldap') |
|
20 |
|
21 import re |
|
22 |
|
23 # import interfaces |
|
24 from pyams_ldap.interfaces import ILDAPPlugin |
|
25 from zope.intid.interfaces import IIntIds |
|
26 |
|
27 # import packages |
|
28 from beaker.cache import cache_region |
|
29 from persistent import Persistent |
|
30 from pyams_ldap.query import LDAPQuery |
|
31 from pyams_security.principal import PrincipalInfo |
|
32 from pyams_utils.registry import query_utility |
|
33 from zope.container.contained import Contained |
|
34 from zope.interface import implementer |
|
35 from zope.schema.fieldproperty import FieldProperty |
|
36 |
|
37 |
|
38 managers = {} |
|
39 |
|
40 |
|
41 FORMAT_ATTRIBUTES = re.compile("\{(\w+)\[?\d*\]?\}") |
|
42 |
|
43 |
|
44 class ConnectionManager(object): |
|
45 """LDAP connections manager""" |
|
46 |
|
47 def __init__(self, plugin): |
|
48 self.server = ldap3.Server(plugin.host, |
|
49 port=plugin.port, |
|
50 use_ssl=plugin.use_ssl, |
|
51 tls=plugin.use_tls) |
|
52 self.bind_dn = plugin.bind_dn |
|
53 self.password = plugin.bind_password |
|
54 if plugin.use_pool: |
|
55 self.strategy = ldap3.STRATEGY_REUSABLE_THREADED |
|
56 self.pool_name = 'pyams_ldap:{prefix}'.format(prefix=plugin.prefix) |
|
57 self.pool_size = plugin.pool_size |
|
58 self.pool_lifetime = plugin.pool_lifetime |
|
59 else: |
|
60 self.strategy = ldap3.STRATEGY_ASYNC_THREADED |
|
61 self.pool_name = None |
|
62 self.pool_size = None |
|
63 self.pool_lifetime = None |
|
64 |
|
65 def get_connection(self, user=None, password=None): |
|
66 if user: |
|
67 conn = ldap3.Connection(self.server, |
|
68 user=user, password=password, |
|
69 client_strategy=ldap3.STRATEGY_SYNC, |
|
70 auto_bind=True, lazy=False, read_only=True) |
|
71 else: |
|
72 conn = ldap3.Connection(self.server, |
|
73 user=self.bind_dn, password=self.password, |
|
74 client_strategy=self.strategy, |
|
75 pool_name=self.pool_name, |
|
76 pool_size=self.pool_size, |
|
77 pool_lifetime=self.pool_lifetime, |
|
78 auto_bind=True, lazy=False, read_only=True) |
|
79 return conn |
|
80 |
|
81 @implementer(ILDAPPlugin) |
|
82 class LDAPPlugin(Persistent, Contained): |
|
83 """LDAP authentication plug-in""" |
|
84 |
|
85 prefix = FieldProperty(ILDAPPlugin['prefix']) |
|
86 title = FieldProperty(ILDAPPlugin['title']) |
|
87 enabled = FieldProperty(ILDAPPlugin['enabled']) |
|
88 |
|
89 _scheme = None |
|
90 _host = None |
|
91 _port = None |
|
92 _use_ssl = False |
|
93 |
|
94 _server_uri = FieldProperty(ILDAPPlugin['server_uri']) |
|
95 bind_dn = FieldProperty(ILDAPPlugin['bind_dn']) |
|
96 bind_password = FieldProperty(ILDAPPlugin['bind_password']) |
|
97 use_tls = FieldProperty(ILDAPPlugin['use_tls']) |
|
98 use_pool = FieldProperty(ILDAPPlugin['use_pool']) |
|
99 pool_size = FieldProperty(ILDAPPlugin['pool_size']) |
|
100 pool_lifetime = FieldProperty(ILDAPPlugin['pool_lifetime']) |
|
101 base_dn = FieldProperty(ILDAPPlugin['base_dn']) |
|
102 search_scope = FieldProperty(ILDAPPlugin['search_scope']) |
|
103 login_attribute = FieldProperty(ILDAPPlugin['login_attribute']) |
|
104 login_query = FieldProperty(ILDAPPlugin['login_query']) |
|
105 uid_attribute = FieldProperty(ILDAPPlugin['uid_attribute']) |
|
106 uid_query = FieldProperty(ILDAPPlugin['uid_query']) |
|
107 title_format = FieldProperty(ILDAPPlugin['title_format']) |
|
108 groups_base_dn = FieldProperty(ILDAPPlugin['groups_base_dn']) |
|
109 groups_search_scope = FieldProperty(ILDAPPlugin['groups_search_scope']) |
|
110 groups_query = FieldProperty(ILDAPPlugin['groups_query']) |
|
111 group_prefix = FieldProperty(ILDAPPlugin['group_prefix']) |
|
112 group_uid_attribute = FieldProperty(ILDAPPlugin['group_uid_attribute']) |
|
113 group_title_format = FieldProperty(ILDAPPlugin['group_title_format']) |
|
114 |
|
115 users_select_query = FieldProperty(ILDAPPlugin['users_select_query']) |
|
116 users_search_query = FieldProperty(ILDAPPlugin['users_search_query']) |
|
117 groups_select_query = FieldProperty(ILDAPPlugin['groups_select_query']) |
|
118 groups_search_query = FieldProperty(ILDAPPlugin['groups_search_query']) |
|
119 |
|
120 @property |
|
121 def server_uri(self): |
|
122 return self._server_uri |
|
123 |
|
124 @server_uri.setter |
|
125 def server_uri(self, value): |
|
126 self._server_uri = value |
|
127 try: |
|
128 scheme, host = value.split('://', 1) |
|
129 except ValueError: |
|
130 scheme = 'ldap' |
|
131 host = value |
|
132 self._use_ssl = scheme == 'ldaps' |
|
133 self._scheme = scheme |
|
134 try: |
|
135 host, port = host.split(':', 1) |
|
136 port = int(port) |
|
137 except ValueError: |
|
138 port = 636 if self._use_ssl else 389 |
|
139 self._host = host |
|
140 self._port = port |
|
141 |
|
142 @property |
|
143 def scheme(self): |
|
144 return self._scheme |
|
145 |
|
146 @property |
|
147 def host(self): |
|
148 return self._host |
|
149 |
|
150 @property |
|
151 def port(self): |
|
152 return self._port |
|
153 |
|
154 @property |
|
155 def use_ssl(self): |
|
156 return self._use_ssl |
|
157 |
|
158 def _get_id(self): |
|
159 intids = query_utility(IIntIds) |
|
160 return intids.register(self) |
|
161 |
|
162 def clear(self): |
|
163 self_id = self._get_id() |
|
164 if self_id in managers: |
|
165 del managers[self_id] |
|
166 |
|
167 def get_connection(self, user=None, password=None): |
|
168 self_id = self._get_id() |
|
169 if self_id not in managers: |
|
170 managers[self_id] = ConnectionManager(self) |
|
171 return managers[self_id].get_connection(user, password) |
|
172 |
|
173 def authenticate(self, credentials, request): |
|
174 if not self.enabled: |
|
175 return None |
|
176 attrs = credentials.attributes |
|
177 login = attrs.get('login') |
|
178 password = attrs.get('password') |
|
179 conn = self.get_connection() |
|
180 search = LDAPQuery(self.base_dn, self.login_query, self.search_scope, (self.login_attribute, |
|
181 self.uid_attribute)) |
|
182 result = search.execute(conn, login=login, password=password) |
|
183 if not result or len(result) > 1: |
|
184 return None |
|
185 result = result[0] |
|
186 login_dn = result[0] |
|
187 try: |
|
188 login_conn = self.get_connection(user=login_dn, password=password) |
|
189 login_conn.unbind() |
|
190 except ldap3.LDAPException: |
|
191 logger.debug("LDAP authentication exception with login %r", login, exc_info=True) |
|
192 return None |
|
193 else: |
|
194 if self.uid_attribute == 'dn': |
|
195 return "{prefix}:{dn}".format(prefix=self.prefix, |
|
196 dn=login_dn) |
|
197 else: |
|
198 attrs = result[1] |
|
199 if self.login_attribute in attrs: |
|
200 return "{prefix}:{attr}".format(prefix=self.prefix, |
|
201 attr=attrs[self.uid_attribute][0]) |
|
202 |
|
203 def _get_group(self, group_id): |
|
204 if not self.enabled: |
|
205 return None |
|
206 |
|
207 def get_principal(self, principal_id): |
|
208 if not self.enabled: |
|
209 return None |
|
210 if not principal_id.startswith(self.prefix + ':'): |
|
211 return None |
|
212 prefix, login = principal_id.split(':', 1) |
|
213 conn = self.get_connection() |
|
214 if login.startswith(self.group_prefix + ':'): |
|
215 group_prefix, group_id = login.split(':', 1) |
|
216 attributes = FORMAT_ATTRIBUTES.findall(self.group_title_format) |
|
217 if self.group_uid_attribute == 'dn': |
|
218 search = LDAPQuery(group_id, '(objectClass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT, attributes) |
|
219 else: |
|
220 search = LDAPQuery(self.base_dn, self.uid_query, self.search_scope, attributes) |
|
221 result = search.execute(conn, login=group_id) |
|
222 if not result or len(result) > 1: |
|
223 return None |
|
224 group_dn, attrs = result[0] |
|
225 return PrincipalInfo(id='{prefix}:{group_prefix}:{group_id}'.format(prefix=self.prefix, |
|
226 group_prefix=self.group_prefix, |
|
227 group_id=group_id), |
|
228 title=self.group_title_format.format(**attrs), |
|
229 dn=group_dn) |
|
230 else: |
|
231 attributes = FORMAT_ATTRIBUTES.findall(self.title_format) |
|
232 if self.uid_attribute == 'dn': |
|
233 search = LDAPQuery(login, '(objectClass=*)', ldap3.SEARCH_SCOPE_BASE_OBJECT, attributes) |
|
234 else: |
|
235 search = LDAPQuery(self.base_dn, self.uid_query, self.search_scope, attributes) |
|
236 result = search.execute(conn, login=login) |
|
237 if not result or len(result) > 1: |
|
238 return None |
|
239 user_dn, attrs = result[0] |
|
240 return PrincipalInfo(id='{prefix}:{login}'.format(prefix=self.prefix, |
|
241 login=login), |
|
242 title=self.title_format.format(**attrs), |
|
243 dn=user_dn) |
|
244 |
|
245 def _get_groups(self, principal): |
|
246 if not self.groups_base_dn: |
|
247 raise StopIteration |
|
248 principal_dn = principal.attributes.get('dn') |
|
249 if principal_dn is None: |
|
250 raise StopIteration |
|
251 conn = self.get_connection() |
|
252 attributes = FORMAT_ATTRIBUTES.findall(self.group_title_format) |
|
253 search = LDAPQuery(self.groups_base_dn, self.groups_query, self.groups_search_scope, attributes) |
|
254 for group_dn, group_attrs in search.execute(conn, dn=principal_dn): |
|
255 if self.group_uid_attribute == 'dn': |
|
256 yield '{prefix}:{group_prefix}:{dn}'.format(prefix=self.prefix, |
|
257 group_prefix=self.group_prefix, |
|
258 dn=group_dn) |
|
259 else: |
|
260 yield '{prefix}:{group_prefix}:{attr}'.format(prefix=self.prefix, |
|
261 group_prefix=self.group_prefix, |
|
262 attr=group_attrs[self.group_uid_attribute]) |
|
263 |
|
264 @cache_region('short') |
|
265 def get_all_principals(self, principal_id): |
|
266 if not self.enabled: |
|
267 return set() |
|
268 principal = self.get_principal(principal_id) |
|
269 if principal is not None: |
|
270 result = {principal_id} |
|
271 if self.groups_query: |
|
272 result |= set(self._get_groups(principal)) |
|
273 return result |
|
274 return set() |
|
275 |
|
276 def find_principals(self, query): |
|
277 if not self.enabled: |
|
278 raise StopIteration |
|
279 if not query: |
|
280 return None |
|
281 conn = self.get_connection() |
|
282 # users search |
|
283 attributes = FORMAT_ATTRIBUTES.findall(self.title_format) + [self.uid_attribute, ] |
|
284 search = LDAPQuery(self.base_dn, self.users_select_query, self.search_scope, attributes) |
|
285 for user_dn, user_attrs in search.execute(conn, query=query): |
|
286 if self.uid_attribute == 'dn': |
|
287 yield PrincipalInfo(id='{prefix}:{dn}'.format(prefix=self.prefix, |
|
288 dn=user_dn), |
|
289 title=self.title_format.format(**user_attrs), |
|
290 dn=user_dn) |
|
291 else: |
|
292 yield PrincipalInfo(id='{prefix}:{attr}'.format(prefix=self.prefix, |
|
293 attr=user_attrs[self.uid_attribute][0]), |
|
294 title=self.title_format.format(**user_attrs), |
|
295 dn=user_dn) |
|
296 # groups search |
|
297 if self.groups_base_dn: |
|
298 attributes = FORMAT_ATTRIBUTES.findall(self.group_title_format) + [self.group_uid_attribute, ] |
|
299 search = LDAPQuery(self.groups_base_dn, self.groups_select_query, self.groups_search_scope, attributes) |
|
300 for group_dn, group_attrs in search.execute(conn, query=query): |
|
301 if self.group_uid_attribute == 'dn': |
|
302 yield PrincipalInfo(id='{prefix}:{group_prefix}:{dn}'.format(prefix=self.prefix, |
|
303 group_prefix=self.group_prefix, |
|
304 dn=group_dn), |
|
305 title=self.group_title_format.format(**group_attrs), |
|
306 dn=group_dn) |
|
307 else: |
|
308 yield PrincipalInfo(id='{prefix}:{group_prefix}:{attr}'.format(prefix=self.prefix, |
|
309 group_prefix=self.group_prefix, |
|
310 attr=group_attrs[ |
|
311 self.group_uid_attribute][0]), |
|
312 title=self.group_title_format.format(**group_attrs), |
|
313 dn=group_dn) |
|
314 |
|
315 def get_search_results(self, data): |
|
316 # LDAP search results are made of tuples containing DN and all |
|
317 # entries attributes |
|
318 query = data.get('query') |
|
319 if not query: |
|
320 return () |
|
321 conn = self.get_connection() |
|
322 # users search |
|
323 search = LDAPQuery(self.base_dn, self.users_search_query, self.search_scope, ldap3.ALL_ATTRIBUTES) |
|
324 for user_dn, user_attrs in search.execute(conn, query=query): |
|
325 yield user_dn, user_attrs |
|
326 # groups search |
|
327 if self.groups_base_dn: |
|
328 search = LDAPQuery(self.groups_base_dn, self.groups_search_query, self.groups_search_scope, ldap3.ALL_ATTRIBUTES) |
|
329 for group_dn, group_attrs in search.execute(conn, query=query): |
|
330 yield group_dn, group_attrs |