##// END OF EJS Templates
authn: Don't use setting value to compute the setting type.
johbo -
r232:299ebd92 default
parent child Browse files
Show More
@@ -1,612 +1,615 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 import colander
25 26 import logging
26 27 import time
27 28 import traceback
28 29 import warnings
29 30
30 31 from pyramid.threadlocal import get_current_registry
31 32 from sqlalchemy.ext.hybrid import hybrid_property
32 33
33 34 from rhodecode.authentication.interface import IAuthnPluginRegistry
34 35 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 36 from rhodecode.lib import caches
36 37 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
37 38 from rhodecode.lib.utils2 import md5_safe, safe_int
38 39 from rhodecode.lib.utils2 import safe_str
39 40 from rhodecode.model.db import User
40 41 from rhodecode.model.meta import Session
41 42 from rhodecode.model.settings import SettingsModel
42 43 from rhodecode.model.user import UserModel
43 44 from rhodecode.model.user_group import UserGroupModel
44 45
45 46
46 47 log = logging.getLogger(__name__)
47 48
48 49 # auth types that authenticate() function can receive
49 50 VCS_TYPE = 'vcs'
50 51 HTTP_TYPE = 'http'
51 52
52 53
53 54 class LazyFormencode(object):
54 55 def __init__(self, formencode_obj, *args, **kwargs):
55 56 self.formencode_obj = formencode_obj
56 57 self.args = args
57 58 self.kwargs = kwargs
58 59
59 60 def __call__(self, *args, **kwargs):
60 61 from inspect import isfunction
61 62 formencode_obj = self.formencode_obj
62 63 if isfunction(formencode_obj):
63 64 # case we wrap validators into functions
64 65 formencode_obj = self.formencode_obj(*args, **kwargs)
65 66 return formencode_obj(*self.args, **self.kwargs)
66 67
67 68
68 69 class RhodeCodeAuthPluginBase(object):
69 70 # cache the authentication request for N amount of seconds. Some kind
70 71 # of authentication methods are very heavy and it's very efficient to cache
71 72 # the result of a call. If it's set to None (default) cache is off
72 73 AUTH_CACHE_TTL = None
73 74 AUTH_CACHE = {}
74 75
75 76 auth_func_attrs = {
76 77 "username": "unique username",
77 78 "firstname": "first name",
78 79 "lastname": "last name",
79 80 "email": "email address",
80 81 "groups": '["list", "of", "groups"]',
81 82 "extern_name": "name in external source of record",
82 83 "extern_type": "type of external source of record",
83 84 "admin": 'True|False defines if user should be RhodeCode super admin',
84 85 "active":
85 86 'True|False defines active state of user internally for RhodeCode',
86 87 "active_from_extern":
87 88 "True|False\None, active state from the external auth, "
88 89 "None means use definition from RhodeCode extern_type active value"
89 90 }
90 91 # set on authenticate() method and via set_auth_type func.
91 92 auth_type = None
92 93
93 94 # List of setting names to store encrypted. Plugins may override this list
94 95 # to store settings encrypted.
95 96 _settings_encrypted = []
96 97
97 98 # Mapping of python to DB settings model types. Plugins may override or
98 99 # extend this mapping.
99 100 _settings_type_map = {
100 str: 'str',
101 int: 'int',
102 unicode: 'unicode',
103 bool: 'bool',
104 list: 'list',
101 colander.String: 'unicode',
102 colander.Integer: 'int',
103 colander.Boolean: 'bool',
104 colander.List: 'list',
105 105 }
106 106
107 107 def __init__(self, plugin_id):
108 108 self._plugin_id = plugin_id
109 109
110 110 def __str__(self):
111 111 return self.get_id()
112 112
113 113 def _get_setting_full_name(self, name):
114 114 """
115 115 Return the full setting name used for storing values in the database.
116 116 """
117 117 # TODO: johbo: Using the name here is problematic. It would be good to
118 118 # introduce either new models in the database to hold Plugin and
119 119 # PluginSetting or to use the plugin id here.
120 120 return 'auth_{}_{}'.format(self.name, name)
121 121
122 def _get_setting_type(self, name, value):
122 def _get_setting_type(self, name):
123 """
124 Return the type of a setting. This type is defined by the SettingsModel
125 and determines how the setting is stored in DB. Optionally the suffix
126 `.encrypted` is appended to instruct SettingsModel to store it
127 encrypted.
123 128 """
124 Get the type as used by the SettingsModel accordingly to type of passed
125 value. Optionally the suffix `.encrypted` is appended to instruct
126 SettingsModel to store it encrypted.
127 """
128 type_ = self._settings_type_map.get(type(value), 'unicode')
129 schema_node = self.get_settings_schema().get(name)
130 db_type = self._settings_type_map.get(
131 schema_node.typ.__class__, 'unicode')
129 132 if name in self._settings_encrypted:
130 type_ = '{}.encrypted'.format(type_)
131 return type_
133 db_type = '{}.encrypted'.format(db_type)
134 return db_type
132 135
133 136 def is_enabled(self):
134 137 """
135 138 Returns true if this plugin is enabled. An enabled plugin can be
136 139 configured in the admin interface but it is not consulted during
137 140 authentication.
138 141 """
139 142 auth_plugins = SettingsModel().get_auth_plugins()
140 143 return self.get_id() in auth_plugins
141 144
142 145 def is_active(self):
143 146 """
144 147 Returns true if the plugin is activated. An activated plugin is
145 148 consulted during authentication, assumed it is also enabled.
146 149 """
147 150 return self.get_setting_by_name('enabled')
148 151
149 152 def get_id(self):
150 153 """
151 154 Returns the plugin id.
152 155 """
153 156 return self._plugin_id
154 157
155 158 def get_display_name(self):
156 159 """
157 160 Returns a translation string for displaying purposes.
158 161 """
159 162 raise NotImplementedError('Not implemented in base class')
160 163
161 164 def get_settings_schema(self):
162 165 """
163 166 Returns a colander schema, representing the plugin settings.
164 167 """
165 168 return AuthnPluginSettingsSchemaBase()
166 169
167 170 def get_setting_by_name(self, name):
168 171 """
169 172 Returns a plugin setting by name.
170 173 """
171 174 full_name = self._get_setting_full_name(name)
172 175 db_setting = SettingsModel().get_setting_by_name(full_name)
173 176 return db_setting.app_settings_value if db_setting else None
174 177
175 178 def create_or_update_setting(self, name, value):
176 179 """
177 180 Create or update a setting for this plugin in the persistent storage.
178 181 """
179 182 full_name = self._get_setting_full_name(name)
180 type_ = self._get_setting_type(name, value)
183 type_ = self._get_setting_type(name)
181 184 db_setting = SettingsModel().create_or_update_setting(
182 185 full_name, value, type_)
183 186 return db_setting.app_settings_value
184 187
185 188 def get_settings(self):
186 189 """
187 190 Returns the plugin settings as dictionary.
188 191 """
189 192 settings = {}
190 193 for node in self.get_settings_schema():
191 194 settings[node.name] = self.get_setting_by_name(node.name)
192 195 return settings
193 196
194 197 @property
195 198 def validators(self):
196 199 """
197 200 Exposes RhodeCode validators modules
198 201 """
199 202 # this is a hack to overcome issues with pylons threadlocals and
200 203 # translator object _() not beein registered properly.
201 204 class LazyCaller(object):
202 205 def __init__(self, name):
203 206 self.validator_name = name
204 207
205 208 def __call__(self, *args, **kwargs):
206 209 from rhodecode.model import validators as v
207 210 obj = getattr(v, self.validator_name)
208 211 # log.debug('Initializing lazy formencode object: %s', obj)
209 212 return LazyFormencode(obj, *args, **kwargs)
210 213
211 214 class ProxyGet(object):
212 215 def __getattribute__(self, name):
213 216 return LazyCaller(name)
214 217
215 218 return ProxyGet()
216 219
217 220 @hybrid_property
218 221 def name(self):
219 222 """
220 223 Returns the name of this authentication plugin.
221 224
222 225 :returns: string
223 226 """
224 227 raise NotImplementedError("Not implemented in base class")
225 228
226 229 @property
227 230 def is_headers_auth(self):
228 231 """
229 232 Returns True if this authentication plugin uses HTTP headers as
230 233 authentication method.
231 234 """
232 235 return False
233 236
234 237 @hybrid_property
235 238 def is_container_auth(self):
236 239 """
237 240 Deprecated method that indicates if this authentication plugin uses
238 241 HTTP headers as authentication method.
239 242 """
240 243 warnings.warn(
241 244 'Use is_headers_auth instead.', category=DeprecationWarning)
242 245 return self.is_headers_auth
243 246
244 247 @hybrid_property
245 248 def allows_creating_users(self):
246 249 """
247 250 Defines if Plugin allows users to be created on-the-fly when
248 251 authentication is called. Controls how external plugins should behave
249 252 in terms if they are allowed to create new users, or not. Base plugins
250 253 should not be allowed to, but External ones should be !
251 254
252 255 :return: bool
253 256 """
254 257 return False
255 258
256 259 def set_auth_type(self, auth_type):
257 260 self.auth_type = auth_type
258 261
259 262 def allows_authentication_from(
260 263 self, user, allows_non_existing_user=True,
261 264 allowed_auth_plugins=None, allowed_auth_sources=None):
262 265 """
263 266 Checks if this authentication module should accept a request for
264 267 the current user.
265 268
266 269 :param user: user object fetched using plugin's get_user() method.
267 270 :param allows_non_existing_user: if True, don't allow the
268 271 user to be empty, meaning not existing in our database
269 272 :param allowed_auth_plugins: if provided, users extern_type will be
270 273 checked against a list of provided extern types, which are plugin
271 274 auth_names in the end
272 275 :param allowed_auth_sources: authentication type allowed,
273 276 `http` or `vcs` default is both.
274 277 defines if plugin will accept only http authentication vcs
275 278 authentication(git/hg) or both
276 279 :returns: boolean
277 280 """
278 281 if not user and not allows_non_existing_user:
279 282 log.debug('User is empty but plugin does not allow empty users,'
280 283 'not allowed to authenticate')
281 284 return False
282 285
283 286 expected_auth_plugins = allowed_auth_plugins or [self.name]
284 287 if user and (user.extern_type and
285 288 user.extern_type not in expected_auth_plugins):
286 289 log.debug(
287 290 'User `%s` is bound to `%s` auth type. Plugin allows only '
288 291 '%s, skipping', user, user.extern_type, expected_auth_plugins)
289 292
290 293 return False
291 294
292 295 # by default accept both
293 296 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
294 297 if self.auth_type not in expected_auth_from:
295 298 log.debug('Current auth source is %s but plugin only allows %s',
296 299 self.auth_type, expected_auth_from)
297 300 return False
298 301
299 302 return True
300 303
301 304 def get_user(self, username=None, **kwargs):
302 305 """
303 306 Helper method for user fetching in plugins, by default it's using
304 307 simple fetch by username, but this method can be custimized in plugins
305 308 eg. headers auth plugin to fetch user by environ params
306 309
307 310 :param username: username if given to fetch from database
308 311 :param kwargs: extra arguments needed for user fetching.
309 312 """
310 313 user = None
311 314 log.debug(
312 315 'Trying to fetch user `%s` from RhodeCode database', username)
313 316 if username:
314 317 user = User.get_by_username(username)
315 318 if not user:
316 319 log.debug('User not found, fallback to fetch user in '
317 320 'case insensitive mode')
318 321 user = User.get_by_username(username, case_insensitive=True)
319 322 else:
320 323 log.debug('provided username:`%s` is empty skipping...', username)
321 324 if not user:
322 325 log.debug('User `%s` not found in database', username)
323 326 return user
324 327
325 328 def user_activation_state(self):
326 329 """
327 330 Defines user activation state when creating new users
328 331
329 332 :returns: boolean
330 333 """
331 334 raise NotImplementedError("Not implemented in base class")
332 335
333 336 def auth(self, userobj, username, passwd, settings, **kwargs):
334 337 """
335 338 Given a user object (which may be null), username, a plaintext
336 339 password, and a settings object (containing all the keys needed as
337 340 listed in settings()), authenticate this user's login attempt.
338 341
339 342 Return None on failure. On success, return a dictionary of the form:
340 343
341 344 see: RhodeCodeAuthPluginBase.auth_func_attrs
342 345 This is later validated for correctness
343 346 """
344 347 raise NotImplementedError("not implemented in base class")
345 348
346 349 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
347 350 """
348 351 Wrapper to call self.auth() that validates call on it
349 352
350 353 :param userobj: userobj
351 354 :param username: username
352 355 :param passwd: plaintext password
353 356 :param settings: plugin settings
354 357 """
355 358 auth = self.auth(userobj, username, passwd, settings, **kwargs)
356 359 if auth:
357 360 # check if hash should be migrated ?
358 361 new_hash = auth.get('_hash_migrate')
359 362 if new_hash:
360 363 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
361 364 return self._validate_auth_return(auth)
362 365 return auth
363 366
364 367 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
365 368 new_hash_cypher = _RhodeCodeCryptoBCrypt()
366 369 # extra checks, so make sure new hash is correct.
367 370 password_encoded = safe_str(password)
368 371 if new_hash and new_hash_cypher.hash_check(
369 372 password_encoded, new_hash):
370 373 cur_user = User.get_by_username(username)
371 374 cur_user.password = new_hash
372 375 Session().add(cur_user)
373 376 Session().flush()
374 377 log.info('Migrated user %s hash to bcrypt', cur_user)
375 378
376 379 def _validate_auth_return(self, ret):
377 380 if not isinstance(ret, dict):
378 381 raise Exception('returned value from auth must be a dict')
379 382 for k in self.auth_func_attrs:
380 383 if k not in ret:
381 384 raise Exception('Missing %s attribute from returned data' % k)
382 385 return ret
383 386
384 387
385 388 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
386 389
387 390 @hybrid_property
388 391 def allows_creating_users(self):
389 392 return True
390 393
391 394 def use_fake_password(self):
392 395 """
393 396 Return a boolean that indicates whether or not we should set the user's
394 397 password to a random value when it is authenticated by this plugin.
395 398 If your plugin provides authentication, then you will generally
396 399 want this.
397 400
398 401 :returns: boolean
399 402 """
400 403 raise NotImplementedError("Not implemented in base class")
401 404
402 405 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
403 406 # at this point _authenticate calls plugin's `auth()` function
404 407 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
405 408 userobj, username, passwd, settings, **kwargs)
406 409 if auth:
407 410 # maybe plugin will clean the username ?
408 411 # we should use the return value
409 412 username = auth['username']
410 413
411 414 # if external source tells us that user is not active, we should
412 415 # skip rest of the process. This can prevent from creating users in
413 416 # RhodeCode when using external authentication, but if it's
414 417 # inactive user we shouldn't create that user anyway
415 418 if auth['active_from_extern'] is False:
416 419 log.warning(
417 420 "User %s authenticated against %s, but is inactive",
418 421 username, self.__module__)
419 422 return None
420 423
421 424 cur_user = User.get_by_username(username, case_insensitive=True)
422 425 is_user_existing = cur_user is not None
423 426
424 427 if is_user_existing:
425 428 log.debug('Syncing user `%s` from '
426 429 '`%s` plugin', username, self.name)
427 430 else:
428 431 log.debug('Creating non existing user `%s` from '
429 432 '`%s` plugin', username, self.name)
430 433
431 434 if self.allows_creating_users:
432 435 log.debug('Plugin `%s` allows to '
433 436 'create new users', self.name)
434 437 else:
435 438 log.debug('Plugin `%s` does not allow to '
436 439 'create new users', self.name)
437 440
438 441 user_parameters = {
439 442 'username': username,
440 443 'email': auth["email"],
441 444 'firstname': auth["firstname"],
442 445 'lastname': auth["lastname"],
443 446 'active': auth["active"],
444 447 'admin': auth["admin"],
445 448 'extern_name': auth["extern_name"],
446 449 'extern_type': self.name,
447 450 'plugin': self,
448 451 'allow_to_create_user': self.allows_creating_users,
449 452 }
450 453
451 454 if not is_user_existing:
452 455 if self.use_fake_password():
453 456 # Randomize the PW because we don't need it, but don't want
454 457 # them blank either
455 458 passwd = PasswordGenerator().gen_password(length=16)
456 459 user_parameters['password'] = passwd
457 460 else:
458 461 # Since the password is required by create_or_update method of
459 462 # UserModel, we need to set it explicitly.
460 463 # The create_or_update method is smart and recognises the
461 464 # password hashes as well.
462 465 user_parameters['password'] = cur_user.password
463 466
464 467 # we either create or update users, we also pass the flag
465 468 # that controls if this method can actually do that.
466 469 # raises NotAllowedToCreateUserError if it cannot, and we try to.
467 470 user = UserModel().create_or_update(**user_parameters)
468 471 Session().flush()
469 472 # enforce user is just in given groups, all of them has to be ones
470 473 # created from plugins. We store this info in _group_data JSON
471 474 # field
472 475 try:
473 476 groups = auth['groups'] or []
474 477 UserGroupModel().enforce_groups(user, groups, self.name)
475 478 except Exception:
476 479 # for any reason group syncing fails, we should
477 480 # proceed with login
478 481 log.error(traceback.format_exc())
479 482 Session().commit()
480 483 return auth
481 484
482 485
483 486 def loadplugin(plugin_id):
484 487 """
485 488 Loads and returns an instantiated authentication plugin.
486 489 Returns the RhodeCodeAuthPluginBase subclass on success,
487 490 or None on failure.
488 491 """
489 492 # TODO: Disusing pyramids thread locals to retrieve the registry.
490 493 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
491 494 plugin = authn_registry.get_plugin(plugin_id)
492 495 if plugin is None:
493 496 log.error('Authentication plugin not found: "%s"', plugin_id)
494 497 return plugin
495 498
496 499
497 500 def get_auth_cache_manager(custom_ttl=None):
498 501 return caches.get_cache_manager(
499 502 'auth_plugins', 'rhodecode.authentication', custom_ttl)
500 503
501 504
502 505 def authenticate(username, password, environ=None, auth_type=None,
503 506 skip_missing=False):
504 507 """
505 508 Authentication function used for access control,
506 509 It tries to authenticate based on enabled authentication modules.
507 510
508 511 :param username: username can be empty for headers auth
509 512 :param password: password can be empty for headers auth
510 513 :param environ: environ headers passed for headers auth
511 514 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
512 515 :param skip_missing: ignores plugins that are in db but not in environment
513 516 :returns: None if auth failed, plugin_user dict if auth is correct
514 517 """
515 518 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
516 519 raise ValueError('auth type must be on of http, vcs got "%s" instead'
517 520 % auth_type)
518 521 headers_only = environ and not (username and password)
519 522
520 523 authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry)
521 524 for plugin in authn_registry.get_plugins_for_authentication():
522 525 plugin.set_auth_type(auth_type)
523 526 user = plugin.get_user(username)
524 527 display_user = user.username if user else username
525 528
526 529 if headers_only and not plugin.is_headers_auth:
527 530 log.debug('Auth type is for headers only and plugin `%s` is not '
528 531 'headers plugin, skipping...', plugin.get_id())
529 532 continue
530 533
531 534 # load plugin settings from RhodeCode database
532 535 plugin_settings = plugin.get_settings()
533 536 log.debug('Plugin settings:%s', plugin_settings)
534 537
535 538 log.debug('Trying authentication using ** %s **', plugin.get_id())
536 539 # use plugin's method of user extraction.
537 540 user = plugin.get_user(username, environ=environ,
538 541 settings=plugin_settings)
539 542 display_user = user.username if user else username
540 543 log.debug(
541 544 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
542 545
543 546 if not plugin.allows_authentication_from(user):
544 547 log.debug('Plugin %s does not accept user `%s` for authentication',
545 548 plugin.get_id(), display_user)
546 549 continue
547 550 else:
548 551 log.debug('Plugin %s accepted user `%s` for authentication',
549 552 plugin.get_id(), display_user)
550 553
551 554 log.info('Authenticating user `%s` using %s plugin',
552 555 display_user, plugin.get_id())
553 556
554 557 _cache_ttl = 0
555 558
556 559 if isinstance(plugin.AUTH_CACHE_TTL, (int, long)):
557 560 # plugin cache set inside is more important than the settings value
558 561 _cache_ttl = plugin.AUTH_CACHE_TTL
559 562 elif plugin_settings.get('auth_cache_ttl'):
560 563 _cache_ttl = safe_int(plugin_settings.get('auth_cache_ttl'), 0)
561 564
562 565 plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0)
563 566
564 567 # get instance of cache manager configured for a namespace
565 568 cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl)
566 569
567 570 log.debug('Cache for plugin `%s` active: %s', plugin.get_id(),
568 571 plugin_cache_active)
569 572
570 573 # for environ based password can be empty, but then the validation is
571 574 # on the server that fills in the env data needed for authentication
572 575 _password_hash = md5_safe(plugin.name + username + (password or ''))
573 576
574 577 # _authenticate is a wrapper for .auth() method of plugin.
575 578 # it checks if .auth() sends proper data.
576 579 # For RhodeCodeExternalAuthPlugin it also maps users to
577 580 # Database and maps the attributes returned from .auth()
578 581 # to RhodeCode database. If this function returns data
579 582 # then auth is correct.
580 583 start = time.time()
581 584 log.debug('Running plugin `%s` _authenticate method',
582 585 plugin.get_id())
583 586
584 587 def auth_func():
585 588 """
586 589 This function is used internally in Cache of Beaker to calculate
587 590 Results
588 591 """
589 592 return plugin._authenticate(
590 593 user, username, password, plugin_settings,
591 594 environ=environ or {})
592 595
593 596 if plugin_cache_active:
594 597 plugin_user = cache_manager.get(
595 598 _password_hash, createfunc=auth_func)
596 599 else:
597 600 plugin_user = auth_func()
598 601
599 602 auth_time = time.time() - start
600 603 log.debug('Authentication for plugin `%s` completed in %.3fs, '
601 604 'expiration time of fetched cache %.1fs.',
602 605 plugin.get_id(), auth_time, _cache_ttl)
603 606
604 607 log.debug('PLUGIN USER DATA: %s', plugin_user)
605 608
606 609 if plugin_user:
607 610 log.debug('Plugin returned proper authentication data')
608 611 return plugin_user
609 612 # we failed to Auth because .auth() method didn't return proper user
610 613 log.debug("User `%s` failed to authenticate against %s",
611 614 display_user, plugin.get_id())
612 615 return None
General Comments 0
You need to be logged in to leave comments. Login now