##// END OF EJS Templates
ldap: fixed email extraction typo. An empty...
marcink -
r991:5fa43f5f stable
parent child Browse files
Show More
@@ -1,464 +1,464 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication plugin for LDAP
23 23 """
24 24
25 25
26 26 import colander
27 27 import logging
28 28 import traceback
29 29
30 30 from pylons.i18n.translation import lazy_ugettext as _
31 31 from sqlalchemy.ext.hybrid import hybrid_property
32 32
33 33 from rhodecode.authentication.base import RhodeCodeExternalAuthPlugin
34 34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 36 from rhodecode.lib.colander_utils import strip_whitespace
37 37 from rhodecode.lib.exceptions import (
38 38 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
39 39 )
40 40 from rhodecode.lib.utils2 import safe_unicode, safe_str
41 41 from rhodecode.model.db import User
42 42 from rhodecode.model.validators import Missing
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46 try:
47 47 import ldap
48 48 except ImportError:
49 49 # means that python-ldap is not installed, we use Missing object to mark
50 50 # ldap lib is Missing
51 51 ldap = Missing
52 52
53 53
54 54 def plugin_factory(plugin_id, *args, **kwds):
55 55 """
56 56 Factory function that is called during plugin discovery.
57 57 It returns the plugin instance.
58 58 """
59 59 plugin = RhodeCodeAuthPlugin(plugin_id)
60 60 return plugin
61 61
62 62
63 63 class LdapAuthnResource(AuthnPluginResourceBase):
64 64 pass
65 65
66 66
67 67 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
68 68 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
69 69 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
70 70 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
71 71
72 72 host = colander.SchemaNode(
73 73 colander.String(),
74 74 default='',
75 75 description=_('Host of the LDAP Server'),
76 76 preparer=strip_whitespace,
77 77 title=_('LDAP Host'),
78 78 widget='string')
79 79 port = colander.SchemaNode(
80 80 colander.Int(),
81 81 default=389,
82 82 description=_('Port that the LDAP server is listening on'),
83 83 preparer=strip_whitespace,
84 84 title=_('Port'),
85 85 validator=colander.Range(min=0, max=65536),
86 86 widget='int')
87 87 dn_user = colander.SchemaNode(
88 88 colander.String(),
89 89 default='',
90 90 description=_('User to connect to LDAP'),
91 91 missing='',
92 92 preparer=strip_whitespace,
93 93 title=_('Account'),
94 94 widget='string')
95 95 dn_pass = colander.SchemaNode(
96 96 colander.String(),
97 97 default='',
98 98 description=_('Password to connect to LDAP'),
99 99 missing='',
100 100 preparer=strip_whitespace,
101 101 title=_('Password'),
102 102 widget='password')
103 103 tls_kind = colander.SchemaNode(
104 104 colander.String(),
105 105 default=tls_kind_choices[0],
106 106 description=_('TLS Type'),
107 107 title=_('Connection Security'),
108 108 validator=colander.OneOf(tls_kind_choices),
109 109 widget='select')
110 110 tls_reqcert = colander.SchemaNode(
111 111 colander.String(),
112 112 default=tls_reqcert_choices[0],
113 113 description=_('Require Cert over TLS?'),
114 114 title=_('Certificate Checks'),
115 115 validator=colander.OneOf(tls_reqcert_choices),
116 116 widget='select')
117 117 base_dn = colander.SchemaNode(
118 118 colander.String(),
119 119 default='',
120 120 description=_('Base DN to search (e.g., dc=mydomain,dc=com)'),
121 121 missing='',
122 122 preparer=strip_whitespace,
123 123 title=_('Base DN'),
124 124 widget='string')
125 125 filter = colander.SchemaNode(
126 126 colander.String(),
127 127 default='',
128 128 description=_('Filter to narrow results (e.g., ou=Users, etc)'),
129 129 missing='',
130 130 preparer=strip_whitespace,
131 131 title=_('LDAP Search Filter'),
132 132 widget='string')
133 133 search_scope = colander.SchemaNode(
134 134 colander.String(),
135 135 default=search_scope_choices[0],
136 136 description=_('How deep to search LDAP'),
137 137 title=_('LDAP Search Scope'),
138 138 validator=colander.OneOf(search_scope_choices),
139 139 widget='select')
140 140 attr_login = colander.SchemaNode(
141 141 colander.String(),
142 142 default='',
143 143 description=_('LDAP Attribute to map to user name'),
144 144 preparer=strip_whitespace,
145 145 title=_('Login Attribute'),
146 146 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
147 147 widget='string')
148 148 attr_firstname = colander.SchemaNode(
149 149 colander.String(),
150 150 default='',
151 151 description=_('LDAP Attribute to map to first name'),
152 152 missing='',
153 153 preparer=strip_whitespace,
154 154 title=_('First Name Attribute'),
155 155 widget='string')
156 156 attr_lastname = colander.SchemaNode(
157 157 colander.String(),
158 158 default='',
159 159 description=_('LDAP Attribute to map to last name'),
160 160 missing='',
161 161 preparer=strip_whitespace,
162 162 title=_('Last Name Attribute'),
163 163 widget='string')
164 164 attr_email = colander.SchemaNode(
165 165 colander.String(),
166 166 default='',
167 167 description=_('LDAP Attribute to map to email address'),
168 168 missing='',
169 169 preparer=strip_whitespace,
170 170 title=_('Email Attribute'),
171 171 widget='string')
172 172
173 173
174 174 class AuthLdap(object):
175 175
176 176 def _build_servers(self):
177 177 return ', '.join(
178 178 ["{}://{}:{}".format(
179 179 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
180 180 for host in self.SERVER_ADDRESSES])
181 181
182 182 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
183 183 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
184 184 search_scope='SUBTREE', attr_login='uid',
185 185 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))'):
186 186 if ldap == Missing:
187 187 raise LdapImportError("Missing or incompatible ldap library")
188 188
189 189 self.debug = False
190 190 self.ldap_version = ldap_version
191 191 self.ldap_server_type = 'ldap'
192 192
193 193 self.TLS_KIND = tls_kind
194 194
195 195 if self.TLS_KIND == 'LDAPS':
196 196 port = port or 689
197 197 self.ldap_server_type += 's'
198 198
199 199 OPT_X_TLS_DEMAND = 2
200 200 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
201 201 OPT_X_TLS_DEMAND)
202 202 # split server into list
203 203 self.SERVER_ADDRESSES = server.split(',')
204 204 self.LDAP_SERVER_PORT = port
205 205
206 206 # USE FOR READ ONLY BIND TO LDAP SERVER
207 207 self.attr_login = attr_login
208 208
209 209 self.LDAP_BIND_DN = safe_str(bind_dn)
210 210 self.LDAP_BIND_PASS = safe_str(bind_pass)
211 211 self.LDAP_SERVER = self._build_servers()
212 212 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
213 213 self.BASE_DN = safe_str(base_dn)
214 214 self.LDAP_FILTER = safe_str(ldap_filter)
215 215
216 216 def _get_ldap_server(self):
217 217 if self.debug:
218 218 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
219 219 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
220 220 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
221 221 '/etc/openldap/cacerts')
222 222 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
223 223 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
224 224 ldap.set_option(ldap.OPT_TIMEOUT, 20)
225 225 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
226 226 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
227 227 if self.TLS_KIND != 'PLAIN':
228 228 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
229 229 server = ldap.initialize(self.LDAP_SERVER)
230 230 if self.ldap_version == 2:
231 231 server.protocol = ldap.VERSION2
232 232 else:
233 233 server.protocol = ldap.VERSION3
234 234
235 235 if self.TLS_KIND == 'START_TLS':
236 236 server.start_tls_s()
237 237
238 238 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
239 239 log.debug('Trying simple_bind with password and given DN: %s',
240 240 self.LDAP_BIND_DN)
241 241 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
242 242
243 243 return server
244 244
245 245 def get_uid(self, username):
246 246 from rhodecode.lib.helpers import chop_at
247 247 uid = username
248 248 for server_addr in self.SERVER_ADDRESSES:
249 249 uid = chop_at(username, "@%s" % server_addr)
250 250 return uid
251 251
252 252 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
253 253 try:
254 254 log.debug('Trying simple bind with %s', dn)
255 255 server.simple_bind_s(dn, safe_str(password))
256 256 user = server.search_ext_s(
257 257 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
258 258 _, attrs = user
259 259 return attrs
260 260
261 261 except ldap.INVALID_CREDENTIALS:
262 262 log.debug(
263 263 "LDAP rejected password for user '%s': %s, org_exc:",
264 264 username, dn, exc_info=True)
265 265
266 266 def authenticate_ldap(self, username, password):
267 267 """
268 268 Authenticate a user via LDAP and return his/her LDAP properties.
269 269
270 270 Raises AuthenticationError if the credentials are rejected, or
271 271 EnvironmentError if the LDAP server can't be reached.
272 272
273 273 :param username: username
274 274 :param password: password
275 275 """
276 276
277 277 uid = self.get_uid(username)
278 278
279 279 if not password:
280 280 msg = "Authenticating user %s with blank password not allowed"
281 281 log.warning(msg, username)
282 282 raise LdapPasswordError(msg)
283 283 if "," in username:
284 284 raise LdapUsernameError("invalid character in username: ,")
285 285 try:
286 286 server = self._get_ldap_server()
287 287 filter_ = '(&%s(%s=%s))' % (
288 288 self.LDAP_FILTER, self.attr_login, username)
289 289 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
290 290 filter_, self.LDAP_SERVER)
291 291 lobjects = server.search_ext_s(
292 292 self.BASE_DN, self.SEARCH_SCOPE, filter_)
293 293
294 294 if not lobjects:
295 295 raise ldap.NO_SUCH_OBJECT()
296 296
297 297 for (dn, _attrs) in lobjects:
298 298 if dn is None:
299 299 continue
300 300
301 301 user_attrs = self.fetch_attrs_from_simple_bind(
302 302 server, dn, username, password)
303 303 if user_attrs:
304 304 break
305 305
306 306 else:
307 307 log.debug("No matching LDAP objects for authentication "
308 308 "of '%s' (%s)", uid, username)
309 309 raise LdapPasswordError('Failed to authenticate user '
310 310 'with given password')
311 311
312 312 except ldap.NO_SUCH_OBJECT:
313 313 log.debug("LDAP says no such user '%s' (%s), org_exc:",
314 314 uid, username, exc_info=True)
315 315 raise LdapUsernameError()
316 316 except ldap.SERVER_DOWN:
317 317 org_exc = traceback.format_exc()
318 318 raise LdapConnectionError(
319 319 "LDAP can't access authentication "
320 320 "server, org_exc:%s" % org_exc)
321 321
322 322 return dn, user_attrs
323 323
324 324
325 325 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
326 326 # used to define dynamic binding in the
327 327 DYNAMIC_BIND_VAR = '$login'
328 328
329 329 def includeme(self, config):
330 330 config.add_authn_plugin(self)
331 331 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
332 332 config.add_view(
333 333 'rhodecode.authentication.views.AuthnPluginViewBase',
334 334 attr='settings_get',
335 335 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
336 336 request_method='GET',
337 337 route_name='auth_home',
338 338 context=LdapAuthnResource)
339 339 config.add_view(
340 340 'rhodecode.authentication.views.AuthnPluginViewBase',
341 341 attr='settings_post',
342 342 renderer='rhodecode:templates/admin/auth/plugin_settings.html',
343 343 request_method='POST',
344 344 route_name='auth_home',
345 345 context=LdapAuthnResource)
346 346
347 347 def get_settings_schema(self):
348 348 return LdapSettingsSchema()
349 349
350 350 def get_display_name(self):
351 351 return _('LDAP')
352 352
353 353 @hybrid_property
354 354 def name(self):
355 355 return "ldap"
356 356
357 357 def use_fake_password(self):
358 358 return True
359 359
360 360 def user_activation_state(self):
361 361 def_user_perms = User.get_default_user().AuthUser.permissions['global']
362 362 return 'hg.extern_activate.auto' in def_user_perms
363 363
364 364 def try_dynamic_binding(self, username, password, current_args):
365 365 """
366 366 Detects marker inside our original bind, and uses dynamic auth if
367 367 present
368 368 """
369 369
370 370 org_bind = current_args['bind_dn']
371 371 passwd = current_args['bind_pass']
372 372
373 373 def has_bind_marker(username):
374 374 if self.DYNAMIC_BIND_VAR in username:
375 375 return True
376 376
377 377 # we only passed in user with "special" variable
378 378 if org_bind and has_bind_marker(org_bind) and not passwd:
379 379 log.debug('Using dynamic user/password binding for ldap '
380 380 'authentication. Replacing `%s` with username',
381 381 self.DYNAMIC_BIND_VAR)
382 382 current_args['bind_dn'] = org_bind.replace(
383 383 self.DYNAMIC_BIND_VAR, username)
384 384 current_args['bind_pass'] = password
385 385
386 386 return current_args
387 387
388 388 def auth(self, userobj, username, password, settings, **kwargs):
389 389 """
390 390 Given a user object (which may be null), username, a plaintext password,
391 391 and a settings object (containing all the keys needed as listed in
392 392 settings()), authenticate this user's login attempt.
393 393
394 394 Return None on failure. On success, return a dictionary of the form:
395 395
396 396 see: RhodeCodeAuthPluginBase.auth_func_attrs
397 397 This is later validated for correctness
398 398 """
399 399
400 400 if not username or not password:
401 401 log.debug('Empty username or password skipping...')
402 402 return None
403 403
404 404 ldap_args = {
405 405 'server': settings.get('host', ''),
406 406 'base_dn': settings.get('base_dn', ''),
407 407 'port': settings.get('port'),
408 408 'bind_dn': settings.get('dn_user'),
409 409 'bind_pass': settings.get('dn_pass'),
410 410 'tls_kind': settings.get('tls_kind'),
411 411 'tls_reqcert': settings.get('tls_reqcert'),
412 412 'search_scope': settings.get('search_scope'),
413 413 'attr_login': settings.get('attr_login'),
414 414 'ldap_version': 3,
415 415 'ldap_filter': settings.get('filter'),
416 416 }
417 417
418 418 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
419 419
420 420 log.debug('Checking for ldap authentication.')
421 421
422 422 try:
423 423 aldap = AuthLdap(**ldap_args)
424 424 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
425 425 log.debug('Got ldap DN response %s', user_dn)
426 426
427 427 def get_ldap_attr(k):
428 428 return ldap_attrs.get(settings.get(k), [''])[0]
429 429
430 430 # old attrs fetched from RhodeCode database
431 431 admin = getattr(userobj, 'admin', False)
432 432 active = getattr(userobj, 'active', True)
433 433 email = getattr(userobj, 'email', '')
434 434 username = getattr(userobj, 'username', username)
435 435 firstname = getattr(userobj, 'firstname', '')
436 436 lastname = getattr(userobj, 'lastname', '')
437 437 extern_type = getattr(userobj, 'extern_type', '')
438 438
439 439 groups = []
440 440 user_attrs = {
441 441 'username': username,
442 442 'firstname': safe_unicode(
443 443 get_ldap_attr('attr_firstname') or firstname),
444 444 'lastname': safe_unicode(
445 445 get_ldap_attr('attr_lastname') or lastname),
446 446 'groups': groups,
447 'email': get_ldap_attr('attr_email' or email),
447 'email': get_ldap_attr('attr_email') or email,
448 448 'admin': admin,
449 449 'active': active,
450 450 "active_from_extern": None,
451 451 'extern_name': user_dn,
452 452 'extern_type': extern_type,
453 453 }
454 454 log.debug('ldap user: %s', user_attrs)
455 455 log.info('user %s authenticated correctly', user_attrs['username'])
456 456
457 457 return user_attrs
458 458
459 459 except (LdapUsernameError, LdapPasswordError, LdapImportError):
460 460 log.exception("LDAP related exception")
461 461 return None
462 462 except (Exception,):
463 463 log.exception("Other exception")
464 464 return None
General Comments 0
You need to be logged in to leave comments. Login now