##// END OF EJS Templates
auth-plugins: fixed problem with cache of settings in multi-worker mode....
marcink -
r2681:c2a00a0d default
parent child Browse files
Show More
@@ -1,727 +1,726 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
25 25 import colander
26 26 import copy
27 27 import logging
28 28 import time
29 29 import traceback
30 30 import warnings
31 31 import functools
32 32
33 33 from pyramid.threadlocal import get_current_registry
34 34 from zope.cachedescriptors.property import Lazy as LazyProperty
35 35
36 36 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 38 from rhodecode.lib import caches
39 39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 40 from rhodecode.lib.utils2 import safe_int
41 41 from rhodecode.lib.utils2 import safe_str
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 @LazyProperty
175 def plugin_settings(self):
176 settings = SettingsModel().get_all_settings()
177 return settings
178
179 174 def is_enabled(self):
180 175 """
181 176 Returns true if this plugin is enabled. An enabled plugin can be
182 177 configured in the admin interface but it is not consulted during
183 178 authentication.
184 179 """
185 180 auth_plugins = SettingsModel().get_auth_plugins()
186 181 return self.get_id() in auth_plugins
187 182
188 def is_active(self):
183 def is_active(self, plugin_cached_settings=None):
189 184 """
190 185 Returns true if the plugin is activated. An activated plugin is
191 186 consulted during authentication, assumed it is also enabled.
192 187 """
193 return self.get_setting_by_name('enabled')
188 return self.get_setting_by_name(
189 'enabled', plugin_cached_settings=plugin_cached_settings)
194 190
195 191 def get_id(self):
196 192 """
197 193 Returns the plugin id.
198 194 """
199 195 return self._plugin_id
200 196
201 197 def get_display_name(self):
202 198 """
203 199 Returns a translation string for displaying purposes.
204 200 """
205 201 raise NotImplementedError('Not implemented in base class')
206 202
207 203 def get_settings_schema(self):
208 204 """
209 205 Returns a colander schema, representing the plugin settings.
210 206 """
211 207 return AuthnPluginSettingsSchemaBase()
212 208
213 def get_setting_by_name(self, name, default=None, cache=True):
209 def get_settings(self):
210 """
211 Returns the plugin settings as dictionary.
212 """
213 settings = {}
214 raw_settings = SettingsModel().get_all_settings()
215 for node in self.get_settings_schema():
216 settings[node.name] = self.get_setting_by_name(
217 node.name, plugin_cached_settings=raw_settings)
218 return settings
219
220 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
214 221 """
215 222 Returns a plugin setting by name.
216 223 """
217 224 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
218 if cache:
219 plugin_settings = self.plugin_settings
225 if plugin_cached_settings:
226 plugin_settings = plugin_cached_settings
220 227 else:
221 228 plugin_settings = SettingsModel().get_all_settings()
222 229
223 230 if full_name in plugin_settings:
224 231 return plugin_settings[full_name]
225 232 else:
226 233 return default
227 234
228 235 def create_or_update_setting(self, name, value):
229 236 """
230 237 Create or update a setting for this plugin in the persistent storage.
231 238 """
232 239 full_name = self._get_setting_full_name(name)
233 240 type_ = self._get_setting_type(name)
234 241 db_setting = SettingsModel().create_or_update_setting(
235 242 full_name, value, type_)
236 243 return db_setting.app_settings_value
237 244
238 def get_settings(self):
239 """
240 Returns the plugin settings as dictionary.
241 """
242 settings = {}
243 for node in self.get_settings_schema():
244 settings[node.name] = self.get_setting_by_name(node.name)
245 return settings
246
247 245 def log_safe_settings(self, settings):
248 246 """
249 247 returns a log safe representation of settings, without any secrets
250 248 """
251 249 settings_copy = copy.deepcopy(settings)
252 250 for k in self._settings_unsafe_keys:
253 251 if k in settings_copy:
254 252 del settings_copy[k]
255 253 return settings_copy
256 254
257 255 @hybrid_property
258 256 def name(self):
259 257 """
260 258 Returns the name of this authentication plugin.
261 259
262 260 :returns: string
263 261 """
264 262 raise NotImplementedError("Not implemented in base class")
265 263
266 264 def get_url_slug(self):
267 265 """
268 266 Returns a slug which should be used when constructing URLs which refer
269 267 to this plugin. By default it returns the plugin name. If the name is
270 268 not suitable for using it in an URL the plugin should override this
271 269 method.
272 270 """
273 271 return self.name
274 272
275 273 @property
276 274 def is_headers_auth(self):
277 275 """
278 276 Returns True if this authentication plugin uses HTTP headers as
279 277 authentication method.
280 278 """
281 279 return False
282 280
283 281 @hybrid_property
284 282 def is_container_auth(self):
285 283 """
286 284 Deprecated method that indicates if this authentication plugin uses
287 285 HTTP headers as authentication method.
288 286 """
289 287 warnings.warn(
290 288 'Use is_headers_auth instead.', category=DeprecationWarning)
291 289 return self.is_headers_auth
292 290
293 291 @hybrid_property
294 292 def allows_creating_users(self):
295 293 """
296 294 Defines if Plugin allows users to be created on-the-fly when
297 295 authentication is called. Controls how external plugins should behave
298 296 in terms if they are allowed to create new users, or not. Base plugins
299 297 should not be allowed to, but External ones should be !
300 298
301 299 :return: bool
302 300 """
303 301 return False
304 302
305 303 def set_auth_type(self, auth_type):
306 304 self.auth_type = auth_type
307 305
308 306 def set_calling_scope_repo(self, acl_repo_name):
309 307 self.acl_repo_name = acl_repo_name
310 308
311 309 def allows_authentication_from(
312 310 self, user, allows_non_existing_user=True,
313 311 allowed_auth_plugins=None, allowed_auth_sources=None):
314 312 """
315 313 Checks if this authentication module should accept a request for
316 314 the current user.
317 315
318 316 :param user: user object fetched using plugin's get_user() method.
319 317 :param allows_non_existing_user: if True, don't allow the
320 318 user to be empty, meaning not existing in our database
321 319 :param allowed_auth_plugins: if provided, users extern_type will be
322 320 checked against a list of provided extern types, which are plugin
323 321 auth_names in the end
324 322 :param allowed_auth_sources: authentication type allowed,
325 323 `http` or `vcs` default is both.
326 324 defines if plugin will accept only http authentication vcs
327 325 authentication(git/hg) or both
328 326 :returns: boolean
329 327 """
330 328 if not user and not allows_non_existing_user:
331 329 log.debug('User is empty but plugin does not allow empty users,'
332 330 'not allowed to authenticate')
333 331 return False
334 332
335 333 expected_auth_plugins = allowed_auth_plugins or [self.name]
336 334 if user and (user.extern_type and
337 335 user.extern_type not in expected_auth_plugins):
338 336 log.debug(
339 337 'User `%s` is bound to `%s` auth type. Plugin allows only '
340 338 '%s, skipping', user, user.extern_type, expected_auth_plugins)
341 339
342 340 return False
343 341
344 342 # by default accept both
345 343 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
346 344 if self.auth_type not in expected_auth_from:
347 345 log.debug('Current auth source is %s but plugin only allows %s',
348 346 self.auth_type, expected_auth_from)
349 347 return False
350 348
351 349 return True
352 350
353 351 def get_user(self, username=None, **kwargs):
354 352 """
355 353 Helper method for user fetching in plugins, by default it's using
356 354 simple fetch by username, but this method can be custimized in plugins
357 355 eg. headers auth plugin to fetch user by environ params
358 356
359 357 :param username: username if given to fetch from database
360 358 :param kwargs: extra arguments needed for user fetching.
361 359 """
362 360 user = None
363 361 log.debug(
364 362 'Trying to fetch user `%s` from RhodeCode database', username)
365 363 if username:
366 364 user = User.get_by_username(username)
367 365 if not user:
368 366 log.debug('User not found, fallback to fetch user in '
369 367 'case insensitive mode')
370 368 user = User.get_by_username(username, case_insensitive=True)
371 369 else:
372 370 log.debug('provided username:`%s` is empty skipping...', username)
373 371 if not user:
374 372 log.debug('User `%s` not found in database', username)
375 373 else:
376 374 log.debug('Got DB user:%s', user)
377 375 return user
378 376
379 377 def user_activation_state(self):
380 378 """
381 379 Defines user activation state when creating new users
382 380
383 381 :returns: boolean
384 382 """
385 383 raise NotImplementedError("Not implemented in base class")
386 384
387 385 def auth(self, userobj, username, passwd, settings, **kwargs):
388 386 """
389 387 Given a user object (which may be null), username, a plaintext
390 388 password, and a settings object (containing all the keys needed as
391 389 listed in settings()), authenticate this user's login attempt.
392 390
393 391 Return None on failure. On success, return a dictionary of the form:
394 392
395 393 see: RhodeCodeAuthPluginBase.auth_func_attrs
396 394 This is later validated for correctness
397 395 """
398 396 raise NotImplementedError("not implemented in base class")
399 397
400 398 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
401 399 """
402 400 Wrapper to call self.auth() that validates call on it
403 401
404 402 :param userobj: userobj
405 403 :param username: username
406 404 :param passwd: plaintext password
407 405 :param settings: plugin settings
408 406 """
409 407 auth = self.auth(userobj, username, passwd, settings, **kwargs)
410 408 if auth:
411 409 auth['_plugin'] = self.name
412 410 auth['_ttl_cache'] = self.get_ttl_cache(settings)
413 411 # check if hash should be migrated ?
414 412 new_hash = auth.get('_hash_migrate')
415 413 if new_hash:
416 414 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
417 415 if 'user_group_sync' not in auth:
418 416 auth['user_group_sync'] = False
419 417 return self._validate_auth_return(auth)
420 418 return auth
421 419
422 420 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
423 421 new_hash_cypher = _RhodeCodeCryptoBCrypt()
424 422 # extra checks, so make sure new hash is correct.
425 423 password_encoded = safe_str(password)
426 424 if new_hash and new_hash_cypher.hash_check(
427 425 password_encoded, new_hash):
428 426 cur_user = User.get_by_username(username)
429 427 cur_user.password = new_hash
430 428 Session().add(cur_user)
431 429 Session().flush()
432 430 log.info('Migrated user %s hash to bcrypt', cur_user)
433 431
434 432 def _validate_auth_return(self, ret):
435 433 if not isinstance(ret, dict):
436 434 raise Exception('returned value from auth must be a dict')
437 435 for k in self.auth_func_attrs:
438 436 if k not in ret:
439 437 raise Exception('Missing %s attribute from returned data' % k)
440 438 return ret
441 439
442 440 def get_ttl_cache(self, settings=None):
443 441 plugin_settings = settings or self.get_settings()
444 442 cache_ttl = 0
445 443
446 444 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
447 445 # plugin cache set inside is more important than the settings value
448 446 cache_ttl = self.AUTH_CACHE_TTL
449 447 elif plugin_settings.get('cache_ttl'):
450 448 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
451 449
452 450 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
453 451 return plugin_cache_active, cache_ttl
454 452
455 453
456 454 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
457 455
458 456 @hybrid_property
459 457 def allows_creating_users(self):
460 458 return True
461 459
462 460 def use_fake_password(self):
463 461 """
464 462 Return a boolean that indicates whether or not we should set the user's
465 463 password to a random value when it is authenticated by this plugin.
466 464 If your plugin provides authentication, then you will generally
467 465 want this.
468 466
469 467 :returns: boolean
470 468 """
471 469 raise NotImplementedError("Not implemented in base class")
472 470
473 471 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
474 472 # at this point _authenticate calls plugin's `auth()` function
475 473 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
476 474 userobj, username, passwd, settings, **kwargs)
477 475
478 476 if auth:
479 477 # maybe plugin will clean the username ?
480 478 # we should use the return value
481 479 username = auth['username']
482 480
483 481 # if external source tells us that user is not active, we should
484 482 # skip rest of the process. This can prevent from creating users in
485 483 # RhodeCode when using external authentication, but if it's
486 484 # inactive user we shouldn't create that user anyway
487 485 if auth['active_from_extern'] is False:
488 486 log.warning(
489 487 "User %s authenticated against %s, but is inactive",
490 488 username, self.__module__)
491 489 return None
492 490
493 491 cur_user = User.get_by_username(username, case_insensitive=True)
494 492 is_user_existing = cur_user is not None
495 493
496 494 if is_user_existing:
497 495 log.debug('Syncing user `%s` from '
498 496 '`%s` plugin', username, self.name)
499 497 else:
500 498 log.debug('Creating non existing user `%s` from '
501 499 '`%s` plugin', username, self.name)
502 500
503 501 if self.allows_creating_users:
504 502 log.debug('Plugin `%s` allows to '
505 503 'create new users', self.name)
506 504 else:
507 505 log.debug('Plugin `%s` does not allow to '
508 506 'create new users', self.name)
509 507
510 508 user_parameters = {
511 509 'username': username,
512 510 'email': auth["email"],
513 511 'firstname': auth["firstname"],
514 512 'lastname': auth["lastname"],
515 513 'active': auth["active"],
516 514 'admin': auth["admin"],
517 515 'extern_name': auth["extern_name"],
518 516 'extern_type': self.name,
519 517 'plugin': self,
520 518 'allow_to_create_user': self.allows_creating_users,
521 519 }
522 520
523 521 if not is_user_existing:
524 522 if self.use_fake_password():
525 523 # Randomize the PW because we don't need it, but don't want
526 524 # them blank either
527 525 passwd = PasswordGenerator().gen_password(length=16)
528 526 user_parameters['password'] = passwd
529 527 else:
530 528 # Since the password is required by create_or_update method of
531 529 # UserModel, we need to set it explicitly.
532 530 # The create_or_update method is smart and recognises the
533 531 # password hashes as well.
534 532 user_parameters['password'] = cur_user.password
535 533
536 534 # we either create or update users, we also pass the flag
537 535 # that controls if this method can actually do that.
538 536 # raises NotAllowedToCreateUserError if it cannot, and we try to.
539 537 user = UserModel().create_or_update(**user_parameters)
540 538 Session().flush()
541 539 # enforce user is just in given groups, all of them has to be ones
542 540 # created from plugins. We store this info in _group_data JSON
543 541 # field
544 542
545 543 if auth['user_group_sync']:
546 544 try:
547 545 groups = auth['groups'] or []
548 546 log.debug(
549 547 'Performing user_group sync based on set `%s` '
550 548 'returned by `%s` plugin', groups, self.name)
551 549 UserGroupModel().enforce_groups(user, groups, self.name)
552 550 except Exception:
553 551 # for any reason group syncing fails, we should
554 552 # proceed with login
555 553 log.error(traceback.format_exc())
556 554
557 555 Session().commit()
558 556 return auth
559 557
560 558
561 559 def loadplugin(plugin_id):
562 560 """
563 561 Loads and returns an instantiated authentication plugin.
564 562 Returns the RhodeCodeAuthPluginBase subclass on success,
565 563 or None on failure.
566 564 """
567 565 # TODO: Disusing pyramids thread locals to retrieve the registry.
568 566 authn_registry = get_authn_registry()
569 567 plugin = authn_registry.get_plugin(plugin_id)
570 568 if plugin is None:
571 569 log.error('Authentication plugin not found: "%s"', plugin_id)
572 570 return plugin
573 571
574 572
575 573 def get_authn_registry(registry=None):
576 574 registry = registry or get_current_registry()
577 575 authn_registry = registry.getUtility(IAuthnPluginRegistry)
578 576 return authn_registry
579 577
580 578
581 579 def get_auth_cache_manager(custom_ttl=None, suffix=None):
582 580 cache_name = 'rhodecode.authentication'
583 581 if suffix:
584 582 cache_name = 'rhodecode.authentication.{}'.format(suffix)
585 583 return caches.get_cache_manager(
586 584 'auth_plugins', cache_name, custom_ttl)
587 585
588 586
589 587 def get_perms_cache_manager(custom_ttl=None, suffix=None):
590 588 cache_name = 'rhodecode.permissions'
591 589 if suffix:
592 590 cache_name = 'rhodecode.permissions.{}'.format(suffix)
593 591 return caches.get_cache_manager(
594 592 'auth_plugins', cache_name, custom_ttl)
595 593
596 594
597 595 def authenticate(username, password, environ=None, auth_type=None,
598 596 skip_missing=False, registry=None, acl_repo_name=None):
599 597 """
600 598 Authentication function used for access control,
601 599 It tries to authenticate based on enabled authentication modules.
602 600
603 601 :param username: username can be empty for headers auth
604 602 :param password: password can be empty for headers auth
605 603 :param environ: environ headers passed for headers auth
606 604 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
607 605 :param skip_missing: ignores plugins that are in db but not in environment
608 606 :returns: None if auth failed, plugin_user dict if auth is correct
609 607 """
610 608 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
611 609 raise ValueError('auth type must be on of http, vcs got "%s" instead'
612 610 % auth_type)
613 611 headers_only = environ and not (username and password)
614 612
615 613 authn_registry = get_authn_registry(registry)
616 614 plugins_to_check = authn_registry.get_plugins_for_authentication()
617 615 log.debug('Starting ordered authentication chain using %s plugins',
618 616 plugins_to_check)
619 617 for plugin in plugins_to_check:
620 618 plugin.set_auth_type(auth_type)
621 619 plugin.set_calling_scope_repo(acl_repo_name)
622 620
623 621 if headers_only and not plugin.is_headers_auth:
624 622 log.debug('Auth type is for headers only and plugin `%s` is not '
625 623 'headers plugin, skipping...', plugin.get_id())
626 624 continue
627 625
628 626 log.debug('Trying authentication using ** %s **', plugin.get_id())
629 627
630 628 # load plugin settings from RhodeCode database
631 629 plugin_settings = plugin.get_settings()
632 630 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
633 631 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
634 632
635 633 # use plugin's method of user extraction.
636 634 user = plugin.get_user(username, environ=environ,
637 635 settings=plugin_settings)
638 636 display_user = user.username if user else username
639 637 log.debug(
640 638 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
641 639
642 640 if not plugin.allows_authentication_from(user):
643 641 log.debug('Plugin %s does not accept user `%s` for authentication',
644 642 plugin.get_id(), display_user)
645 643 continue
646 644 else:
647 645 log.debug('Plugin %s accepted user `%s` for authentication',
648 646 plugin.get_id(), display_user)
649 647
650 648 log.info('Authenticating user `%s` using %s plugin',
651 649 display_user, plugin.get_id())
652 650
653 651 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
654 652
655 653 # get instance of cache manager configured for a namespace
656 654 cache_manager = get_auth_cache_manager(
657 655 custom_ttl=cache_ttl, suffix=user.user_id if user else '')
658 656
659 657 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
660 658 plugin.get_id(), plugin_cache_active, cache_ttl)
661 659
662 660 # for environ based password can be empty, but then the validation is
663 661 # on the server that fills in the env data needed for authentication
664 662
665 663 _password_hash = caches.compute_key_from_params(
666 664 plugin.name, username, (password or ''))
667 665
668 666 # _authenticate is a wrapper for .auth() method of plugin.
669 667 # it checks if .auth() sends proper data.
670 668 # For RhodeCodeExternalAuthPlugin it also maps users to
671 669 # Database and maps the attributes returned from .auth()
672 670 # to RhodeCode database. If this function returns data
673 671 # then auth is correct.
674 672 start = time.time()
675 673 log.debug('Running plugin `%s` _authenticate method', plugin.get_id())
676 674
677 675 def auth_func():
678 676 """
679 677 This function is used internally in Cache of Beaker to calculate
680 678 Results
681 679 """
682 680 log.debug('auth: calculating password access now...')
683 681 return plugin._authenticate(
684 682 user, username, password, plugin_settings,
685 683 environ=environ or {})
686 684
687 685 if plugin_cache_active:
688 log.debug('Trying to fetch cached auth by `...%s`', _password_hash[:6])
686 log.debug('Trying to fetch cached auth by pwd hash `...%s`',
687 _password_hash[:6])
689 688 plugin_user = cache_manager.get(
690 689 _password_hash, createfunc=auth_func)
691 690 else:
692 691 plugin_user = auth_func()
693 692
694 693 auth_time = time.time() - start
695 694 log.debug('Authentication for plugin `%s` completed in %.3fs, '
696 695 'expiration time of fetched cache %.1fs.',
697 696 plugin.get_id(), auth_time, cache_ttl)
698 697
699 698 log.debug('PLUGIN USER DATA: %s', plugin_user)
700 699
701 700 if plugin_user:
702 701 log.debug('Plugin returned proper authentication data')
703 702 return plugin_user
704 703 # we failed to Auth because .auth() method didn't return proper user
705 704 log.debug("User `%s` failed to authenticate against %s",
706 705 display_user, plugin.get_id())
707 706
708 707 # case when we failed to authenticate against all defined plugins
709 708 return None
710 709
711 710
712 711 def chop_at(s, sub, inclusive=False):
713 712 """Truncate string ``s`` at the first occurrence of ``sub``.
714 713
715 714 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
716 715
717 716 >>> chop_at("plutocratic brats", "rat")
718 717 'plutoc'
719 718 >>> chop_at("plutocratic brats", "rat", True)
720 719 'plutocrat'
721 720 """
722 721 pos = s.find(sub)
723 722 if pos == -1:
724 723 return s
725 724 if inclusive:
726 725 return s[:pos+len(sub)]
727 726 return s[:pos]
@@ -1,87 +1,89 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 import logging
22 22
23 23 from pyramid.exceptions import ConfigurationError
24 24 from zope.interface import implementer
25 25
26 26 from rhodecode.authentication.interface import IAuthnPluginRegistry
27 27 from rhodecode.lib.utils2 import safe_str
28 28 from rhodecode.model.settings import SettingsModel
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 @implementer(IAuthnPluginRegistry)
34 34 class AuthenticationPluginRegistry(object):
35 35
36 36 # INI settings key to set a fallback authentication plugin.
37 37 fallback_plugin_key = 'rhodecode.auth_plugin_fallback'
38 38
39 39 def __init__(self, settings):
40 40 self._plugins = {}
41 41 self._fallback_plugin = settings.get(self.fallback_plugin_key, None)
42 42
43 43 def add_authn_plugin(self, config, plugin):
44 44 plugin_id = plugin.get_id()
45 45 if plugin_id in self._plugins.keys():
46 46 raise ConfigurationError(
47 47 'Cannot register authentication plugin twice: "%s"', plugin_id)
48 48 else:
49 49 log.debug('Register authentication plugin: "%s"', plugin_id)
50 50 self._plugins[plugin_id] = plugin
51 51
52 52 def get_plugins(self):
53 53 def sort_key(plugin):
54 54 return str.lower(safe_str(plugin.get_display_name()))
55 55
56 56 return sorted(self._plugins.values(), key=sort_key)
57 57
58 58 def get_plugin(self, plugin_id):
59 59 return self._plugins.get(plugin_id, None)
60 60
61 61 def get_plugins_for_authentication(self):
62 62 """
63 63 Returns a list of plugins which should be consulted when authenticating
64 64 a user. It only returns plugins which are enabled and active.
65 65 Additionally it includes the fallback plugin from the INI file, if
66 66 `rhodecode.auth_plugin_fallback` is set to a plugin ID.
67 67 """
68 68 plugins = []
69 69
70 70 # Add all enabled and active plugins to the list. We iterate over the
71 71 # auth_plugins setting from DB because it also represents the ordering.
72 72 enabled_plugins = SettingsModel().get_auth_plugins()
73 raw_settings = SettingsModel().get_all_settings()
73 74 for plugin_id in enabled_plugins:
74 75 plugin = self.get_plugin(plugin_id)
75 if plugin is not None and plugin.is_active():
76 if plugin is not None and plugin.is_active(
77 plugin_cached_settings=raw_settings):
76 78 plugins.append(plugin)
77 79
78 80 # Add the fallback plugin from ini file.
79 81 if self._fallback_plugin:
80 82 log.warn(
81 83 'Using fallback authentication plugin from INI file: "%s"',
82 84 self._fallback_plugin)
83 85 plugin = self.get_plugin(self._fallback_plugin)
84 86 if plugin is not None and plugin not in plugins:
85 87 plugins.append(plugin)
86 88
87 89 return plugins
@@ -1,191 +1,191 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 import colander
22 22 import formencode.htmlfill
23 23 import logging
24 24
25 25 from pyramid.httpexceptions import HTTPFound
26 26 from pyramid.renderers import render
27 27 from pyramid.response import Response
28 28
29 29 from rhodecode.apps._base import BaseAppView
30 30 from rhodecode.authentication.base import (
31 31 get_auth_cache_manager, get_perms_cache_manager, get_authn_registry)
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
35 35 from rhodecode.lib.caches import clear_cache_manager
36 36 from rhodecode.model.forms import AuthSettingsForm
37 37 from rhodecode.model.meta import Session
38 38 from rhodecode.model.settings import SettingsModel
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class AuthnPluginViewBase(BaseAppView):
44 44
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context()
47 47 self.plugin = self.context.plugin
48 48 return c
49 49
50 50 @LoginRequired()
51 51 @HasPermissionAllDecorator('hg.admin')
52 52 def settings_get(self, defaults=None, errors=None):
53 53 """
54 54 View that displays the plugin settings as a form.
55 55 """
56 56 c = self.load_default_context()
57 57 defaults = defaults or {}
58 58 errors = errors or {}
59 59 schema = self.plugin.get_settings_schema()
60 60
61 61 # Compute default values for the form. Priority is:
62 62 # 1. Passed to this method 2. DB value 3. Schema default
63 63 for node in schema:
64 64 if node.name not in defaults:
65 65 defaults[node.name] = self.plugin.get_setting_by_name(
66 node.name, node.default, cache=False)
66 node.name, node.default)
67 67
68 68 template_context = {
69 69 'defaults': defaults,
70 70 'errors': errors,
71 71 'plugin': self.context.plugin,
72 72 'resource': self.context,
73 73 }
74 74
75 75 return self._get_template_context(c, **template_context)
76 76
77 77 @LoginRequired()
78 78 @HasPermissionAllDecorator('hg.admin')
79 79 @CSRFRequired()
80 80 def settings_post(self):
81 81 """
82 82 View that validates and stores the plugin settings.
83 83 """
84 84 _ = self.request.translate
85 85 self.load_default_context()
86 86 schema = self.plugin.get_settings_schema()
87 87 data = self.request.params
88 88
89 89 try:
90 90 valid_data = schema.deserialize(data)
91 91 except colander.Invalid as e:
92 92 # Display error message and display form again.
93 93 h.flash(
94 94 _('Errors exist when saving plugin settings. '
95 95 'Please check the form inputs.'),
96 96 category='error')
97 97 defaults = {key: data[key] for key in data if key in schema}
98 98 return self.settings_get(errors=e.asdict(), defaults=defaults)
99 99
100 100 # Store validated data.
101 101 for name, value in valid_data.items():
102 102 self.plugin.create_or_update_setting(name, value)
103 103 Session().commit()
104 104
105 105 # cleanup cache managers in case of change for plugin
106 106 # TODO(marcink): because we can register multiple namespaces
107 107 # we should at some point figure out how to retrieve ALL namespace
108 108 # cache managers and clear them...
109 109 cache_manager = get_auth_cache_manager()
110 110 clear_cache_manager(cache_manager)
111 111
112 112 cache_manager = get_perms_cache_manager()
113 113 clear_cache_manager(cache_manager)
114 114
115 115 # Display success message and redirect.
116 116 h.flash(_('Auth settings updated successfully.'), category='success')
117 117 redirect_to = self.request.resource_path(
118 118 self.context, route_name='auth_home')
119 119 return HTTPFound(redirect_to)
120 120
121 121
122 122 class AuthSettingsView(BaseAppView):
123 123 def load_default_context(self):
124 124 c = self._get_local_tmpl_context()
125 125 return c
126 126
127 127 @LoginRequired()
128 128 @HasPermissionAllDecorator('hg.admin')
129 129 def index(self, defaults=None, errors=None, prefix_error=False):
130 130 c = self.load_default_context()
131 131
132 132 defaults = defaults or {}
133 133 authn_registry = get_authn_registry(self.request.registry)
134 134 enabled_plugins = SettingsModel().get_auth_plugins()
135 135
136 136 # Create template context and render it.
137 137 template_context = {
138 138 'resource': self.context,
139 139 'available_plugins': authn_registry.get_plugins(),
140 140 'enabled_plugins': enabled_plugins,
141 141 }
142 142 html = render('rhodecode:templates/admin/auth/auth_settings.mako',
143 143 self._get_template_context(c, **template_context),
144 144 self.request)
145 145
146 146 # Create form default values and fill the form.
147 147 form_defaults = {
148 148 'auth_plugins': ',\n'.join(enabled_plugins)
149 149 }
150 150 form_defaults.update(defaults)
151 151 html = formencode.htmlfill.render(
152 152 html,
153 153 defaults=form_defaults,
154 154 errors=errors,
155 155 prefix_error=prefix_error,
156 156 encoding="UTF-8",
157 157 force_defaults=False)
158 158
159 159 return Response(html)
160 160
161 161 @LoginRequired()
162 162 @HasPermissionAllDecorator('hg.admin')
163 163 @CSRFRequired()
164 164 def auth_settings(self):
165 165 _ = self.request.translate
166 166 try:
167 167 form = AuthSettingsForm(self.request.translate)()
168 168 form_result = form.to_python(self.request.POST)
169 169 plugins = ','.join(form_result['auth_plugins'])
170 170 setting = SettingsModel().create_or_update_setting(
171 171 'auth_plugins', plugins)
172 172 Session().add(setting)
173 173 Session().commit()
174 174
175 175 h.flash(_('Auth settings updated successfully.'), category='success')
176 176 except formencode.Invalid as errors:
177 177 e = errors.error_dict or {}
178 178 h.flash(_('Errors exist when saving plugin setting. '
179 179 'Please check the form inputs.'), category='error')
180 180 return self.index(
181 181 defaults=errors.value,
182 182 errors=e,
183 183 prefix_error=False)
184 184 except Exception:
185 185 log.exception('Exception in auth_settings')
186 186 h.flash(_('Error occurred during update of auth settings.'),
187 187 category='error')
188 188
189 189 redirect_to = self.request.resource_path(
190 190 self.context, route_name='auth_home')
191 191 return HTTPFound(redirect_to)
General Comments 0
You need to be logged in to leave comments. Login now