##// END OF EJS Templates
ldap: logging fixes
marcink -
r4244:ad22cfe4 stable
parent child Browse files
Show More
@@ -1,808 +1,808 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 Authentication modules
23 23 """
24 24 import socket
25 25 import string
26 26 import colander
27 27 import copy
28 28 import logging
29 29 import time
30 30 import traceback
31 31 import warnings
32 32 import functools
33 33
34 34 from pyramid.threadlocal import get_current_registry
35 35
36 36 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 38 from rhodecode.lib import rc_cache
39 39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 40 from rhodecode.lib.utils2 import safe_int, safe_str
41 41 from rhodecode.lib.exceptions import LdapConnectionError, LdapUsernameError, \
42 42 LdapPasswordError
43 43 from rhodecode.model.db import User
44 44 from rhodecode.model.meta import Session
45 45 from rhodecode.model.settings import SettingsModel
46 46 from rhodecode.model.user import UserModel
47 47 from rhodecode.model.user_group import UserGroupModel
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52 # auth types that authenticate() function can receive
53 53 VCS_TYPE = 'vcs'
54 54 HTTP_TYPE = 'http'
55 55
56 56 external_auth_session_key = 'rhodecode.external_auth'
57 57
58 58
59 59 class hybrid_property(object):
60 60 """
61 61 a property decorator that works both for instance and class
62 62 """
63 63 def __init__(self, fget, fset=None, fdel=None, expr=None):
64 64 self.fget = fget
65 65 self.fset = fset
66 66 self.fdel = fdel
67 67 self.expr = expr or fget
68 68 functools.update_wrapper(self, fget)
69 69
70 70 def __get__(self, instance, owner):
71 71 if instance is None:
72 72 return self.expr(owner)
73 73 else:
74 74 return self.fget(instance)
75 75
76 76 def __set__(self, instance, value):
77 77 self.fset(instance, value)
78 78
79 79 def __delete__(self, instance):
80 80 self.fdel(instance)
81 81
82 82
83 83 class LazyFormencode(object):
84 84 def __init__(self, formencode_obj, *args, **kwargs):
85 85 self.formencode_obj = formencode_obj
86 86 self.args = args
87 87 self.kwargs = kwargs
88 88
89 89 def __call__(self, *args, **kwargs):
90 90 from inspect import isfunction
91 91 formencode_obj = self.formencode_obj
92 92 if isfunction(formencode_obj):
93 93 # case we wrap validators into functions
94 94 formencode_obj = self.formencode_obj(*args, **kwargs)
95 95 return formencode_obj(*self.args, **self.kwargs)
96 96
97 97
98 98 class RhodeCodeAuthPluginBase(object):
99 99 # UID is used to register plugin to the registry
100 100 uid = None
101 101
102 102 # cache the authentication request for N amount of seconds. Some kind
103 103 # of authentication methods are very heavy and it's very efficient to cache
104 104 # the result of a call. If it's set to None (default) cache is off
105 105 AUTH_CACHE_TTL = None
106 106 AUTH_CACHE = {}
107 107
108 108 auth_func_attrs = {
109 109 "username": "unique username",
110 110 "firstname": "first name",
111 111 "lastname": "last name",
112 112 "email": "email address",
113 113 "groups": '["list", "of", "groups"]',
114 114 "user_group_sync":
115 115 'True|False defines if returned user groups should be synced',
116 116 "extern_name": "name in external source of record",
117 117 "extern_type": "type of external source of record",
118 118 "admin": 'True|False defines if user should be RhodeCode super admin',
119 119 "active":
120 120 'True|False defines active state of user internally for RhodeCode',
121 121 "active_from_extern":
122 122 "True|False|None, active state from the external auth, "
123 123 "None means use definition from RhodeCode extern_type active value"
124 124
125 125 }
126 126 # set on authenticate() method and via set_auth_type func.
127 127 auth_type = None
128 128
129 129 # set on authenticate() method and via set_calling_scope_repo, this is a
130 130 # calling scope repository when doing authentication most likely on VCS
131 131 # operations
132 132 acl_repo_name = None
133 133
134 134 # List of setting names to store encrypted. Plugins may override this list
135 135 # to store settings encrypted.
136 136 _settings_encrypted = []
137 137
138 138 # Mapping of python to DB settings model types. Plugins may override or
139 139 # extend this mapping.
140 140 _settings_type_map = {
141 141 colander.String: 'unicode',
142 142 colander.Integer: 'int',
143 143 colander.Boolean: 'bool',
144 144 colander.List: 'list',
145 145 }
146 146
147 147 # list of keys in settings that are unsafe to be logged, should be passwords
148 148 # or other crucial credentials
149 149 _settings_unsafe_keys = []
150 150
151 151 def __init__(self, plugin_id):
152 152 self._plugin_id = plugin_id
153 153 self._settings = {}
154 154
155 155 def __str__(self):
156 156 return self.get_id()
157 157
158 158 def _get_setting_full_name(self, name):
159 159 """
160 160 Return the full setting name used for storing values in the database.
161 161 """
162 162 # TODO: johbo: Using the name here is problematic. It would be good to
163 163 # introduce either new models in the database to hold Plugin and
164 164 # PluginSetting or to use the plugin id here.
165 165 return 'auth_{}_{}'.format(self.name, name)
166 166
167 167 def _get_setting_type(self, name):
168 168 """
169 169 Return the type of a setting. This type is defined by the SettingsModel
170 170 and determines how the setting is stored in DB. Optionally the suffix
171 171 `.encrypted` is appended to instruct SettingsModel to store it
172 172 encrypted.
173 173 """
174 174 schema_node = self.get_settings_schema().get(name)
175 175 db_type = self._settings_type_map.get(
176 176 type(schema_node.typ), 'unicode')
177 177 if name in self._settings_encrypted:
178 178 db_type = '{}.encrypted'.format(db_type)
179 179 return db_type
180 180
181 181 @classmethod
182 182 def docs(cls):
183 183 """
184 184 Defines documentation url which helps with plugin setup
185 185 """
186 186 return ''
187 187
188 188 @classmethod
189 189 def icon(cls):
190 190 """
191 191 Defines ICON in SVG format for authentication method
192 192 """
193 193 return ''
194 194
195 195 def is_enabled(self):
196 196 """
197 197 Returns true if this plugin is enabled. An enabled plugin can be
198 198 configured in the admin interface but it is not consulted during
199 199 authentication.
200 200 """
201 201 auth_plugins = SettingsModel().get_auth_plugins()
202 202 return self.get_id() in auth_plugins
203 203
204 204 def is_active(self, plugin_cached_settings=None):
205 205 """
206 206 Returns true if the plugin is activated. An activated plugin is
207 207 consulted during authentication, assumed it is also enabled.
208 208 """
209 209 return self.get_setting_by_name(
210 210 'enabled', plugin_cached_settings=plugin_cached_settings)
211 211
212 212 def get_id(self):
213 213 """
214 214 Returns the plugin id.
215 215 """
216 216 return self._plugin_id
217 217
218 218 def get_display_name(self):
219 219 """
220 220 Returns a translation string for displaying purposes.
221 221 """
222 222 raise NotImplementedError('Not implemented in base class')
223 223
224 224 def get_settings_schema(self):
225 225 """
226 226 Returns a colander schema, representing the plugin settings.
227 227 """
228 228 return AuthnPluginSettingsSchemaBase()
229 229
230 230 def _propagate_settings(self, raw_settings):
231 231 settings = {}
232 232 for node in self.get_settings_schema():
233 233 settings[node.name] = self.get_setting_by_name(
234 234 node.name, plugin_cached_settings=raw_settings)
235 235 return settings
236 236
237 237 def get_settings(self, use_cache=True):
238 238 """
239 239 Returns the plugin settings as dictionary.
240 240 """
241 241 if self._settings != {} and use_cache:
242 242 return self._settings
243 243
244 244 raw_settings = SettingsModel().get_all_settings()
245 245 settings = self._propagate_settings(raw_settings)
246 246
247 247 self._settings = settings
248 248 return self._settings
249 249
250 250 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
251 251 """
252 252 Returns a plugin setting by name.
253 253 """
254 254 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
255 255 if plugin_cached_settings:
256 256 plugin_settings = plugin_cached_settings
257 257 else:
258 258 plugin_settings = SettingsModel().get_all_settings()
259 259
260 260 if full_name in plugin_settings:
261 261 return plugin_settings[full_name]
262 262 else:
263 263 return default
264 264
265 265 def create_or_update_setting(self, name, value):
266 266 """
267 267 Create or update a setting for this plugin in the persistent storage.
268 268 """
269 269 full_name = self._get_setting_full_name(name)
270 270 type_ = self._get_setting_type(name)
271 271 db_setting = SettingsModel().create_or_update_setting(
272 272 full_name, value, type_)
273 273 return db_setting.app_settings_value
274 274
275 275 def log_safe_settings(self, settings):
276 276 """
277 277 returns a log safe representation of settings, without any secrets
278 278 """
279 279 settings_copy = copy.deepcopy(settings)
280 280 for k in self._settings_unsafe_keys:
281 281 if k in settings_copy:
282 282 del settings_copy[k]
283 283 return settings_copy
284 284
285 285 @hybrid_property
286 286 def name(self):
287 287 """
288 288 Returns the name of this authentication plugin.
289 289
290 290 :returns: string
291 291 """
292 292 raise NotImplementedError("Not implemented in base class")
293 293
294 294 def get_url_slug(self):
295 295 """
296 296 Returns a slug which should be used when constructing URLs which refer
297 297 to this plugin. By default it returns the plugin name. If the name is
298 298 not suitable for using it in an URL the plugin should override this
299 299 method.
300 300 """
301 301 return self.name
302 302
303 303 @property
304 304 def is_headers_auth(self):
305 305 """
306 306 Returns True if this authentication plugin uses HTTP headers as
307 307 authentication method.
308 308 """
309 309 return False
310 310
311 311 @hybrid_property
312 312 def is_container_auth(self):
313 313 """
314 314 Deprecated method that indicates if this authentication plugin uses
315 315 HTTP headers as authentication method.
316 316 """
317 317 warnings.warn(
318 318 'Use is_headers_auth instead.', category=DeprecationWarning)
319 319 return self.is_headers_auth
320 320
321 321 @hybrid_property
322 322 def allows_creating_users(self):
323 323 """
324 324 Defines if Plugin allows users to be created on-the-fly when
325 325 authentication is called. Controls how external plugins should behave
326 326 in terms if they are allowed to create new users, or not. Base plugins
327 327 should not be allowed to, but External ones should be !
328 328
329 329 :return: bool
330 330 """
331 331 return False
332 332
333 333 def set_auth_type(self, auth_type):
334 334 self.auth_type = auth_type
335 335
336 336 def set_calling_scope_repo(self, acl_repo_name):
337 337 self.acl_repo_name = acl_repo_name
338 338
339 339 def allows_authentication_from(
340 340 self, user, allows_non_existing_user=True,
341 341 allowed_auth_plugins=None, allowed_auth_sources=None):
342 342 """
343 343 Checks if this authentication module should accept a request for
344 344 the current user.
345 345
346 346 :param user: user object fetched using plugin's get_user() method.
347 347 :param allows_non_existing_user: if True, don't allow the
348 348 user to be empty, meaning not existing in our database
349 349 :param allowed_auth_plugins: if provided, users extern_type will be
350 350 checked against a list of provided extern types, which are plugin
351 351 auth_names in the end
352 352 :param allowed_auth_sources: authentication type allowed,
353 353 `http` or `vcs` default is both.
354 354 defines if plugin will accept only http authentication vcs
355 355 authentication(git/hg) or both
356 356 :returns: boolean
357 357 """
358 358 if not user and not allows_non_existing_user:
359 359 log.debug('User is empty but plugin does not allow empty users,'
360 360 'not allowed to authenticate')
361 361 return False
362 362
363 363 expected_auth_plugins = allowed_auth_plugins or [self.name]
364 364 if user and (user.extern_type and
365 365 user.extern_type not in expected_auth_plugins):
366 366 log.debug(
367 367 'User `%s` is bound to `%s` auth type. Plugin allows only '
368 368 '%s, skipping', user, user.extern_type, expected_auth_plugins)
369 369
370 370 return False
371 371
372 372 # by default accept both
373 373 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
374 374 if self.auth_type not in expected_auth_from:
375 375 log.debug('Current auth source is %s but plugin only allows %s',
376 376 self.auth_type, expected_auth_from)
377 377 return False
378 378
379 379 return True
380 380
381 381 def get_user(self, username=None, **kwargs):
382 382 """
383 383 Helper method for user fetching in plugins, by default it's using
384 384 simple fetch by username, but this method can be custimized in plugins
385 385 eg. headers auth plugin to fetch user by environ params
386 386
387 387 :param username: username if given to fetch from database
388 388 :param kwargs: extra arguments needed for user fetching.
389 389 """
390 390 user = None
391 391 log.debug(
392 392 'Trying to fetch user `%s` from RhodeCode database', username)
393 393 if username:
394 394 user = User.get_by_username(username)
395 395 if not user:
396 396 log.debug('User not found, fallback to fetch user in '
397 397 'case insensitive mode')
398 398 user = User.get_by_username(username, case_insensitive=True)
399 399 else:
400 400 log.debug('provided username:`%s` is empty skipping...', username)
401 401 if not user:
402 402 log.debug('User `%s` not found in database', username)
403 403 else:
404 404 log.debug('Got DB user:%s', user)
405 405 return user
406 406
407 407 def user_activation_state(self):
408 408 """
409 409 Defines user activation state when creating new users
410 410
411 411 :returns: boolean
412 412 """
413 413 raise NotImplementedError("Not implemented in base class")
414 414
415 415 def auth(self, userobj, username, passwd, settings, **kwargs):
416 416 """
417 417 Given a user object (which may be null), username, a plaintext
418 418 password, and a settings object (containing all the keys needed as
419 419 listed in settings()), authenticate this user's login attempt.
420 420
421 421 Return None on failure. On success, return a dictionary of the form:
422 422
423 423 see: RhodeCodeAuthPluginBase.auth_func_attrs
424 424 This is later validated for correctness
425 425 """
426 426 raise NotImplementedError("not implemented in base class")
427 427
428 428 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
429 429 """
430 430 Wrapper to call self.auth() that validates call on it
431 431
432 432 :param userobj: userobj
433 433 :param username: username
434 434 :param passwd: plaintext password
435 435 :param settings: plugin settings
436 436 """
437 437 auth = self.auth(userobj, username, passwd, settings, **kwargs)
438 438 if auth:
439 439 auth['_plugin'] = self.name
440 440 auth['_ttl_cache'] = self.get_ttl_cache(settings)
441 441 # check if hash should be migrated ?
442 442 new_hash = auth.get('_hash_migrate')
443 443 if new_hash:
444 444 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
445 445 if 'user_group_sync' not in auth:
446 446 auth['user_group_sync'] = False
447 447 return self._validate_auth_return(auth)
448 448 return auth
449 449
450 450 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
451 451 new_hash_cypher = _RhodeCodeCryptoBCrypt()
452 452 # extra checks, so make sure new hash is correct.
453 453 password_encoded = safe_str(password)
454 454 if new_hash and new_hash_cypher.hash_check(
455 455 password_encoded, new_hash):
456 456 cur_user = User.get_by_username(username)
457 457 cur_user.password = new_hash
458 458 Session().add(cur_user)
459 459 Session().flush()
460 460 log.info('Migrated user %s hash to bcrypt', cur_user)
461 461
462 462 def _validate_auth_return(self, ret):
463 463 if not isinstance(ret, dict):
464 464 raise Exception('returned value from auth must be a dict')
465 465 for k in self.auth_func_attrs:
466 466 if k not in ret:
467 467 raise Exception('Missing %s attribute from returned data' % k)
468 468 return ret
469 469
470 470 def get_ttl_cache(self, settings=None):
471 471 plugin_settings = settings or self.get_settings()
472 472 # we set default to 30, we make a compromise here,
473 473 # performance > security, mostly due to LDAP/SVN, majority
474 474 # of users pick cache_ttl to be enabled
475 475 from rhodecode.authentication import plugin_default_auth_ttl
476 476 cache_ttl = plugin_default_auth_ttl
477 477
478 478 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
479 479 # plugin cache set inside is more important than the settings value
480 480 cache_ttl = self.AUTH_CACHE_TTL
481 481 elif plugin_settings.get('cache_ttl'):
482 482 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
483 483
484 484 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
485 485 return plugin_cache_active, cache_ttl
486 486
487 487
488 488 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
489 489
490 490 @hybrid_property
491 491 def allows_creating_users(self):
492 492 return True
493 493
494 494 def use_fake_password(self):
495 495 """
496 496 Return a boolean that indicates whether or not we should set the user's
497 497 password to a random value when it is authenticated by this plugin.
498 498 If your plugin provides authentication, then you will generally
499 499 want this.
500 500
501 501 :returns: boolean
502 502 """
503 503 raise NotImplementedError("Not implemented in base class")
504 504
505 505 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
506 506 # at this point _authenticate calls plugin's `auth()` function
507 507 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
508 508 userobj, username, passwd, settings, **kwargs)
509 509
510 510 if auth:
511 511 # maybe plugin will clean the username ?
512 512 # we should use the return value
513 513 username = auth['username']
514 514
515 515 # if external source tells us that user is not active, we should
516 516 # skip rest of the process. This can prevent from creating users in
517 517 # RhodeCode when using external authentication, but if it's
518 518 # inactive user we shouldn't create that user anyway
519 519 if auth['active_from_extern'] is False:
520 520 log.warning(
521 521 "User %s authenticated against %s, but is inactive",
522 522 username, self.__module__)
523 523 return None
524 524
525 525 cur_user = User.get_by_username(username, case_insensitive=True)
526 526 is_user_existing = cur_user is not None
527 527
528 528 if is_user_existing:
529 529 log.debug('Syncing user `%s` from '
530 530 '`%s` plugin', username, self.name)
531 531 else:
532 532 log.debug('Creating non existing user `%s` from '
533 533 '`%s` plugin', username, self.name)
534 534
535 535 if self.allows_creating_users:
536 536 log.debug('Plugin `%s` allows to '
537 537 'create new users', self.name)
538 538 else:
539 539 log.debug('Plugin `%s` does not allow to '
540 540 'create new users', self.name)
541 541
542 542 user_parameters = {
543 543 'username': username,
544 544 'email': auth["email"],
545 545 'firstname': auth["firstname"],
546 546 'lastname': auth["lastname"],
547 547 'active': auth["active"],
548 548 'admin': auth["admin"],
549 549 'extern_name': auth["extern_name"],
550 550 'extern_type': self.name,
551 551 'plugin': self,
552 552 'allow_to_create_user': self.allows_creating_users,
553 553 }
554 554
555 555 if not is_user_existing:
556 556 if self.use_fake_password():
557 557 # Randomize the PW because we don't need it, but don't want
558 558 # them blank either
559 559 passwd = PasswordGenerator().gen_password(length=16)
560 560 user_parameters['password'] = passwd
561 561 else:
562 562 # Since the password is required by create_or_update method of
563 563 # UserModel, we need to set it explicitly.
564 564 # The create_or_update method is smart and recognises the
565 565 # password hashes as well.
566 566 user_parameters['password'] = cur_user.password
567 567
568 568 # we either create or update users, we also pass the flag
569 569 # that controls if this method can actually do that.
570 570 # raises NotAllowedToCreateUserError if it cannot, and we try to.
571 571 user = UserModel().create_or_update(**user_parameters)
572 572 Session().flush()
573 573 # enforce user is just in given groups, all of them has to be ones
574 574 # created from plugins. We store this info in _group_data JSON
575 575 # field
576 576
577 577 if auth['user_group_sync']:
578 578 try:
579 579 groups = auth['groups'] or []
580 580 log.debug(
581 581 'Performing user_group sync based on set `%s` '
582 582 'returned by `%s` plugin', groups, self.name)
583 583 UserGroupModel().enforce_groups(user, groups, self.name)
584 584 except Exception:
585 585 # for any reason group syncing fails, we should
586 586 # proceed with login
587 587 log.error(traceback.format_exc())
588 588
589 589 Session().commit()
590 590 return auth
591 591
592 592
593 593 class AuthLdapBase(object):
594 594
595 595 @classmethod
596 596 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
597 597
598 598 def host_resolver(host, port, full_resolve=True):
599 599 """
600 600 Main work for this function is to prevent ldap connection issues,
601 601 and detect them early using a "greenified" sockets
602 602 """
603 603 host = host.strip()
604 604 if not full_resolve:
605 605 return '{}:{}'.format(host, port)
606 606
607 log.debug('LDAP: Resolving IP for LDAP host %s', host)
607 log.debug('LDAP: Resolving IP for LDAP host `%s`', host)
608 608 try:
609 609 ip = socket.gethostbyname(host)
610 log.debug('Got LDAP server %s ip %s', host, ip)
610 log.debug('LDAP: Got LDAP host `%s` ip %s', host, ip)
611 611 except Exception:
612 raise LdapConnectionError(
613 'Failed to resolve host: `{}`'.format(host))
612 raise LdapConnectionError('Failed to resolve host: `{}`'.format(host))
614 613
615 614 log.debug('LDAP: Checking if IP %s is accessible', ip)
616 615 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
617 616 try:
618 617 s.connect((ip, int(port)))
619 618 s.shutdown(socket.SHUT_RD)
619 log.debug('LDAP: connection to %s successful', ip)
620 620 except Exception:
621 621 raise LdapConnectionError(
622 622 'Failed to connect to host: `{}:{}`'.format(host, port))
623 623
624 624 return '{}:{}'.format(host, port)
625 625
626 626 if len(ldap_server) == 1:
627 627 # in case of single server use resolver to detect potential
628 628 # connection issues
629 629 full_resolve = True
630 630 else:
631 631 full_resolve = False
632 632
633 633 return ', '.join(
634 634 ["{}://{}".format(
635 635 ldap_server_type,
636 636 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
637 637 for host in ldap_server])
638 638
639 639 @classmethod
640 640 def _get_server_list(cls, servers):
641 641 return map(string.strip, servers.split(','))
642 642
643 643 @classmethod
644 644 def get_uid(cls, username, server_addresses):
645 645 uid = username
646 646 for server_addr in server_addresses:
647 647 uid = chop_at(username, "@%s" % server_addr)
648 648 return uid
649 649
650 650 @classmethod
651 651 def validate_username(cls, username):
652 652 if "," in username:
653 653 raise LdapUsernameError(
654 654 "invalid character `,` in username: `{}`".format(username))
655 655
656 656 @classmethod
657 657 def validate_password(cls, username, password):
658 658 if not password:
659 659 msg = "Authenticating user %s with blank password not allowed"
660 660 log.warning(msg, username)
661 661 raise LdapPasswordError(msg)
662 662
663 663
664 664 def loadplugin(plugin_id):
665 665 """
666 666 Loads and returns an instantiated authentication plugin.
667 667 Returns the RhodeCodeAuthPluginBase subclass on success,
668 668 or None on failure.
669 669 """
670 670 # TODO: Disusing pyramids thread locals to retrieve the registry.
671 671 authn_registry = get_authn_registry()
672 672 plugin = authn_registry.get_plugin(plugin_id)
673 673 if plugin is None:
674 674 log.error('Authentication plugin not found: "%s"', plugin_id)
675 675 return plugin
676 676
677 677
678 678 def get_authn_registry(registry=None):
679 679 registry = registry or get_current_registry()
680 680 authn_registry = registry.queryUtility(IAuthnPluginRegistry)
681 681 return authn_registry
682 682
683 683
684 684 def authenticate(username, password, environ=None, auth_type=None,
685 685 skip_missing=False, registry=None, acl_repo_name=None):
686 686 """
687 687 Authentication function used for access control,
688 688 It tries to authenticate based on enabled authentication modules.
689 689
690 690 :param username: username can be empty for headers auth
691 691 :param password: password can be empty for headers auth
692 692 :param environ: environ headers passed for headers auth
693 693 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
694 694 :param skip_missing: ignores plugins that are in db but not in environment
695 695 :returns: None if auth failed, plugin_user dict if auth is correct
696 696 """
697 697 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
698 698 raise ValueError('auth type must be on of http, vcs got "%s" instead'
699 699 % auth_type)
700 700 headers_only = environ and not (username and password)
701 701
702 702 authn_registry = get_authn_registry(registry)
703 703
704 704 plugins_to_check = authn_registry.get_plugins_for_authentication()
705 705 log.debug('Starting ordered authentication chain using %s plugins',
706 706 [x.name for x in plugins_to_check])
707 707 for plugin in plugins_to_check:
708 708 plugin.set_auth_type(auth_type)
709 709 plugin.set_calling_scope_repo(acl_repo_name)
710 710
711 711 if headers_only and not plugin.is_headers_auth:
712 712 log.debug('Auth type is for headers only and plugin `%s` is not '
713 713 'headers plugin, skipping...', plugin.get_id())
714 714 continue
715 715
716 716 log.debug('Trying authentication using ** %s **', plugin.get_id())
717 717
718 718 # load plugin settings from RhodeCode database
719 719 plugin_settings = plugin.get_settings()
720 720 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
721 721 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
722 722
723 723 # use plugin's method of user extraction.
724 724 user = plugin.get_user(username, environ=environ,
725 725 settings=plugin_settings)
726 726 display_user = user.username if user else username
727 727 log.debug(
728 728 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
729 729
730 730 if not plugin.allows_authentication_from(user):
731 731 log.debug('Plugin %s does not accept user `%s` for authentication',
732 732 plugin.get_id(), display_user)
733 733 continue
734 734 else:
735 735 log.debug('Plugin %s accepted user `%s` for authentication',
736 736 plugin.get_id(), display_user)
737 737
738 738 log.info('Authenticating user `%s` using %s plugin',
739 739 display_user, plugin.get_id())
740 740
741 741 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
742 742
743 743 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
744 744 plugin.get_id(), plugin_cache_active, cache_ttl)
745 745
746 746 user_id = user.user_id if user else None
747 747 # don't cache for empty users
748 748 plugin_cache_active = plugin_cache_active and user_id
749 749 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
750 750 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
751 751
752 752 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
753 753 expiration_time=cache_ttl,
754 754 condition=plugin_cache_active)
755 755 def compute_auth(
756 756 cache_name, plugin_name, username, password):
757 757
758 758 # _authenticate is a wrapper for .auth() method of plugin.
759 759 # it checks if .auth() sends proper data.
760 760 # For RhodeCodeExternalAuthPlugin it also maps users to
761 761 # Database and maps the attributes returned from .auth()
762 762 # to RhodeCode database. If this function returns data
763 763 # then auth is correct.
764 764 log.debug('Running plugin `%s` _authenticate method '
765 765 'using username and password', plugin.get_id())
766 766 return plugin._authenticate(
767 767 user, username, password, plugin_settings,
768 768 environ=environ or {})
769 769
770 770 start = time.time()
771 771 # for environ based auth, password can be empty, but then the validation is
772 772 # on the server that fills in the env data needed for authentication
773 773 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
774 774
775 775 auth_time = time.time() - start
776 776 log.debug('Authentication for plugin `%s` completed in %.4fs, '
777 777 'expiration time of fetched cache %.1fs.',
778 778 plugin.get_id(), auth_time, cache_ttl)
779 779
780 780 log.debug('PLUGIN USER DATA: %s', plugin_user)
781 781
782 782 if plugin_user:
783 783 log.debug('Plugin returned proper authentication data')
784 784 return plugin_user
785 785 # we failed to Auth because .auth() method didn't return proper user
786 786 log.debug("User `%s` failed to authenticate against %s",
787 787 display_user, plugin.get_id())
788 788
789 789 # case when we failed to authenticate against all defined plugins
790 790 return None
791 791
792 792
793 793 def chop_at(s, sub, inclusive=False):
794 794 """Truncate string ``s`` at the first occurrence of ``sub``.
795 795
796 796 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
797 797
798 798 >>> chop_at("plutocratic brats", "rat")
799 799 'plutoc'
800 800 >>> chop_at("plutocratic brats", "rat", True)
801 801 'plutocrat'
802 802 """
803 803 pos = s.find(sub)
804 804 if pos == -1:
805 805 return s
806 806 if inclusive:
807 807 return s[:pos+len(sub)]
808 808 return s[:pos]
@@ -1,535 +1,535 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 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 import logging
26 26 import traceback
27 27
28 28 import colander
29 29 from rhodecode.translation import _
30 30 from rhodecode.authentication.base import (
31 31 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
32 32 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 33 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 34 from rhodecode.lib.colander_utils import strip_whitespace
35 35 from rhodecode.lib.exceptions import (
36 36 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
37 37 )
38 38 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 39 from rhodecode.model.db import User
40 40 from rhodecode.model.validators import Missing
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44 try:
45 45 import ldap
46 46 except ImportError:
47 47 # means that python-ldap is not installed, we use Missing object to mark
48 48 # ldap lib is Missing
49 49 ldap = Missing
50 50
51 51
52 52 class LdapError(Exception):
53 53 pass
54 54
55 55
56 56 def plugin_factory(plugin_id, *args, **kwargs):
57 57 """
58 58 Factory function that is called during plugin discovery.
59 59 It returns the plugin instance.
60 60 """
61 61 plugin = RhodeCodeAuthPlugin(plugin_id)
62 62 return plugin
63 63
64 64
65 65 class LdapAuthnResource(AuthnPluginResourceBase):
66 66 pass
67 67
68 68
69 69 class AuthLdap(AuthLdapBase):
70 70 default_tls_cert_dir = '/etc/openldap/cacerts'
71 71
72 72 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
73 73 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
74 74 tls_cert_dir=None, ldap_version=3,
75 75 search_scope='SUBTREE', attr_login='uid',
76 76 ldap_filter='', timeout=None):
77 77 if ldap == Missing:
78 78 raise LdapImportError("Missing or incompatible ldap library")
79 79
80 80 self.debug = False
81 81 self.timeout = timeout or 60 * 5
82 82 self.ldap_version = ldap_version
83 83 self.ldap_server_type = 'ldap'
84 84
85 85 self.TLS_KIND = tls_kind
86 86
87 87 if self.TLS_KIND == 'LDAPS':
88 88 port = port or 689
89 89 self.ldap_server_type += 's'
90 90
91 91 OPT_X_TLS_DEMAND = 2
92 92 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
93 93 self.TLS_CERT_FILE = tls_cert_file or ''
94 94 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
95 95
96 96 # split server into list
97 97 self.SERVER_ADDRESSES = self._get_server_list(server)
98 98 self.LDAP_SERVER_PORT = port
99 99
100 100 # USE FOR READ ONLY BIND TO LDAP SERVER
101 101 self.attr_login = attr_login
102 102
103 103 self.LDAP_BIND_DN = safe_str(bind_dn)
104 104 self.LDAP_BIND_PASS = safe_str(bind_pass)
105 105
106 106 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
107 107 self.BASE_DN = safe_str(base_dn)
108 108 self.LDAP_FILTER = safe_str(ldap_filter)
109 109
110 110 def _get_ldap_conn(self):
111 111
112 112 if self.debug:
113 113 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
114 114
115 115 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
116 116 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
117 117
118 118 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
119 119 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
120 120
121 121 if self.TLS_KIND != 'PLAIN':
122 122 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
123 123
124 124 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
125 125 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
126 126
127 127 # init connection now
128 128 ldap_servers = self._build_servers(
129 129 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
130 130 log.debug('initializing LDAP connection to:%s', ldap_servers)
131 131 ldap_conn = ldap.initialize(ldap_servers)
132 132 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
133 133 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
134 134 ldap_conn.timeout = self.timeout
135 135
136 136 if self.ldap_version == 2:
137 137 ldap_conn.protocol = ldap.VERSION2
138 138 else:
139 139 ldap_conn.protocol = ldap.VERSION3
140 140
141 141 if self.TLS_KIND == 'START_TLS':
142 142 ldap_conn.start_tls_s()
143 143
144 144 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
145 145 log.debug('Trying simple_bind with password and given login DN: %r',
146 146 self.LDAP_BIND_DN)
147 147 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
148
148 log.debug('simple_bind successful')
149 149 return ldap_conn
150 150
151 151 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
152 152 try:
153 153 log.debug('Trying simple bind with %r', dn)
154 154 server.simple_bind_s(dn, safe_str(password))
155 user = server.search_ext_s(
155 _dn, attrs = server.search_ext_s(
156 156 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
157 _, attrs = user
157
158 158 return attrs
159 159
160 160 except ldap.INVALID_CREDENTIALS:
161 161 log.debug(
162 162 "LDAP rejected password for user '%s': %s, org_exc:",
163 163 username, dn, exc_info=True)
164 164
165 165 def authenticate_ldap(self, username, password):
166 166 """
167 167 Authenticate a user via LDAP and return his/her LDAP properties.
168 168
169 169 Raises AuthenticationError if the credentials are rejected, or
170 170 EnvironmentError if the LDAP server can't be reached.
171 171
172 172 :param username: username
173 173 :param password: password
174 174 """
175 175
176 176 uid = self.get_uid(username, self.SERVER_ADDRESSES)
177 177 user_attrs = {}
178 178 dn = ''
179 179
180 180 self.validate_password(username, password)
181 181 self.validate_username(username)
182 182
183 183 ldap_conn = None
184 184 try:
185 185 ldap_conn = self._get_ldap_conn()
186 186 filter_ = '(&%s(%s=%s))' % (
187 187 self.LDAP_FILTER, self.attr_login, username)
188 188 log.debug("Authenticating %r filter %s", self.BASE_DN, filter_)
189 189
190 190 lobjects = ldap_conn.search_ext_s(
191 191 self.BASE_DN, self.SEARCH_SCOPE, filter_)
192 192
193 193 if not lobjects:
194 194 log.debug("No matching LDAP objects for authentication "
195 195 "of UID:'%s' username:(%s)", uid, username)
196 196 raise ldap.NO_SUCH_OBJECT()
197 197
198 198 log.debug('Found matching ldap object, trying to authenticate')
199 199 for (dn, _attrs) in lobjects:
200 200 if dn is None:
201 201 continue
202 202
203 203 user_attrs = self.fetch_attrs_from_simple_bind(
204 204 ldap_conn, dn, username, password)
205 205 if user_attrs:
206 206 break
207 207 else:
208 208 raise LdapPasswordError(
209 209 'Failed to authenticate user `{}`'
210 210 'with given password'.format(username))
211 211
212 212 except ldap.NO_SUCH_OBJECT:
213 213 log.debug("LDAP says no such user '%s' (%s), org_exc:",
214 214 uid, username, exc_info=True)
215 215 raise LdapUsernameError('Unable to find user')
216 216 except ldap.SERVER_DOWN:
217 217 org_exc = traceback.format_exc()
218 218 raise LdapConnectionError(
219 219 "LDAP can't access authentication "
220 220 "server, org_exc:%s" % org_exc)
221 221 finally:
222 222 if ldap_conn:
223 223 log.debug('ldap: connection release')
224 224 try:
225 225 ldap_conn.unbind_s()
226 226 except Exception:
227 227 # for any reason this can raise exception we must catch it
228 228 # to not crush the server
229 229 pass
230 230
231 231 return dn, user_attrs
232 232
233 233
234 234 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
235 235 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
236 236 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
237 237 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
238 238
239 239 host = colander.SchemaNode(
240 240 colander.String(),
241 241 default='',
242 242 description=_('Host[s] of the LDAP Server \n'
243 243 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
244 244 'Multiple servers can be specified using commas'),
245 245 preparer=strip_whitespace,
246 246 title=_('LDAP Host'),
247 247 widget='string')
248 248 port = colander.SchemaNode(
249 249 colander.Int(),
250 250 default=389,
251 251 description=_('Custom port that the LDAP server is listening on. '
252 252 'Default value is: 389, use 689 for LDAPS(SSL)'),
253 253 preparer=strip_whitespace,
254 254 title=_('Port'),
255 255 validator=colander.Range(min=0, max=65536),
256 256 widget='int')
257 257
258 258 timeout = colander.SchemaNode(
259 259 colander.Int(),
260 260 default=60 * 5,
261 261 description=_('Timeout for LDAP connection'),
262 262 preparer=strip_whitespace,
263 263 title=_('Connection timeout'),
264 264 validator=colander.Range(min=1),
265 265 widget='int')
266 266
267 267 dn_user = colander.SchemaNode(
268 268 colander.String(),
269 269 default='',
270 270 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
271 271 'e.g., cn=admin,dc=mydomain,dc=com, or '
272 272 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
273 273 missing='',
274 274 preparer=strip_whitespace,
275 title=_('Account'),
275 title=_('Bind account'),
276 276 widget='string')
277 277 dn_pass = colander.SchemaNode(
278 278 colander.String(),
279 279 default='',
280 280 description=_('Password to authenticate for given user DN.'),
281 281 missing='',
282 282 preparer=strip_whitespace,
283 title=_('Password'),
283 title=_('Bind account password'),
284 284 widget='password')
285 285 tls_kind = colander.SchemaNode(
286 286 colander.String(),
287 287 default=tls_kind_choices[0],
288 288 description=_('TLS Type'),
289 289 title=_('Connection Security'),
290 290 validator=colander.OneOf(tls_kind_choices),
291 291 widget='select')
292 292 tls_reqcert = colander.SchemaNode(
293 293 colander.String(),
294 294 default=tls_reqcert_choices[0],
295 295 description=_('Require Cert over TLS?. Self-signed and custom '
296 296 'certificates can be used when\n `RhodeCode Certificate` '
297 297 'found in admin > settings > system info page is extended.'),
298 298 title=_('Certificate Checks'),
299 299 validator=colander.OneOf(tls_reqcert_choices),
300 300 widget='select')
301 301 tls_cert_file = colander.SchemaNode(
302 302 colander.String(),
303 303 default='',
304 304 description=_('This specifies the PEM-format file path containing '
305 305 'certificates for use in TLS connection.\n'
306 306 'If not specified `TLS Cert dir` will be used'),
307 307 title=_('TLS Cert file'),
308 308 missing='',
309 309 widget='string')
310 310 tls_cert_dir = colander.SchemaNode(
311 311 colander.String(),
312 312 default=AuthLdap.default_tls_cert_dir,
313 313 description=_('This specifies the path of a directory that contains individual '
314 314 'CA certificates in separate files.'),
315 315 title=_('TLS Cert dir'),
316 316 widget='string')
317 317 base_dn = colander.SchemaNode(
318 318 colander.String(),
319 319 default='',
320 320 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
321 'in it to be replaced with current user credentials \n'
321 'in it to be replaced with current user username \n'
322 322 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
323 323 missing='',
324 324 preparer=strip_whitespace,
325 325 title=_('Base DN'),
326 326 widget='string')
327 327 filter = colander.SchemaNode(
328 328 colander.String(),
329 329 default='',
330 330 description=_('Filter to narrow results \n'
331 331 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
332 332 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
333 333 missing='',
334 334 preparer=strip_whitespace,
335 335 title=_('LDAP Search Filter'),
336 336 widget='string')
337 337
338 338 search_scope = colander.SchemaNode(
339 339 colander.String(),
340 340 default=search_scope_choices[2],
341 341 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
342 342 title=_('LDAP Search Scope'),
343 343 validator=colander.OneOf(search_scope_choices),
344 344 widget='select')
345 345 attr_login = colander.SchemaNode(
346 346 colander.String(),
347 347 default='uid',
348 348 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
349 349 preparer=strip_whitespace,
350 350 title=_('Login Attribute'),
351 351 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
352 352 widget='string')
353 353 attr_email = colander.SchemaNode(
354 354 colander.String(),
355 355 default='',
356 356 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
357 357 'Emails are a crucial part of RhodeCode. \n'
358 358 'If possible add a valid email attribute to ldap users.'),
359 359 missing='',
360 360 preparer=strip_whitespace,
361 361 title=_('Email Attribute'),
362 362 widget='string')
363 363 attr_firstname = colander.SchemaNode(
364 364 colander.String(),
365 365 default='',
366 366 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
367 367 missing='',
368 368 preparer=strip_whitespace,
369 369 title=_('First Name Attribute'),
370 370 widget='string')
371 371 attr_lastname = colander.SchemaNode(
372 372 colander.String(),
373 373 default='',
374 374 description=_('LDAP Attribute to map to last name (e.g., sn)'),
375 375 missing='',
376 376 preparer=strip_whitespace,
377 377 title=_('Last Name Attribute'),
378 378 widget='string')
379 379
380 380
381 381 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
382 382 uid = 'ldap'
383 383 # used to define dynamic binding in the
384 384 DYNAMIC_BIND_VAR = '$login'
385 385 _settings_unsafe_keys = ['dn_pass']
386 386
387 387 def includeme(self, config):
388 388 config.add_authn_plugin(self)
389 389 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
390 390 config.add_view(
391 391 'rhodecode.authentication.views.AuthnPluginViewBase',
392 392 attr='settings_get',
393 393 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
394 394 request_method='GET',
395 395 route_name='auth_home',
396 396 context=LdapAuthnResource)
397 397 config.add_view(
398 398 'rhodecode.authentication.views.AuthnPluginViewBase',
399 399 attr='settings_post',
400 400 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
401 401 request_method='POST',
402 402 route_name='auth_home',
403 403 context=LdapAuthnResource)
404 404
405 405 def get_settings_schema(self):
406 406 return LdapSettingsSchema()
407 407
408 408 def get_display_name(self):
409 409 return _('LDAP')
410 410
411 411 @classmethod
412 412 def docs(cls):
413 413 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
414 414
415 415 @hybrid_property
416 416 def name(self):
417 417 return u"ldap"
418 418
419 419 def use_fake_password(self):
420 420 return True
421 421
422 422 def user_activation_state(self):
423 423 def_user_perms = User.get_default_user().AuthUser().permissions['global']
424 424 return 'hg.extern_activate.auto' in def_user_perms
425 425
426 426 def try_dynamic_binding(self, username, password, current_args):
427 427 """
428 428 Detects marker inside our original bind, and uses dynamic auth if
429 429 present
430 430 """
431 431
432 432 org_bind = current_args['bind_dn']
433 433 passwd = current_args['bind_pass']
434 434
435 435 def has_bind_marker(username):
436 436 if self.DYNAMIC_BIND_VAR in username:
437 437 return True
438 438
439 439 # we only passed in user with "special" variable
440 440 if org_bind and has_bind_marker(org_bind) and not passwd:
441 441 log.debug('Using dynamic user/password binding for ldap '
442 442 'authentication. Replacing `%s` with username',
443 443 self.DYNAMIC_BIND_VAR)
444 444 current_args['bind_dn'] = org_bind.replace(
445 445 self.DYNAMIC_BIND_VAR, username)
446 446 current_args['bind_pass'] = password
447 447
448 448 return current_args
449 449
450 450 def auth(self, userobj, username, password, settings, **kwargs):
451 451 """
452 452 Given a user object (which may be null), username, a plaintext password,
453 453 and a settings object (containing all the keys needed as listed in
454 454 settings()), authenticate this user's login attempt.
455 455
456 456 Return None on failure. On success, return a dictionary of the form:
457 457
458 458 see: RhodeCodeAuthPluginBase.auth_func_attrs
459 459 This is later validated for correctness
460 460 """
461 461
462 462 if not username or not password:
463 463 log.debug('Empty username or password skipping...')
464 464 return None
465 465
466 466 ldap_args = {
467 467 'server': settings.get('host', ''),
468 468 'base_dn': settings.get('base_dn', ''),
469 469 'port': settings.get('port'),
470 470 'bind_dn': settings.get('dn_user'),
471 471 'bind_pass': settings.get('dn_pass'),
472 472 'tls_kind': settings.get('tls_kind'),
473 473 'tls_reqcert': settings.get('tls_reqcert'),
474 474 'tls_cert_file': settings.get('tls_cert_file'),
475 475 'tls_cert_dir': settings.get('tls_cert_dir'),
476 476 'search_scope': settings.get('search_scope'),
477 477 'attr_login': settings.get('attr_login'),
478 478 'ldap_version': 3,
479 479 'ldap_filter': settings.get('filter'),
480 480 'timeout': settings.get('timeout')
481 481 }
482 482
483 483 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
484 484
485 485 log.debug('Checking for ldap authentication.')
486 486
487 487 try:
488 488 aldap = AuthLdap(**ldap_args)
489 489 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
490 490 log.debug('Got ldap DN response %s', user_dn)
491 491
492 492 def get_ldap_attr(k):
493 493 return ldap_attrs.get(settings.get(k), [''])[0]
494 494
495 495 # old attrs fetched from RhodeCode database
496 496 admin = getattr(userobj, 'admin', False)
497 497 active = getattr(userobj, 'active', True)
498 498 email = getattr(userobj, 'email', '')
499 499 username = getattr(userobj, 'username', username)
500 500 firstname = getattr(userobj, 'firstname', '')
501 501 lastname = getattr(userobj, 'lastname', '')
502 502 extern_type = getattr(userobj, 'extern_type', '')
503 503
504 504 groups = []
505 505
506 506 user_attrs = {
507 507 'username': username,
508 508 'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
509 509 'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
510 510 'groups': groups,
511 511 'user_group_sync': False,
512 512 'email': get_ldap_attr('attr_email') or email,
513 513 'admin': admin,
514 514 'active': active,
515 515 'active_from_extern': None,
516 516 'extern_name': user_dn,
517 517 'extern_type': extern_type,
518 518 }
519 519
520 520 log.debug('ldap user: %s', user_attrs)
521 521 log.info('user `%s` authenticated correctly', user_attrs['username'])
522 522
523 523 return user_attrs
524 524
525 525 except (LdapUsernameError, LdapPasswordError, LdapImportError):
526 526 log.exception("LDAP related exception")
527 527 return None
528 528 except (Exception,):
529 529 log.exception("Other exception")
530 530 return None
531 531
532 532
533 533 def includeme(config):
534 534 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
535 535 plugin_factory(plugin_id).includeme(config)
General Comments 0
You need to be logged in to leave comments. Login now