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