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