##// END OF EJS Templates
authentication: register global shared session key used for external authentication session data storeage....
marcink -
r3247:3c175ca2 default
parent child Browse files
Show More
@@ -1,795 +1,797 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, 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 external_auth_session_key = 'rhodecode.external_auth'
57
56 58
57 59 class hybrid_property(object):
58 60 """
59 61 a property decorator that works both for instance and class
60 62 """
61 63 def __init__(self, fget, fset=None, fdel=None, expr=None):
62 64 self.fget = fget
63 65 self.fset = fset
64 66 self.fdel = fdel
65 67 self.expr = expr or fget
66 68 functools.update_wrapper(self, fget)
67 69
68 70 def __get__(self, instance, owner):
69 71 if instance is None:
70 72 return self.expr(owner)
71 73 else:
72 74 return self.fget(instance)
73 75
74 76 def __set__(self, instance, value):
75 77 self.fset(instance, value)
76 78
77 79 def __delete__(self, instance):
78 80 self.fdel(instance)
79 81
80 82
81 83 class LazyFormencode(object):
82 84 def __init__(self, formencode_obj, *args, **kwargs):
83 85 self.formencode_obj = formencode_obj
84 86 self.args = args
85 87 self.kwargs = kwargs
86 88
87 89 def __call__(self, *args, **kwargs):
88 90 from inspect import isfunction
89 91 formencode_obj = self.formencode_obj
90 92 if isfunction(formencode_obj):
91 93 # case we wrap validators into functions
92 94 formencode_obj = self.formencode_obj(*args, **kwargs)
93 95 return formencode_obj(*self.args, **self.kwargs)
94 96
95 97
96 98 class RhodeCodeAuthPluginBase(object):
97 99 # UID is used to register plugin to the registry
98 100 uid = None
99 101
100 102 # cache the authentication request for N amount of seconds. Some kind
101 103 # of authentication methods are very heavy and it's very efficient to cache
102 104 # the result of a call. If it's set to None (default) cache is off
103 105 AUTH_CACHE_TTL = None
104 106 AUTH_CACHE = {}
105 107
106 108 auth_func_attrs = {
107 109 "username": "unique username",
108 110 "firstname": "first name",
109 111 "lastname": "last name",
110 112 "email": "email address",
111 113 "groups": '["list", "of", "groups"]',
112 114 "user_group_sync":
113 115 'True|False defines if returned user groups should be synced',
114 116 "extern_name": "name in external source of record",
115 117 "extern_type": "type of external source of record",
116 118 "admin": 'True|False defines if user should be RhodeCode super admin',
117 119 "active":
118 120 'True|False defines active state of user internally for RhodeCode',
119 121 "active_from_extern":
120 122 "True|False\None, active state from the external auth, "
121 123 "None means use definition from RhodeCode extern_type active value"
122 124
123 125 }
124 126 # set on authenticate() method and via set_auth_type func.
125 127 auth_type = None
126 128
127 129 # set on authenticate() method and via set_calling_scope_repo, this is a
128 130 # calling scope repository when doing authentication most likely on VCS
129 131 # operations
130 132 acl_repo_name = None
131 133
132 134 # List of setting names to store encrypted. Plugins may override this list
133 135 # to store settings encrypted.
134 136 _settings_encrypted = []
135 137
136 138 # Mapping of python to DB settings model types. Plugins may override or
137 139 # extend this mapping.
138 140 _settings_type_map = {
139 141 colander.String: 'unicode',
140 142 colander.Integer: 'int',
141 143 colander.Boolean: 'bool',
142 144 colander.List: 'list',
143 145 }
144 146
145 147 # list of keys in settings that are unsafe to be logged, should be passwords
146 148 # or other crucial credentials
147 149 _settings_unsafe_keys = []
148 150
149 151 def __init__(self, plugin_id):
150 152 self._plugin_id = plugin_id
151 153
152 154 def __str__(self):
153 155 return self.get_id()
154 156
155 157 def _get_setting_full_name(self, name):
156 158 """
157 159 Return the full setting name used for storing values in the database.
158 160 """
159 161 # TODO: johbo: Using the name here is problematic. It would be good to
160 162 # introduce either new models in the database to hold Plugin and
161 163 # PluginSetting or to use the plugin id here.
162 164 return 'auth_{}_{}'.format(self.name, name)
163 165
164 166 def _get_setting_type(self, name):
165 167 """
166 168 Return the type of a setting. This type is defined by the SettingsModel
167 169 and determines how the setting is stored in DB. Optionally the suffix
168 170 `.encrypted` is appended to instruct SettingsModel to store it
169 171 encrypted.
170 172 """
171 173 schema_node = self.get_settings_schema().get(name)
172 174 db_type = self._settings_type_map.get(
173 175 type(schema_node.typ), 'unicode')
174 176 if name in self._settings_encrypted:
175 177 db_type = '{}.encrypted'.format(db_type)
176 178 return db_type
177 179
178 180 @classmethod
179 181 def docs(cls):
180 182 """
181 183 Defines documentation url which helps with plugin setup
182 184 """
183 185 return ''
184 186
185 187 @classmethod
186 188 def icon(cls):
187 189 """
188 190 Defines ICON in SVG format for authentication method
189 191 """
190 192 return ''
191 193
192 194 def is_enabled(self):
193 195 """
194 196 Returns true if this plugin is enabled. An enabled plugin can be
195 197 configured in the admin interface but it is not consulted during
196 198 authentication.
197 199 """
198 200 auth_plugins = SettingsModel().get_auth_plugins()
199 201 return self.get_id() in auth_plugins
200 202
201 203 def is_active(self, plugin_cached_settings=None):
202 204 """
203 205 Returns true if the plugin is activated. An activated plugin is
204 206 consulted during authentication, assumed it is also enabled.
205 207 """
206 208 return self.get_setting_by_name(
207 209 'enabled', plugin_cached_settings=plugin_cached_settings)
208 210
209 211 def get_id(self):
210 212 """
211 213 Returns the plugin id.
212 214 """
213 215 return self._plugin_id
214 216
215 217 def get_display_name(self):
216 218 """
217 219 Returns a translation string for displaying purposes.
218 220 """
219 221 raise NotImplementedError('Not implemented in base class')
220 222
221 223 def get_settings_schema(self):
222 224 """
223 225 Returns a colander schema, representing the plugin settings.
224 226 """
225 227 return AuthnPluginSettingsSchemaBase()
226 228
227 229 def get_settings(self):
228 230 """
229 231 Returns the plugin settings as dictionary.
230 232 """
231 233 settings = {}
232 234 raw_settings = SettingsModel().get_all_settings()
233 235 for node in self.get_settings_schema():
234 236 settings[node.name] = self.get_setting_by_name(
235 237 node.name, plugin_cached_settings=raw_settings)
236 238 return settings
237 239
238 240 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
239 241 """
240 242 Returns a plugin setting by name.
241 243 """
242 244 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
243 245 if plugin_cached_settings:
244 246 plugin_settings = plugin_cached_settings
245 247 else:
246 248 plugin_settings = SettingsModel().get_all_settings()
247 249
248 250 if full_name in plugin_settings:
249 251 return plugin_settings[full_name]
250 252 else:
251 253 return default
252 254
253 255 def create_or_update_setting(self, name, value):
254 256 """
255 257 Create or update a setting for this plugin in the persistent storage.
256 258 """
257 259 full_name = self._get_setting_full_name(name)
258 260 type_ = self._get_setting_type(name)
259 261 db_setting = SettingsModel().create_or_update_setting(
260 262 full_name, value, type_)
261 263 return db_setting.app_settings_value
262 264
263 265 def log_safe_settings(self, settings):
264 266 """
265 267 returns a log safe representation of settings, without any secrets
266 268 """
267 269 settings_copy = copy.deepcopy(settings)
268 270 for k in self._settings_unsafe_keys:
269 271 if k in settings_copy:
270 272 del settings_copy[k]
271 273 return settings_copy
272 274
273 275 @hybrid_property
274 276 def name(self):
275 277 """
276 278 Returns the name of this authentication plugin.
277 279
278 280 :returns: string
279 281 """
280 282 raise NotImplementedError("Not implemented in base class")
281 283
282 284 def get_url_slug(self):
283 285 """
284 286 Returns a slug which should be used when constructing URLs which refer
285 287 to this plugin. By default it returns the plugin name. If the name is
286 288 not suitable for using it in an URL the plugin should override this
287 289 method.
288 290 """
289 291 return self.name
290 292
291 293 @property
292 294 def is_headers_auth(self):
293 295 """
294 296 Returns True if this authentication plugin uses HTTP headers as
295 297 authentication method.
296 298 """
297 299 return False
298 300
299 301 @hybrid_property
300 302 def is_container_auth(self):
301 303 """
302 304 Deprecated method that indicates if this authentication plugin uses
303 305 HTTP headers as authentication method.
304 306 """
305 307 warnings.warn(
306 308 'Use is_headers_auth instead.', category=DeprecationWarning)
307 309 return self.is_headers_auth
308 310
309 311 @hybrid_property
310 312 def allows_creating_users(self):
311 313 """
312 314 Defines if Plugin allows users to be created on-the-fly when
313 315 authentication is called. Controls how external plugins should behave
314 316 in terms if they are allowed to create new users, or not. Base plugins
315 317 should not be allowed to, but External ones should be !
316 318
317 319 :return: bool
318 320 """
319 321 return False
320 322
321 323 def set_auth_type(self, auth_type):
322 324 self.auth_type = auth_type
323 325
324 326 def set_calling_scope_repo(self, acl_repo_name):
325 327 self.acl_repo_name = acl_repo_name
326 328
327 329 def allows_authentication_from(
328 330 self, user, allows_non_existing_user=True,
329 331 allowed_auth_plugins=None, allowed_auth_sources=None):
330 332 """
331 333 Checks if this authentication module should accept a request for
332 334 the current user.
333 335
334 336 :param user: user object fetched using plugin's get_user() method.
335 337 :param allows_non_existing_user: if True, don't allow the
336 338 user to be empty, meaning not existing in our database
337 339 :param allowed_auth_plugins: if provided, users extern_type will be
338 340 checked against a list of provided extern types, which are plugin
339 341 auth_names in the end
340 342 :param allowed_auth_sources: authentication type allowed,
341 343 `http` or `vcs` default is both.
342 344 defines if plugin will accept only http authentication vcs
343 345 authentication(git/hg) or both
344 346 :returns: boolean
345 347 """
346 348 if not user and not allows_non_existing_user:
347 349 log.debug('User is empty but plugin does not allow empty users,'
348 350 'not allowed to authenticate')
349 351 return False
350 352
351 353 expected_auth_plugins = allowed_auth_plugins or [self.name]
352 354 if user and (user.extern_type and
353 355 user.extern_type not in expected_auth_plugins):
354 356 log.debug(
355 357 'User `%s` is bound to `%s` auth type. Plugin allows only '
356 358 '%s, skipping', user, user.extern_type, expected_auth_plugins)
357 359
358 360 return False
359 361
360 362 # by default accept both
361 363 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
362 364 if self.auth_type not in expected_auth_from:
363 365 log.debug('Current auth source is %s but plugin only allows %s',
364 366 self.auth_type, expected_auth_from)
365 367 return False
366 368
367 369 return True
368 370
369 371 def get_user(self, username=None, **kwargs):
370 372 """
371 373 Helper method for user fetching in plugins, by default it's using
372 374 simple fetch by username, but this method can be custimized in plugins
373 375 eg. headers auth plugin to fetch user by environ params
374 376
375 377 :param username: username if given to fetch from database
376 378 :param kwargs: extra arguments needed for user fetching.
377 379 """
378 380 user = None
379 381 log.debug(
380 382 'Trying to fetch user `%s` from RhodeCode database', username)
381 383 if username:
382 384 user = User.get_by_username(username)
383 385 if not user:
384 386 log.debug('User not found, fallback to fetch user in '
385 387 'case insensitive mode')
386 388 user = User.get_by_username(username, case_insensitive=True)
387 389 else:
388 390 log.debug('provided username:`%s` is empty skipping...', username)
389 391 if not user:
390 392 log.debug('User `%s` not found in database', username)
391 393 else:
392 394 log.debug('Got DB user:%s', user)
393 395 return user
394 396
395 397 def user_activation_state(self):
396 398 """
397 399 Defines user activation state when creating new users
398 400
399 401 :returns: boolean
400 402 """
401 403 raise NotImplementedError("Not implemented in base class")
402 404
403 405 def auth(self, userobj, username, passwd, settings, **kwargs):
404 406 """
405 407 Given a user object (which may be null), username, a plaintext
406 408 password, and a settings object (containing all the keys needed as
407 409 listed in settings()), authenticate this user's login attempt.
408 410
409 411 Return None on failure. On success, return a dictionary of the form:
410 412
411 413 see: RhodeCodeAuthPluginBase.auth_func_attrs
412 414 This is later validated for correctness
413 415 """
414 416 raise NotImplementedError("not implemented in base class")
415 417
416 418 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
417 419 """
418 420 Wrapper to call self.auth() that validates call on it
419 421
420 422 :param userobj: userobj
421 423 :param username: username
422 424 :param passwd: plaintext password
423 425 :param settings: plugin settings
424 426 """
425 427 auth = self.auth(userobj, username, passwd, settings, **kwargs)
426 428 if auth:
427 429 auth['_plugin'] = self.name
428 430 auth['_ttl_cache'] = self.get_ttl_cache(settings)
429 431 # check if hash should be migrated ?
430 432 new_hash = auth.get('_hash_migrate')
431 433 if new_hash:
432 434 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
433 435 if 'user_group_sync' not in auth:
434 436 auth['user_group_sync'] = False
435 437 return self._validate_auth_return(auth)
436 438 return auth
437 439
438 440 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
439 441 new_hash_cypher = _RhodeCodeCryptoBCrypt()
440 442 # extra checks, so make sure new hash is correct.
441 443 password_encoded = safe_str(password)
442 444 if new_hash and new_hash_cypher.hash_check(
443 445 password_encoded, new_hash):
444 446 cur_user = User.get_by_username(username)
445 447 cur_user.password = new_hash
446 448 Session().add(cur_user)
447 449 Session().flush()
448 450 log.info('Migrated user %s hash to bcrypt', cur_user)
449 451
450 452 def _validate_auth_return(self, ret):
451 453 if not isinstance(ret, dict):
452 454 raise Exception('returned value from auth must be a dict')
453 455 for k in self.auth_func_attrs:
454 456 if k not in ret:
455 457 raise Exception('Missing %s attribute from returned data' % k)
456 458 return ret
457 459
458 460 def get_ttl_cache(self, settings=None):
459 461 plugin_settings = settings or self.get_settings()
460 462 # we set default to 30, we make a compromise here,
461 463 # performance > security, mostly due to LDAP/SVN, majority
462 464 # of users pick cache_ttl to be enabled
463 465 from rhodecode.authentication import plugin_default_auth_ttl
464 466 cache_ttl = plugin_default_auth_ttl
465 467
466 468 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
467 469 # plugin cache set inside is more important than the settings value
468 470 cache_ttl = self.AUTH_CACHE_TTL
469 471 elif plugin_settings.get('cache_ttl'):
470 472 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
471 473
472 474 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
473 475 return plugin_cache_active, cache_ttl
474 476
475 477
476 478 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
477 479
478 480 @hybrid_property
479 481 def allows_creating_users(self):
480 482 return True
481 483
482 484 def use_fake_password(self):
483 485 """
484 486 Return a boolean that indicates whether or not we should set the user's
485 487 password to a random value when it is authenticated by this plugin.
486 488 If your plugin provides authentication, then you will generally
487 489 want this.
488 490
489 491 :returns: boolean
490 492 """
491 493 raise NotImplementedError("Not implemented in base class")
492 494
493 495 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
494 496 # at this point _authenticate calls plugin's `auth()` function
495 497 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
496 498 userobj, username, passwd, settings, **kwargs)
497 499
498 500 if auth:
499 501 # maybe plugin will clean the username ?
500 502 # we should use the return value
501 503 username = auth['username']
502 504
503 505 # if external source tells us that user is not active, we should
504 506 # skip rest of the process. This can prevent from creating users in
505 507 # RhodeCode when using external authentication, but if it's
506 508 # inactive user we shouldn't create that user anyway
507 509 if auth['active_from_extern'] is False:
508 510 log.warning(
509 511 "User %s authenticated against %s, but is inactive",
510 512 username, self.__module__)
511 513 return None
512 514
513 515 cur_user = User.get_by_username(username, case_insensitive=True)
514 516 is_user_existing = cur_user is not None
515 517
516 518 if is_user_existing:
517 519 log.debug('Syncing user `%s` from '
518 520 '`%s` plugin', username, self.name)
519 521 else:
520 522 log.debug('Creating non existing user `%s` from '
521 523 '`%s` plugin', username, self.name)
522 524
523 525 if self.allows_creating_users:
524 526 log.debug('Plugin `%s` allows to '
525 527 'create new users', self.name)
526 528 else:
527 529 log.debug('Plugin `%s` does not allow to '
528 530 'create new users', self.name)
529 531
530 532 user_parameters = {
531 533 'username': username,
532 534 'email': auth["email"],
533 535 'firstname': auth["firstname"],
534 536 'lastname': auth["lastname"],
535 537 'active': auth["active"],
536 538 'admin': auth["admin"],
537 539 'extern_name': auth["extern_name"],
538 540 'extern_type': self.name,
539 541 'plugin': self,
540 542 'allow_to_create_user': self.allows_creating_users,
541 543 }
542 544
543 545 if not is_user_existing:
544 546 if self.use_fake_password():
545 547 # Randomize the PW because we don't need it, but don't want
546 548 # them blank either
547 549 passwd = PasswordGenerator().gen_password(length=16)
548 550 user_parameters['password'] = passwd
549 551 else:
550 552 # Since the password is required by create_or_update method of
551 553 # UserModel, we need to set it explicitly.
552 554 # The create_or_update method is smart and recognises the
553 555 # password hashes as well.
554 556 user_parameters['password'] = cur_user.password
555 557
556 558 # we either create or update users, we also pass the flag
557 559 # that controls if this method can actually do that.
558 560 # raises NotAllowedToCreateUserError if it cannot, and we try to.
559 561 user = UserModel().create_or_update(**user_parameters)
560 562 Session().flush()
561 563 # enforce user is just in given groups, all of them has to be ones
562 564 # created from plugins. We store this info in _group_data JSON
563 565 # field
564 566
565 567 if auth['user_group_sync']:
566 568 try:
567 569 groups = auth['groups'] or []
568 570 log.debug(
569 571 'Performing user_group sync based on set `%s` '
570 572 'returned by `%s` plugin', groups, self.name)
571 573 UserGroupModel().enforce_groups(user, groups, self.name)
572 574 except Exception:
573 575 # for any reason group syncing fails, we should
574 576 # proceed with login
575 577 log.error(traceback.format_exc())
576 578
577 579 Session().commit()
578 580 return auth
579 581
580 582
581 583 class AuthLdapBase(object):
582 584
583 585 @classmethod
584 586 def _build_servers(cls, ldap_server_type, ldap_server, port, use_resolver=True):
585 587
586 588 def host_resolver(host, port, full_resolve=True):
587 589 """
588 590 Main work for this function is to prevent ldap connection issues,
589 591 and detect them early using a "greenified" sockets
590 592 """
591 593 host = host.strip()
592 594 if not full_resolve:
593 595 return '{}:{}'.format(host, port)
594 596
595 597 log.debug('LDAP: Resolving IP for LDAP host %s', host)
596 598 try:
597 599 ip = socket.gethostbyname(host)
598 600 log.debug('Got LDAP server %s ip %s', host, ip)
599 601 except Exception:
600 602 raise LdapConnectionError(
601 603 'Failed to resolve host: `{}`'.format(host))
602 604
603 605 log.debug('LDAP: Checking if IP %s is accessible', ip)
604 606 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
605 607 try:
606 608 s.connect((ip, int(port)))
607 609 s.shutdown(socket.SHUT_RD)
608 610 except Exception:
609 611 raise LdapConnectionError(
610 612 'Failed to connect to host: `{}:{}`'.format(host, port))
611 613
612 614 return '{}:{}'.format(host, port)
613 615
614 616 if len(ldap_server) == 1:
615 617 # in case of single server use resolver to detect potential
616 618 # connection issues
617 619 full_resolve = True
618 620 else:
619 621 full_resolve = False
620 622
621 623 return ', '.join(
622 624 ["{}://{}".format(
623 625 ldap_server_type,
624 626 host_resolver(host, port, full_resolve=use_resolver and full_resolve))
625 627 for host in ldap_server])
626 628
627 629 @classmethod
628 630 def _get_server_list(cls, servers):
629 631 return map(string.strip, servers.split(','))
630 632
631 633 @classmethod
632 634 def get_uid(cls, username, server_addresses):
633 635 uid = username
634 636 for server_addr in server_addresses:
635 637 uid = chop_at(username, "@%s" % server_addr)
636 638 return uid
637 639
638 640 @classmethod
639 641 def validate_username(cls, username):
640 642 if "," in username:
641 643 raise LdapUsernameError(
642 644 "invalid character `,` in username: `{}`".format(username))
643 645
644 646 @classmethod
645 647 def validate_password(cls, username, password):
646 648 if not password:
647 649 msg = "Authenticating user %s with blank password not allowed"
648 650 log.warning(msg, username)
649 651 raise LdapPasswordError(msg)
650 652
651 653
652 654 def loadplugin(plugin_id):
653 655 """
654 656 Loads and returns an instantiated authentication plugin.
655 657 Returns the RhodeCodeAuthPluginBase subclass on success,
656 658 or None on failure.
657 659 """
658 660 # TODO: Disusing pyramids thread locals to retrieve the registry.
659 661 authn_registry = get_authn_registry()
660 662 plugin = authn_registry.get_plugin(plugin_id)
661 663 if plugin is None:
662 664 log.error('Authentication plugin not found: "%s"', plugin_id)
663 665 return plugin
664 666
665 667
666 668 def get_authn_registry(registry=None):
667 669 registry = registry or get_current_registry()
668 670 authn_registry = registry.getUtility(IAuthnPluginRegistry)
669 671 return authn_registry
670 672
671 673
672 674 def authenticate(username, password, environ=None, auth_type=None,
673 675 skip_missing=False, registry=None, acl_repo_name=None):
674 676 """
675 677 Authentication function used for access control,
676 678 It tries to authenticate based on enabled authentication modules.
677 679
678 680 :param username: username can be empty for headers auth
679 681 :param password: password can be empty for headers auth
680 682 :param environ: environ headers passed for headers auth
681 683 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
682 684 :param skip_missing: ignores plugins that are in db but not in environment
683 685 :returns: None if auth failed, plugin_user dict if auth is correct
684 686 """
685 687 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
686 688 raise ValueError('auth type must be on of http, vcs got "%s" instead'
687 689 % auth_type)
688 690 headers_only = environ and not (username and password)
689 691
690 692 authn_registry = get_authn_registry(registry)
691 693 plugins_to_check = authn_registry.get_plugins_for_authentication()
692 694 log.debug('Starting ordered authentication chain using %s plugins',
693 695 [x.name for x in plugins_to_check])
694 696 for plugin in plugins_to_check:
695 697 plugin.set_auth_type(auth_type)
696 698 plugin.set_calling_scope_repo(acl_repo_name)
697 699
698 700 if headers_only and not plugin.is_headers_auth:
699 701 log.debug('Auth type is for headers only and plugin `%s` is not '
700 702 'headers plugin, skipping...', plugin.get_id())
701 703 continue
702 704
703 705 log.debug('Trying authentication using ** %s **', plugin.get_id())
704 706
705 707 # load plugin settings from RhodeCode database
706 708 plugin_settings = plugin.get_settings()
707 709 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
708 710 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
709 711
710 712 # use plugin's method of user extraction.
711 713 user = plugin.get_user(username, environ=environ,
712 714 settings=plugin_settings)
713 715 display_user = user.username if user else username
714 716 log.debug(
715 717 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
716 718
717 719 if not plugin.allows_authentication_from(user):
718 720 log.debug('Plugin %s does not accept user `%s` for authentication',
719 721 plugin.get_id(), display_user)
720 722 continue
721 723 else:
722 724 log.debug('Plugin %s accepted user `%s` for authentication',
723 725 plugin.get_id(), display_user)
724 726
725 727 log.info('Authenticating user `%s` using %s plugin',
726 728 display_user, plugin.get_id())
727 729
728 730 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
729 731
730 732 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
731 733 plugin.get_id(), plugin_cache_active, cache_ttl)
732 734
733 735 user_id = user.user_id if user else None
734 736 # don't cache for empty users
735 737 plugin_cache_active = plugin_cache_active and user_id
736 738 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
737 739 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
738 740
739 741 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
740 742 expiration_time=cache_ttl,
741 743 condition=plugin_cache_active)
742 744 def compute_auth(
743 745 cache_name, plugin_name, username, password):
744 746
745 747 # _authenticate is a wrapper for .auth() method of plugin.
746 748 # it checks if .auth() sends proper data.
747 749 # For RhodeCodeExternalAuthPlugin it also maps users to
748 750 # Database and maps the attributes returned from .auth()
749 751 # to RhodeCode database. If this function returns data
750 752 # then auth is correct.
751 753 log.debug('Running plugin `%s` _authenticate method '
752 754 'using username and password', plugin.get_id())
753 755 return plugin._authenticate(
754 756 user, username, password, plugin_settings,
755 757 environ=environ or {})
756 758
757 759 start = time.time()
758 760 # for environ based auth, password can be empty, but then the validation is
759 761 # on the server that fills in the env data needed for authentication
760 762 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
761 763
762 764 auth_time = time.time() - start
763 765 log.debug('Authentication for plugin `%s` completed in %.3fs, '
764 766 'expiration time of fetched cache %.1fs.',
765 767 plugin.get_id(), auth_time, cache_ttl)
766 768
767 769 log.debug('PLUGIN USER DATA: %s', plugin_user)
768 770
769 771 if plugin_user:
770 772 log.debug('Plugin returned proper authentication data')
771 773 return plugin_user
772 774 # we failed to Auth because .auth() method didn't return proper user
773 775 log.debug("User `%s` failed to authenticate against %s",
774 776 display_user, plugin.get_id())
775 777
776 778 # case when we failed to authenticate against all defined plugins
777 779 return None
778 780
779 781
780 782 def chop_at(s, sub, inclusive=False):
781 783 """Truncate string ``s`` at the first occurrence of ``sub``.
782 784
783 785 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
784 786
785 787 >>> chop_at("plutocratic brats", "rat")
786 788 'plutoc'
787 789 >>> chop_at("plutocratic brats", "rat", True)
788 790 'plutocrat'
789 791 """
790 792 pos = s.find(sub)
791 793 if pos == -1:
792 794 return s
793 795 if inclusive:
794 796 return s[:pos+len(sub)]
795 797 return s[:pos]
General Comments 0
You need to be logged in to leave comments. Login now