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