##// END OF EJS Templates
caches: enable cache TTL=30s for auth-plugins....
marcink -
r2954:5ef9c564 default
parent child Browse files
Show More
@@ -1,131 +1,132 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import logging
23 23 import importlib
24 24
25 25 from pkg_resources import iter_entry_points
26 26 from pyramid.authentication import SessionAuthenticationPolicy
27 27
28 28 from rhodecode.authentication.registry import AuthenticationPluginRegistry
29 29 from rhodecode.authentication.routes import root_factory
30 30 from rhodecode.authentication.routes import AuthnRootResource
31 31 from rhodecode.apps._base import ADMIN_PREFIX
32 32 from rhodecode.model.settings import SettingsModel
33 33
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37 # Plugin ID prefixes to distinct between normal and legacy plugins.
38 38 plugin_prefix = 'egg:'
39 39 legacy_plugin_prefix = 'py:'
40 plugin_default_auth_ttl = 30
40 41
41 42
42 43 # TODO: Currently this is only used to discover the authentication plugins.
43 44 # Later on this may be used in a generic way to look up and include all kinds
44 45 # of supported enterprise plugins. Therefore this has to be moved and
45 46 # refactored to a real 'plugin look up' machinery.
46 47 # TODO: When refactoring this think about splitting it up into distinct
47 48 # discover, load and include phases.
48 49 def _discover_plugins(config, entry_point='enterprise.plugins1'):
49 50 for ep in iter_entry_points(entry_point):
50 51 plugin_id = '{}{}#{}'.format(
51 52 plugin_prefix, ep.dist.project_name, ep.name)
52 53 log.debug('Plugin discovered: "%s"', plugin_id)
53 54 try:
54 55 module = ep.load()
55 56 plugin = module(plugin_id=plugin_id)
56 57 config.include(plugin.includeme)
57 58 except Exception as e:
58 59 log.exception(
59 60 'Exception while loading authentication plugin '
60 61 '"{}": {}'.format(plugin_id, e.message))
61 62
62 63
63 64 def _import_legacy_plugin(plugin_id):
64 65 module_name = plugin_id.split(legacy_plugin_prefix, 1)[-1]
65 66 module = importlib.import_module(module_name)
66 67 return module.plugin_factory(plugin_id=plugin_id)
67 68
68 69
69 70 def _discover_legacy_plugins(config, prefix=legacy_plugin_prefix):
70 71 """
71 72 Function that imports the legacy plugins stored in the 'auth_plugins'
72 73 setting in database which are using the specified prefix. Normally 'py:' is
73 74 used for the legacy plugins.
74 75 """
75 76 try:
76 77 auth_plugins = SettingsModel().get_setting_by_name('auth_plugins')
77 78 enabled_plugins = auth_plugins.app_settings_value
78 79 legacy_plugins = [id_ for id_ in enabled_plugins if id_.startswith(prefix)]
79 80 except Exception:
80 81 legacy_plugins = []
81 82
82 83 for plugin_id in legacy_plugins:
83 84 log.debug('Legacy plugin discovered: "%s"', plugin_id)
84 85 try:
85 86 plugin = _import_legacy_plugin(plugin_id)
86 87 config.include(plugin.includeme)
87 88 except Exception as e:
88 89 log.exception(
89 90 'Exception while loading legacy authentication plugin '
90 91 '"{}": {}'.format(plugin_id, e.message))
91 92
92 93
93 94 def includeme(config):
94 95 # Set authentication policy.
95 96 authn_policy = SessionAuthenticationPolicy()
96 97 config.set_authentication_policy(authn_policy)
97 98
98 99 # Create authentication plugin registry and add it to the pyramid registry.
99 100 authn_registry = AuthenticationPluginRegistry(config.get_settings())
100 101 config.add_directive('add_authn_plugin', authn_registry.add_authn_plugin)
101 102 config.registry.registerUtility(authn_registry)
102 103
103 104 # Create authentication traversal root resource.
104 105 authn_root_resource = root_factory()
105 106 config.add_directive('add_authn_resource',
106 107 authn_root_resource.add_authn_resource)
107 108
108 109 # Add the authentication traversal route.
109 110 config.add_route('auth_home',
110 111 ADMIN_PREFIX + '/auth*traverse',
111 112 factory=root_factory)
112 113 # Add the authentication settings root views.
113 114 config.add_view('rhodecode.authentication.views.AuthSettingsView',
114 115 attr='index',
115 116 request_method='GET',
116 117 route_name='auth_home',
117 118 context=AuthnRootResource)
118 119 config.add_view('rhodecode.authentication.views.AuthSettingsView',
119 120 attr='auth_settings',
120 121 request_method='POST',
121 122 route_name='auth_home',
122 123 context=AuthnRootResource)
123 124
124 125 for key in ['RC_CMD_SETUP_RC', 'RC_CMD_UPGRADE_DB', 'RC_CMD_SSH_WRAPPER']:
125 126 if os.environ.get(key):
126 127 # skip this heavy step below on certain CLI commands
127 128 return
128 129
129 130 # Auto discover authentication plugins and include their configuration.
130 131 _discover_plugins(config)
131 132 _discover_legacy_plugins(config)
@@ -1,759 +1,763 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Authentication modules
23 23 """
24 24 import socket
25 25 import string
26 26 import colander
27 27 import copy
28 28 import logging
29 29 import time
30 30 import traceback
31 31 import warnings
32 32 import functools
33 33
34 34 from pyramid.threadlocal import get_current_registry
35 35
36 36 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 38 from rhodecode.lib import rc_cache
39 39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 40 from rhodecode.lib.utils2 import safe_int, safe_str
41 41 from rhodecode.lib.exceptions import LdapConnectionError
42 42 from rhodecode.model.db import User
43 43 from rhodecode.model.meta import Session
44 44 from rhodecode.model.settings import SettingsModel
45 45 from rhodecode.model.user import UserModel
46 46 from rhodecode.model.user_group import UserGroupModel
47 47
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51 # auth types that authenticate() function can receive
52 52 VCS_TYPE = 'vcs'
53 53 HTTP_TYPE = 'http'
54 54
55 55
56 56 class hybrid_property(object):
57 57 """
58 58 a property decorator that works both for instance and class
59 59 """
60 60 def __init__(self, fget, fset=None, fdel=None, expr=None):
61 61 self.fget = fget
62 62 self.fset = fset
63 63 self.fdel = fdel
64 64 self.expr = expr or fget
65 65 functools.update_wrapper(self, fget)
66 66
67 67 def __get__(self, instance, owner):
68 68 if instance is None:
69 69 return self.expr(owner)
70 70 else:
71 71 return self.fget(instance)
72 72
73 73 def __set__(self, instance, value):
74 74 self.fset(instance, value)
75 75
76 76 def __delete__(self, instance):
77 77 self.fdel(instance)
78 78
79 79
80 80 class LazyFormencode(object):
81 81 def __init__(self, formencode_obj, *args, **kwargs):
82 82 self.formencode_obj = formencode_obj
83 83 self.args = args
84 84 self.kwargs = kwargs
85 85
86 86 def __call__(self, *args, **kwargs):
87 87 from inspect import isfunction
88 88 formencode_obj = self.formencode_obj
89 89 if isfunction(formencode_obj):
90 90 # case we wrap validators into functions
91 91 formencode_obj = self.formencode_obj(*args, **kwargs)
92 92 return formencode_obj(*self.args, **self.kwargs)
93 93
94 94
95 95 class RhodeCodeAuthPluginBase(object):
96 96 # cache the authentication request for N amount of seconds. Some kind
97 97 # of authentication methods are very heavy and it's very efficient to cache
98 98 # the result of a call. If it's set to None (default) cache is off
99 99 AUTH_CACHE_TTL = None
100 100 AUTH_CACHE = {}
101 101
102 102 auth_func_attrs = {
103 103 "username": "unique username",
104 104 "firstname": "first name",
105 105 "lastname": "last name",
106 106 "email": "email address",
107 107 "groups": '["list", "of", "groups"]',
108 108 "user_group_sync":
109 109 'True|False defines if returned user groups should be synced',
110 110 "extern_name": "name in external source of record",
111 111 "extern_type": "type of external source of record",
112 112 "admin": 'True|False defines if user should be RhodeCode super admin',
113 113 "active":
114 114 'True|False defines active state of user internally for RhodeCode',
115 115 "active_from_extern":
116 116 "True|False\None, active state from the external auth, "
117 117 "None means use definition from RhodeCode extern_type active value"
118 118
119 119 }
120 120 # set on authenticate() method and via set_auth_type func.
121 121 auth_type = None
122 122
123 123 # set on authenticate() method and via set_calling_scope_repo, this is a
124 124 # calling scope repository when doing authentication most likely on VCS
125 125 # operations
126 126 acl_repo_name = None
127 127
128 128 # List of setting names to store encrypted. Plugins may override this list
129 129 # to store settings encrypted.
130 130 _settings_encrypted = []
131 131
132 132 # Mapping of python to DB settings model types. Plugins may override or
133 133 # extend this mapping.
134 134 _settings_type_map = {
135 135 colander.String: 'unicode',
136 136 colander.Integer: 'int',
137 137 colander.Boolean: 'bool',
138 138 colander.List: 'list',
139 139 }
140 140
141 141 # list of keys in settings that are unsafe to be logged, should be passwords
142 142 # or other crucial credentials
143 143 _settings_unsafe_keys = []
144 144
145 145 def __init__(self, plugin_id):
146 146 self._plugin_id = plugin_id
147 147
148 148 def __str__(self):
149 149 return self.get_id()
150 150
151 151 def _get_setting_full_name(self, name):
152 152 """
153 153 Return the full setting name used for storing values in the database.
154 154 """
155 155 # TODO: johbo: Using the name here is problematic. It would be good to
156 156 # introduce either new models in the database to hold Plugin and
157 157 # PluginSetting or to use the plugin id here.
158 158 return 'auth_{}_{}'.format(self.name, name)
159 159
160 160 def _get_setting_type(self, name):
161 161 """
162 162 Return the type of a setting. This type is defined by the SettingsModel
163 163 and determines how the setting is stored in DB. Optionally the suffix
164 164 `.encrypted` is appended to instruct SettingsModel to store it
165 165 encrypted.
166 166 """
167 167 schema_node = self.get_settings_schema().get(name)
168 168 db_type = self._settings_type_map.get(
169 169 type(schema_node.typ), 'unicode')
170 170 if name in self._settings_encrypted:
171 171 db_type = '{}.encrypted'.format(db_type)
172 172 return db_type
173 173
174 174 def is_enabled(self):
175 175 """
176 176 Returns true if this plugin is enabled. An enabled plugin can be
177 177 configured in the admin interface but it is not consulted during
178 178 authentication.
179 179 """
180 180 auth_plugins = SettingsModel().get_auth_plugins()
181 181 return self.get_id() in auth_plugins
182 182
183 183 def is_active(self, plugin_cached_settings=None):
184 184 """
185 185 Returns true if the plugin is activated. An activated plugin is
186 186 consulted during authentication, assumed it is also enabled.
187 187 """
188 188 return self.get_setting_by_name(
189 189 'enabled', plugin_cached_settings=plugin_cached_settings)
190 190
191 191 def get_id(self):
192 192 """
193 193 Returns the plugin id.
194 194 """
195 195 return self._plugin_id
196 196
197 197 def get_display_name(self):
198 198 """
199 199 Returns a translation string for displaying purposes.
200 200 """
201 201 raise NotImplementedError('Not implemented in base class')
202 202
203 203 def get_settings_schema(self):
204 204 """
205 205 Returns a colander schema, representing the plugin settings.
206 206 """
207 207 return AuthnPluginSettingsSchemaBase()
208 208
209 209 def get_settings(self):
210 210 """
211 211 Returns the plugin settings as dictionary.
212 212 """
213 213 settings = {}
214 214 raw_settings = SettingsModel().get_all_settings()
215 215 for node in self.get_settings_schema():
216 216 settings[node.name] = self.get_setting_by_name(
217 217 node.name, plugin_cached_settings=raw_settings)
218 218 return settings
219 219
220 220 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
221 221 """
222 222 Returns a plugin setting by name.
223 223 """
224 224 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
225 225 if plugin_cached_settings:
226 226 plugin_settings = plugin_cached_settings
227 227 else:
228 228 plugin_settings = SettingsModel().get_all_settings()
229 229
230 230 if full_name in plugin_settings:
231 231 return plugin_settings[full_name]
232 232 else:
233 233 return default
234 234
235 235 def create_or_update_setting(self, name, value):
236 236 """
237 237 Create or update a setting for this plugin in the persistent storage.
238 238 """
239 239 full_name = self._get_setting_full_name(name)
240 240 type_ = self._get_setting_type(name)
241 241 db_setting = SettingsModel().create_or_update_setting(
242 242 full_name, value, type_)
243 243 return db_setting.app_settings_value
244 244
245 245 def log_safe_settings(self, settings):
246 246 """
247 247 returns a log safe representation of settings, without any secrets
248 248 """
249 249 settings_copy = copy.deepcopy(settings)
250 250 for k in self._settings_unsafe_keys:
251 251 if k in settings_copy:
252 252 del settings_copy[k]
253 253 return settings_copy
254 254
255 255 @hybrid_property
256 256 def name(self):
257 257 """
258 258 Returns the name of this authentication plugin.
259 259
260 260 :returns: string
261 261 """
262 262 raise NotImplementedError("Not implemented in base class")
263 263
264 264 def get_url_slug(self):
265 265 """
266 266 Returns a slug which should be used when constructing URLs which refer
267 267 to this plugin. By default it returns the plugin name. If the name is
268 268 not suitable for using it in an URL the plugin should override this
269 269 method.
270 270 """
271 271 return self.name
272 272
273 273 @property
274 274 def is_headers_auth(self):
275 275 """
276 276 Returns True if this authentication plugin uses HTTP headers as
277 277 authentication method.
278 278 """
279 279 return False
280 280
281 281 @hybrid_property
282 282 def is_container_auth(self):
283 283 """
284 284 Deprecated method that indicates if this authentication plugin uses
285 285 HTTP headers as authentication method.
286 286 """
287 287 warnings.warn(
288 288 'Use is_headers_auth instead.', category=DeprecationWarning)
289 289 return self.is_headers_auth
290 290
291 291 @hybrid_property
292 292 def allows_creating_users(self):
293 293 """
294 294 Defines if Plugin allows users to be created on-the-fly when
295 295 authentication is called. Controls how external plugins should behave
296 296 in terms if they are allowed to create new users, or not. Base plugins
297 297 should not be allowed to, but External ones should be !
298 298
299 299 :return: bool
300 300 """
301 301 return False
302 302
303 303 def set_auth_type(self, auth_type):
304 304 self.auth_type = auth_type
305 305
306 306 def set_calling_scope_repo(self, acl_repo_name):
307 307 self.acl_repo_name = acl_repo_name
308 308
309 309 def allows_authentication_from(
310 310 self, user, allows_non_existing_user=True,
311 311 allowed_auth_plugins=None, allowed_auth_sources=None):
312 312 """
313 313 Checks if this authentication module should accept a request for
314 314 the current user.
315 315
316 316 :param user: user object fetched using plugin's get_user() method.
317 317 :param allows_non_existing_user: if True, don't allow the
318 318 user to be empty, meaning not existing in our database
319 319 :param allowed_auth_plugins: if provided, users extern_type will be
320 320 checked against a list of provided extern types, which are plugin
321 321 auth_names in the end
322 322 :param allowed_auth_sources: authentication type allowed,
323 323 `http` or `vcs` default is both.
324 324 defines if plugin will accept only http authentication vcs
325 325 authentication(git/hg) or both
326 326 :returns: boolean
327 327 """
328 328 if not user and not allows_non_existing_user:
329 329 log.debug('User is empty but plugin does not allow empty users,'
330 330 'not allowed to authenticate')
331 331 return False
332 332
333 333 expected_auth_plugins = allowed_auth_plugins or [self.name]
334 334 if user and (user.extern_type and
335 335 user.extern_type not in expected_auth_plugins):
336 336 log.debug(
337 337 'User `%s` is bound to `%s` auth type. Plugin allows only '
338 338 '%s, skipping', user, user.extern_type, expected_auth_plugins)
339 339
340 340 return False
341 341
342 342 # by default accept both
343 343 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
344 344 if self.auth_type not in expected_auth_from:
345 345 log.debug('Current auth source is %s but plugin only allows %s',
346 346 self.auth_type, expected_auth_from)
347 347 return False
348 348
349 349 return True
350 350
351 351 def get_user(self, username=None, **kwargs):
352 352 """
353 353 Helper method for user fetching in plugins, by default it's using
354 354 simple fetch by username, but this method can be custimized in plugins
355 355 eg. headers auth plugin to fetch user by environ params
356 356
357 357 :param username: username if given to fetch from database
358 358 :param kwargs: extra arguments needed for user fetching.
359 359 """
360 360 user = None
361 361 log.debug(
362 362 'Trying to fetch user `%s` from RhodeCode database', username)
363 363 if username:
364 364 user = User.get_by_username(username)
365 365 if not user:
366 366 log.debug('User not found, fallback to fetch user in '
367 367 'case insensitive mode')
368 368 user = User.get_by_username(username, case_insensitive=True)
369 369 else:
370 370 log.debug('provided username:`%s` is empty skipping...', username)
371 371 if not user:
372 372 log.debug('User `%s` not found in database', username)
373 373 else:
374 374 log.debug('Got DB user:%s', user)
375 375 return user
376 376
377 377 def user_activation_state(self):
378 378 """
379 379 Defines user activation state when creating new users
380 380
381 381 :returns: boolean
382 382 """
383 383 raise NotImplementedError("Not implemented in base class")
384 384
385 385 def auth(self, userobj, username, passwd, settings, **kwargs):
386 386 """
387 387 Given a user object (which may be null), username, a plaintext
388 388 password, and a settings object (containing all the keys needed as
389 389 listed in settings()), authenticate this user's login attempt.
390 390
391 391 Return None on failure. On success, return a dictionary of the form:
392 392
393 393 see: RhodeCodeAuthPluginBase.auth_func_attrs
394 394 This is later validated for correctness
395 395 """
396 396 raise NotImplementedError("not implemented in base class")
397 397
398 398 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
399 399 """
400 400 Wrapper to call self.auth() that validates call on it
401 401
402 402 :param userobj: userobj
403 403 :param username: username
404 404 :param passwd: plaintext password
405 405 :param settings: plugin settings
406 406 """
407 407 auth = self.auth(userobj, username, passwd, settings, **kwargs)
408 408 if auth:
409 409 auth['_plugin'] = self.name
410 410 auth['_ttl_cache'] = self.get_ttl_cache(settings)
411 411 # check if hash should be migrated ?
412 412 new_hash = auth.get('_hash_migrate')
413 413 if new_hash:
414 414 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
415 415 if 'user_group_sync' not in auth:
416 416 auth['user_group_sync'] = False
417 417 return self._validate_auth_return(auth)
418 418 return auth
419 419
420 420 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
421 421 new_hash_cypher = _RhodeCodeCryptoBCrypt()
422 422 # extra checks, so make sure new hash is correct.
423 423 password_encoded = safe_str(password)
424 424 if new_hash and new_hash_cypher.hash_check(
425 425 password_encoded, new_hash):
426 426 cur_user = User.get_by_username(username)
427 427 cur_user.password = new_hash
428 428 Session().add(cur_user)
429 429 Session().flush()
430 430 log.info('Migrated user %s hash to bcrypt', cur_user)
431 431
432 432 def _validate_auth_return(self, ret):
433 433 if not isinstance(ret, dict):
434 434 raise Exception('returned value from auth must be a dict')
435 435 for k in self.auth_func_attrs:
436 436 if k not in ret:
437 437 raise Exception('Missing %s attribute from returned data' % k)
438 438 return ret
439 439
440 440 def get_ttl_cache(self, settings=None):
441 441 plugin_settings = settings or self.get_settings()
442 cache_ttl = 0
442 # we set default to 30, we make a compromise here,
443 # performance > security, mostly due to LDAP/SVN, majority
444 # of users pick cache_ttl to be enabled
445 from rhodecode.authentication import plugin_default_auth_ttl
446 cache_ttl = plugin_default_auth_ttl
443 447
444 448 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
445 449 # plugin cache set inside is more important than the settings value
446 450 cache_ttl = self.AUTH_CACHE_TTL
447 451 elif plugin_settings.get('cache_ttl'):
448 452 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
449 453
450 454 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
451 455 return plugin_cache_active, cache_ttl
452 456
453 457
454 458 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
455 459
456 460 @hybrid_property
457 461 def allows_creating_users(self):
458 462 return True
459 463
460 464 def use_fake_password(self):
461 465 """
462 466 Return a boolean that indicates whether or not we should set the user's
463 467 password to a random value when it is authenticated by this plugin.
464 468 If your plugin provides authentication, then you will generally
465 469 want this.
466 470
467 471 :returns: boolean
468 472 """
469 473 raise NotImplementedError("Not implemented in base class")
470 474
471 475 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
472 476 # at this point _authenticate calls plugin's `auth()` function
473 477 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
474 478 userobj, username, passwd, settings, **kwargs)
475 479
476 480 if auth:
477 481 # maybe plugin will clean the username ?
478 482 # we should use the return value
479 483 username = auth['username']
480 484
481 485 # if external source tells us that user is not active, we should
482 486 # skip rest of the process. This can prevent from creating users in
483 487 # RhodeCode when using external authentication, but if it's
484 488 # inactive user we shouldn't create that user anyway
485 489 if auth['active_from_extern'] is False:
486 490 log.warning(
487 491 "User %s authenticated against %s, but is inactive",
488 492 username, self.__module__)
489 493 return None
490 494
491 495 cur_user = User.get_by_username(username, case_insensitive=True)
492 496 is_user_existing = cur_user is not None
493 497
494 498 if is_user_existing:
495 499 log.debug('Syncing user `%s` from '
496 500 '`%s` plugin', username, self.name)
497 501 else:
498 502 log.debug('Creating non existing user `%s` from '
499 503 '`%s` plugin', username, self.name)
500 504
501 505 if self.allows_creating_users:
502 506 log.debug('Plugin `%s` allows to '
503 507 'create new users', self.name)
504 508 else:
505 509 log.debug('Plugin `%s` does not allow to '
506 510 'create new users', self.name)
507 511
508 512 user_parameters = {
509 513 'username': username,
510 514 'email': auth["email"],
511 515 'firstname': auth["firstname"],
512 516 'lastname': auth["lastname"],
513 517 'active': auth["active"],
514 518 'admin': auth["admin"],
515 519 'extern_name': auth["extern_name"],
516 520 'extern_type': self.name,
517 521 'plugin': self,
518 522 'allow_to_create_user': self.allows_creating_users,
519 523 }
520 524
521 525 if not is_user_existing:
522 526 if self.use_fake_password():
523 527 # Randomize the PW because we don't need it, but don't want
524 528 # them blank either
525 529 passwd = PasswordGenerator().gen_password(length=16)
526 530 user_parameters['password'] = passwd
527 531 else:
528 532 # Since the password is required by create_or_update method of
529 533 # UserModel, we need to set it explicitly.
530 534 # The create_or_update method is smart and recognises the
531 535 # password hashes as well.
532 536 user_parameters['password'] = cur_user.password
533 537
534 538 # we either create or update users, we also pass the flag
535 539 # that controls if this method can actually do that.
536 540 # raises NotAllowedToCreateUserError if it cannot, and we try to.
537 541 user = UserModel().create_or_update(**user_parameters)
538 542 Session().flush()
539 543 # enforce user is just in given groups, all of them has to be ones
540 544 # created from plugins. We store this info in _group_data JSON
541 545 # field
542 546
543 547 if auth['user_group_sync']:
544 548 try:
545 549 groups = auth['groups'] or []
546 550 log.debug(
547 551 'Performing user_group sync based on set `%s` '
548 552 'returned by `%s` plugin', groups, self.name)
549 553 UserGroupModel().enforce_groups(user, groups, self.name)
550 554 except Exception:
551 555 # for any reason group syncing fails, we should
552 556 # proceed with login
553 557 log.error(traceback.format_exc())
554 558
555 559 Session().commit()
556 560 return auth
557 561
558 562
559 563 class AuthLdapBase(object):
560 564
561 565 @classmethod
562 566 def _build_servers(cls, ldap_server_type, ldap_server, port):
563 567 def host_resolver(host, port, full_resolve=True):
564 568 """
565 569 Main work for this function is to prevent ldap connection issues,
566 570 and detect them early using a "greenified" sockets
567 571 """
568 572 host = host.strip()
569 573 if not full_resolve:
570 574 return '{}:{}'.format(host, port)
571 575
572 576 log.debug('LDAP: Resolving IP for LDAP host %s', host)
573 577 try:
574 578 ip = socket.gethostbyname(host)
575 579 log.debug('Got LDAP server %s ip %s', host, ip)
576 580 except Exception:
577 581 raise LdapConnectionError(
578 582 'Failed to resolve host: `{}`'.format(host))
579 583
580 584 log.debug('LDAP: Checking if IP %s is accessible', ip)
581 585 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
582 586 try:
583 587 s.connect((ip, int(port)))
584 588 s.shutdown(socket.SHUT_RD)
585 589 except Exception:
586 590 raise LdapConnectionError(
587 591 'Failed to connect to host: `{}:{}`'.format(host, port))
588 592
589 593 return '{}:{}'.format(host, port)
590 594
591 595 if len(ldap_server) == 1:
592 596 # in case of single server use resolver to detect potential
593 597 # connection issues
594 598 full_resolve = True
595 599 else:
596 600 full_resolve = False
597 601
598 602 return ', '.join(
599 603 ["{}://{}".format(
600 604 ldap_server_type,
601 605 host_resolver(host, port, full_resolve=full_resolve))
602 606 for host in ldap_server])
603 607
604 608 @classmethod
605 609 def _get_server_list(cls, servers):
606 610 return map(string.strip, servers.split(','))
607 611
608 612 @classmethod
609 613 def get_uid(cls, username, server_addresses):
610 614 uid = username
611 615 for server_addr in server_addresses:
612 616 uid = chop_at(username, "@%s" % server_addr)
613 617 return uid
614 618
615 619
616 620 def loadplugin(plugin_id):
617 621 """
618 622 Loads and returns an instantiated authentication plugin.
619 623 Returns the RhodeCodeAuthPluginBase subclass on success,
620 624 or None on failure.
621 625 """
622 626 # TODO: Disusing pyramids thread locals to retrieve the registry.
623 627 authn_registry = get_authn_registry()
624 628 plugin = authn_registry.get_plugin(plugin_id)
625 629 if plugin is None:
626 630 log.error('Authentication plugin not found: "%s"', plugin_id)
627 631 return plugin
628 632
629 633
630 634 def get_authn_registry(registry=None):
631 635 registry = registry or get_current_registry()
632 636 authn_registry = registry.getUtility(IAuthnPluginRegistry)
633 637 return authn_registry
634 638
635 639
636 640 def authenticate(username, password, environ=None, auth_type=None,
637 641 skip_missing=False, registry=None, acl_repo_name=None):
638 642 """
639 643 Authentication function used for access control,
640 644 It tries to authenticate based on enabled authentication modules.
641 645
642 646 :param username: username can be empty for headers auth
643 647 :param password: password can be empty for headers auth
644 648 :param environ: environ headers passed for headers auth
645 649 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
646 650 :param skip_missing: ignores plugins that are in db but not in environment
647 651 :returns: None if auth failed, plugin_user dict if auth is correct
648 652 """
649 653 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
650 654 raise ValueError('auth type must be on of http, vcs got "%s" instead'
651 655 % auth_type)
652 656 headers_only = environ and not (username and password)
653 657
654 658 authn_registry = get_authn_registry(registry)
655 659 plugins_to_check = authn_registry.get_plugins_for_authentication()
656 660 log.debug('Starting ordered authentication chain using %s plugins',
657 661 [x.name for x in plugins_to_check])
658 662 for plugin in plugins_to_check:
659 663 plugin.set_auth_type(auth_type)
660 664 plugin.set_calling_scope_repo(acl_repo_name)
661 665
662 666 if headers_only and not plugin.is_headers_auth:
663 667 log.debug('Auth type is for headers only and plugin `%s` is not '
664 668 'headers plugin, skipping...', plugin.get_id())
665 669 continue
666 670
667 671 log.debug('Trying authentication using ** %s **', plugin.get_id())
668 672
669 673 # load plugin settings from RhodeCode database
670 674 plugin_settings = plugin.get_settings()
671 675 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
672 676 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
673 677
674 678 # use plugin's method of user extraction.
675 679 user = plugin.get_user(username, environ=environ,
676 680 settings=plugin_settings)
677 681 display_user = user.username if user else username
678 682 log.debug(
679 683 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
680 684
681 685 if not plugin.allows_authentication_from(user):
682 686 log.debug('Plugin %s does not accept user `%s` for authentication',
683 687 plugin.get_id(), display_user)
684 688 continue
685 689 else:
686 690 log.debug('Plugin %s accepted user `%s` for authentication',
687 691 plugin.get_id(), display_user)
688 692
689 693 log.info('Authenticating user `%s` using %s plugin',
690 694 display_user, plugin.get_id())
691 695
692 696 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
693 697
694 698 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
695 699 plugin.get_id(), plugin_cache_active, cache_ttl)
696 700
697 701 user_id = user.user_id if user else None
698 702 # don't cache for empty users
699 703 plugin_cache_active = plugin_cache_active and user_id
700 704 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
701 705 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
702 706
703 707 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
704 708 expiration_time=cache_ttl,
705 709 condition=plugin_cache_active)
706 710 def compute_auth(
707 711 cache_name, plugin_name, username, password):
708 712
709 713 # _authenticate is a wrapper for .auth() method of plugin.
710 714 # it checks if .auth() sends proper data.
711 715 # For RhodeCodeExternalAuthPlugin it also maps users to
712 716 # Database and maps the attributes returned from .auth()
713 717 # to RhodeCode database. If this function returns data
714 718 # then auth is correct.
715 719 log.debug('Running plugin `%s` _authenticate method '
716 720 'using username and password', plugin.get_id())
717 721 return plugin._authenticate(
718 722 user, username, password, plugin_settings,
719 723 environ=environ or {})
720 724
721 725 start = time.time()
722 726 # for environ based auth, password can be empty, but then the validation is
723 727 # on the server that fills in the env data needed for authentication
724 728 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
725 729
726 730 auth_time = time.time() - start
727 731 log.debug('Authentication for plugin `%s` completed in %.3fs, '
728 732 'expiration time of fetched cache %.1fs.',
729 733 plugin.get_id(), auth_time, cache_ttl)
730 734
731 735 log.debug('PLUGIN USER DATA: %s', plugin_user)
732 736
733 737 if plugin_user:
734 738 log.debug('Plugin returned proper authentication data')
735 739 return plugin_user
736 740 # we failed to Auth because .auth() method didn't return proper user
737 741 log.debug("User `%s` failed to authenticate against %s",
738 742 display_user, plugin.get_id())
739 743
740 744 # case when we failed to authenticate against all defined plugins
741 745 return None
742 746
743 747
744 748 def chop_at(s, sub, inclusive=False):
745 749 """Truncate string ``s`` at the first occurrence of ``sub``.
746 750
747 751 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
748 752
749 753 >>> chop_at("plutocratic brats", "rat")
750 754 'plutoc'
751 755 >>> chop_at("plutocratic brats", "rat", True)
752 756 'plutocrat'
753 757 """
754 758 pos = s.find(sub)
755 759 if pos == -1:
756 760 return s
757 761 if inclusive:
758 762 return s[:pos+len(sub)]
759 763 return s[:pos]
@@ -1,51 +1,52 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22
23 from rhodecode.authentication import plugin_default_auth_ttl
23 24 from rhodecode.translation import _
24 25
25 26
26 27 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
27 28 """
28 29 This base schema is intended for use in authentication plugins.
29 30 It adds a few default settings (e.g., "enabled"), so that plugin
30 31 authors don't have to maintain a bunch of boilerplate.
31 32 """
32 33 enabled = colander.SchemaNode(
33 34 colander.Bool(),
34 35 default=False,
35 36 description=_('Enable or disable this authentication plugin.'),
36 37 missing=False,
37 38 title=_('Enabled'),
38 39 widget='bool',
39 40 )
40 41 cache_ttl = colander.SchemaNode(
41 42 colander.Int(),
42 default=0,
43 default=plugin_default_auth_ttl,
43 44 description=_('Amount of seconds to cache the authentication and '
44 45 'permissions check response call for this plugin. \n'
45 46 'Useful for expensive calls like LDAP to improve the '
46 47 'performance of the system (0 means disabled).'),
47 48 missing=0,
48 49 title=_('Auth Cache TTL'),
49 50 validator=colander.Range(min=0, max=None),
50 51 widget='int',
51 52 )
@@ -1,92 +1,89 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import pytest
23 23
24 24
25 25 class EnabledAuthPlugin(object):
26 26 """
27 27 Context manager that updates the 'auth_plugins' setting in DB to enable
28 28 a plugin. Previous setting is restored on exit. The rhodecode auth plugin
29 29 is also enabled because it is needed to login the test users.
30 30 """
31 31
32 32 def __init__(self, plugin):
33 self.new_value = set([
34 'egg:rhodecode-enterprise-ce#rhodecode',
35 plugin.get_id()
36 ])
33 self.new_value = {'egg:rhodecode-enterprise-ce#rhodecode', plugin.get_id()}
37 34
38 35 def __enter__(self):
39 36 from rhodecode.model.settings import SettingsModel
40 37 self._old_value = SettingsModel().get_auth_plugins()
41 38 SettingsModel().create_or_update_setting(
42 39 'auth_plugins', ','.join(self.new_value))
43 40
44 41 def __exit__(self, type, value, traceback):
45 42 from rhodecode.model.settings import SettingsModel
46 43 SettingsModel().create_or_update_setting(
47 44 'auth_plugins', ','.join(self._old_value))
48 45
49 46
50 class DisabledAuthPlugin():
47 class DisabledAuthPlugin(object):
51 48 """
52 49 Context manager that updates the 'auth_plugins' setting in DB to disable
53 50 a plugin. Previous setting is restored on exit.
54 51 """
55 52
56 53 def __init__(self, plugin):
57 54 self.plugin_id = plugin.get_id()
58 55
59 56 def __enter__(self):
60 57 from rhodecode.model.settings import SettingsModel
61 58 self._old_value = SettingsModel().get_auth_plugins()
62 59 new_value = [id_ for id_ in self._old_value if id_ != self.plugin_id]
63 60 SettingsModel().create_or_update_setting(
64 61 'auth_plugins', ','.join(new_value))
65 62
66 63 def __exit__(self, type, value, traceback):
67 64 from rhodecode.model.settings import SettingsModel
68 65 SettingsModel().create_or_update_setting(
69 66 'auth_plugins', ','.join(self._old_value))
70 67
71 68
72 69 @pytest.fixture(params=[
73 70 ('rhodecode.authentication.plugins.auth_crowd', 'egg:rhodecode-enterprise-ce#crowd'),
74 71 ('rhodecode.authentication.plugins.auth_headers', 'egg:rhodecode-enterprise-ce#headers'),
75 72 ('rhodecode.authentication.plugins.auth_jasig_cas', 'egg:rhodecode-enterprise-ce#jasig_cas'),
76 73 ('rhodecode.authentication.plugins.auth_ldap', 'egg:rhodecode-enterprise-ce#ldap'),
77 74 ('rhodecode.authentication.plugins.auth_pam', 'egg:rhodecode-enterprise-ce#pam'),
78 75 ('rhodecode.authentication.plugins.auth_rhodecode', 'egg:rhodecode-enterprise-ce#rhodecode'),
79 76 ('rhodecode.authentication.plugins.auth_token', 'egg:rhodecode-enterprise-ce#token'),
80 77 ])
81 78 def auth_plugin(request):
82 79 """
83 80 Fixture that provides instance for each authentication plugin. These
84 81 instances are NOT the instances which are registered to the authentication
85 82 registry.
86 83 """
87 84 from importlib import import_module
88 85
89 86 # Create plugin instance.
90 87 module, plugin_id = request.param
91 88 plugin_module = import_module(module)
92 89 return plugin_module.plugin_factory(plugin_id)
@@ -1,81 +1,81 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21
22 21 import os
23 22 import logging
24 23 import rhodecode
25 24
26
27 25 from rhodecode.config import utils
28 26
29 27 from rhodecode.lib.utils import load_rcextensions
30 28 from rhodecode.lib.utils2 import str2bool
31 29 from rhodecode.lib.vcs import connect_vcs
32 30
33 31 log = logging.getLogger(__name__)
34 32
35 33
36 34 def load_pyramid_environment(global_config, settings):
37 35 # Some parts of the code expect a merge of global and app settings.
38 36 settings_merged = global_config.copy()
39 37 settings_merged.update(settings)
40 38
41 39 # TODO(marcink): probably not required anymore
42 40 # configure channelstream,
43 41 settings_merged['channelstream_config'] = {
44 42 'enabled': str2bool(settings_merged.get('channelstream.enabled', False)),
45 43 'server': settings_merged.get('channelstream.server'),
46 44 'secret': settings_merged.get('channelstream.secret')
47 45 }
48 46
49 47 # If this is a test run we prepare the test environment like
50 48 # creating a test database, test search index and test repositories.
51 49 # This has to be done before the database connection is initialized.
52 50 if settings['is_test']:
53 51 rhodecode.is_test = True
54 52 rhodecode.disable_error_handler = True
53 from rhodecode import authentication
54 authentication.plugin_default_auth_ttl = 0
55 55
56 56 utils.initialize_test_environment(settings_merged)
57 57
58 58 # Initialize the database connection.
59 59 utils.initialize_database(settings_merged)
60 60
61 61 load_rcextensions(root_path=settings_merged['here'])
62 62
63 63 # Limit backends to `vcs.backends` from configuration
64 64 for alias in rhodecode.BACKENDS.keys():
65 65 if alias not in settings['vcs.backends']:
66 66 del rhodecode.BACKENDS[alias]
67 67 log.info('Enabled VCS backends: %s', rhodecode.BACKENDS.keys())
68 68
69 69 # initialize vcs client and optionally run the server if enabled
70 70 vcs_server_uri = settings['vcs.server']
71 71 vcs_server_enabled = settings['vcs.server.enable']
72 72
73 73 utils.configure_vcs(settings)
74 74
75 75 # Store the settings to make them available to other modules.
76 76
77 77 rhodecode.PYRAMID_SETTINGS = settings_merged
78 78 rhodecode.CONFIG = settings_merged
79 79
80 80 if vcs_server_enabled:
81 81 connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings))
General Comments 0
You need to be logged in to leave comments. Login now