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