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