Show More
@@ -37,7 +37,7 b' from rhodecode.authentication.interface ' | |||
|
37 | 37 | from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase |
|
38 | 38 | from rhodecode.lib import caches |
|
39 | 39 | from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt |
|
40 |
from rhodecode.lib.utils2 import |
|
|
40 | from rhodecode.lib.utils2 import safe_int | |
|
41 | 41 | from rhodecode.lib.utils2 import safe_str |
|
42 | 42 | from rhodecode.model.db import User |
|
43 | 43 | from rhodecode.model.meta import Session |
@@ -423,11 +423,14 b' class RhodeCodeAuthPluginBase(object):' | |||
|
423 | 423 | """ |
|
424 | 424 | auth = self.auth(userobj, username, passwd, settings, **kwargs) |
|
425 | 425 | if auth: |
|
426 | auth['_plugin'] = self.name | |
|
427 | auth['_ttl_cache'] = self.get_ttl_cache(settings) | |
|
426 | 428 | # check if hash should be migrated ? |
|
427 | 429 | new_hash = auth.get('_hash_migrate') |
|
428 | 430 | if new_hash: |
|
429 | 431 | self._migrate_hash_to_bcrypt(username, passwd, new_hash) |
|
430 | 432 | return self._validate_auth_return(auth) |
|
433 | ||
|
431 | 434 | return auth |
|
432 | 435 | |
|
433 | 436 | def _migrate_hash_to_bcrypt(self, username, password, new_hash): |
@@ -450,6 +453,19 b' class RhodeCodeAuthPluginBase(object):' | |||
|
450 | 453 | raise Exception('Missing %s attribute from returned data' % k) |
|
451 | 454 | return ret |
|
452 | 455 | |
|
456 | def get_ttl_cache(self, settings=None): | |
|
457 | plugin_settings = settings or self.get_settings() | |
|
458 | cache_ttl = 0 | |
|
459 | ||
|
460 | if isinstance(self.AUTH_CACHE_TTL, (int, long)): | |
|
461 | # plugin cache set inside is more important than the settings value | |
|
462 | cache_ttl = self.AUTH_CACHE_TTL | |
|
463 | elif plugin_settings.get('cache_ttl'): | |
|
464 | cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0) | |
|
465 | ||
|
466 | plugin_cache_active = bool(cache_ttl and cache_ttl > 0) | |
|
467 | return plugin_cache_active, cache_ttl | |
|
468 | ||
|
453 | 469 | |
|
454 | 470 | class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase): |
|
455 | 471 | |
@@ -578,6 +594,11 b' def get_auth_cache_manager(custom_ttl=No' | |||
|
578 | 594 | 'auth_plugins', 'rhodecode.authentication', custom_ttl) |
|
579 | 595 | |
|
580 | 596 | |
|
597 | def get_perms_cache_manager(custom_ttl=None): | |
|
598 | return caches.get_cache_manager( | |
|
599 | 'auth_plugins', 'rhodecode.permissions', custom_ttl) | |
|
600 | ||
|
601 | ||
|
581 | 602 | def authenticate(username, password, environ=None, auth_type=None, |
|
582 | 603 | skip_missing=False, registry=None, acl_repo_name=None): |
|
583 | 604 | """ |
@@ -633,25 +654,19 b' def authenticate(username, password, env' | |||
|
633 | 654 | log.info('Authenticating user `%s` using %s plugin', |
|
634 | 655 | display_user, plugin.get_id()) |
|
635 | 656 | |
|
636 | _cache_ttl = 0 | |
|
637 | ||
|
638 | if isinstance(plugin.AUTH_CACHE_TTL, (int, long)): | |
|
639 | # plugin cache set inside is more important than the settings value | |
|
640 | _cache_ttl = plugin.AUTH_CACHE_TTL | |
|
641 | elif plugin_settings.get('cache_ttl'): | |
|
642 | _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0) | |
|
643 | ||
|
644 | plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0) | |
|
657 | plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings) | |
|
645 | 658 | |
|
646 | 659 | # get instance of cache manager configured for a namespace |
|
647 |
cache_manager = get_auth_cache_manager(custom_ttl= |
|
|
660 | cache_manager = get_auth_cache_manager(custom_ttl=cache_ttl) | |
|
648 | 661 | |
|
649 | 662 | log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)', |
|
650 |
plugin.get_id(), plugin_cache_active, |
|
|
663 | plugin.get_id(), plugin_cache_active, cache_ttl) | |
|
651 | 664 | |
|
652 | 665 | # for environ based password can be empty, but then the validation is |
|
653 | 666 | # on the server that fills in the env data needed for authentication |
|
654 | _password_hash = md5_safe(plugin.name + username + (password or '')) | |
|
667 | ||
|
668 | _password_hash = caches.compute_key_from_params( | |
|
669 | plugin.name, username, (password or '')) | |
|
655 | 670 | |
|
656 | 671 | # _authenticate is a wrapper for .auth() method of plugin. |
|
657 | 672 | # it checks if .auth() sends proper data. |
@@ -667,11 +682,13 b' def authenticate(username, password, env' | |||
|
667 | 682 | This function is used internally in Cache of Beaker to calculate |
|
668 | 683 | Results |
|
669 | 684 | """ |
|
685 | log.debug('auth: calculating password access now...') | |
|
670 | 686 | return plugin._authenticate( |
|
671 | 687 | user, username, password, plugin_settings, |
|
672 | 688 | environ=environ or {}) |
|
673 | 689 | |
|
674 | 690 | if plugin_cache_active: |
|
691 | log.debug('Trying to fetch cached auth by %s', _password_hash[:6]) | |
|
675 | 692 | plugin_user = cache_manager.get( |
|
676 | 693 | _password_hash, createfunc=auth_func) |
|
677 | 694 | else: |
@@ -680,7 +697,7 b' def authenticate(username, password, env' | |||
|
680 | 697 | auth_time = time.time() - start |
|
681 | 698 | log.debug('Authentication for plugin `%s` completed in %.3fs, ' |
|
682 | 699 | 'expiration time of fetched cache %.1fs.', |
|
683 |
plugin.get_id(), auth_time, |
|
|
700 | plugin.get_id(), auth_time, cache_ttl) | |
|
684 | 701 | |
|
685 | 702 | log.debug('PLUGIN USER DATA: %s', plugin_user) |
|
686 | 703 | |
@@ -690,6 +707,8 b' def authenticate(username, password, env' | |||
|
690 | 707 | # we failed to Auth because .auth() method didn't return proper user |
|
691 | 708 | log.debug("User `%s` failed to authenticate against %s", |
|
692 | 709 | display_user, plugin.get_id()) |
|
710 | ||
|
711 | # case when we failed to authenticate against all defined plugins | |
|
693 | 712 | return None |
|
694 | 713 | |
|
695 | 714 |
@@ -40,11 +40,10 b' class AuthnPluginSettingsSchemaBase(cola' | |||
|
40 | 40 | cache_ttl = colander.SchemaNode( |
|
41 | 41 | colander.Int(), |
|
42 | 42 | default=0, |
|
43 |
description=_('Amount of seconds to cache the authentication |
|
|
44 | 'call for this plugin. \n' | |
|
45 |
'Useful for |
|
|
46 |
'performance of the |
|
|
47 | '(0 means disabled).'), | |
|
43 | description=_('Amount of seconds to cache the authentication and ' | |
|
44 | 'permissions check response call for this plugin. \n' | |
|
45 | 'Useful for expensive calls like LDAP to improve the ' | |
|
46 | 'performance of the system (0 means disabled).'), | |
|
48 | 47 | missing=0, |
|
49 | 48 | title=_('Auth Cache TTL'), |
|
50 | 49 | validator=colander.Range(min=0, max=None), |
@@ -277,10 +277,11 b' class BasicAuth(AuthBasicAuthenticator):' | |||
|
277 | 277 | _parts = auth.split(':', 1) |
|
278 | 278 | if len(_parts) == 2: |
|
279 | 279 | username, password = _parts |
|
280 |
|
|
|
280 | auth_data = self.authfunc( | |
|
281 | 281 | username, password, environ, VCS_TYPE, |
|
282 |
registry=self.registry, acl_repo_name=self.acl_repo_name) |
|
|
283 | return username | |
|
282 | registry=self.registry, acl_repo_name=self.acl_repo_name) | |
|
283 | if auth_data: | |
|
284 | return {'username': username, 'auth_data': auth_data} | |
|
284 | 285 | if username and password: |
|
285 | 286 | # we mark that we actually executed authentication once, at |
|
286 | 287 | # that point we can use the alternative auth code |
@@ -24,17 +24,20 b" It's implemented with basic auth functio" | |||
|
24 | 24 | """ |
|
25 | 25 | |
|
26 | 26 | import os |
|
27 | import re | |
|
27 | 28 | import logging |
|
28 | 29 | import importlib |
|
29 | import re | |
|
30 | 30 | from functools import wraps |
|
31 | 31 | |
|
32 | import time | |
|
32 | 33 | from paste.httpheaders import REMOTE_USER, AUTH_TYPE |
|
33 | 34 | from webob.exc import ( |
|
34 | 35 | HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError) |
|
35 | 36 | |
|
36 | 37 | import rhodecode |
|
37 |
from rhodecode.authentication.base import |
|
|
38 | from rhodecode.authentication.base import ( | |
|
39 | authenticate, get_perms_cache_manager, VCS_TYPE) | |
|
40 | from rhodecode.lib import caches | |
|
38 | 41 | from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware |
|
39 | 42 | from rhodecode.lib.base import ( |
|
40 | 43 | BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context) |
@@ -44,8 +47,7 b' from rhodecode.lib.exceptions import (' | |||
|
44 | 47 | from rhodecode.lib.hooks_daemon import prepare_callback_daemon |
|
45 | 48 | from rhodecode.lib.middleware import appenlight |
|
46 | 49 | from rhodecode.lib.middleware.utils import scm_app_http |
|
47 |
from rhodecode.lib.utils import |
|
|
48 | is_valid_repo, get_rhodecode_base_path, SLUG_RE) | |
|
50 | from rhodecode.lib.utils import is_valid_repo, SLUG_RE | |
|
49 | 51 | from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode |
|
50 | 52 | from rhodecode.lib.vcs.conf import settings as vcs_settings |
|
51 | 53 | from rhodecode.lib.vcs.backends import base |
@@ -242,39 +244,80 b' class SimpleVCS(object):' | |||
|
242 | 244 | def is_shadow_repo_dir(self): |
|
243 | 245 | return os.path.isdir(self.vcs_repo_name) |
|
244 | 246 | |
|
245 |
def _check_permission(self, action, user, repo_name, ip_addr=None |
|
|
247 | def _check_permission(self, action, user, repo_name, ip_addr=None, | |
|
248 | plugin_id='', plugin_cache_active=False, cache_ttl=0): | |
|
246 | 249 | """ |
|
247 | 250 | Checks permissions using action (push/pull) user and repository |
|
248 | name | |
|
251 | name. If plugin_cache and ttl is set it will use the plugin which | |
|
252 | authenticated the user to store the cached permissions result for N | |
|
253 | amount of seconds as in cache_ttl | |
|
249 | 254 | |
|
250 | 255 | :param action: push or pull action |
|
251 | 256 | :param user: user instance |
|
252 | 257 | :param repo_name: repository name |
|
253 | 258 | """ |
|
254 | # check IP | |
|
255 | inherit = user.inherit_default_permissions | |
|
256 | ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr, | |
|
257 | inherit_from_default=inherit) | |
|
258 | if ip_allowed: | |
|
259 | log.info('Access for IP:%s allowed', ip_addr) | |
|
260 | else: | |
|
261 | return False | |
|
259 | ||
|
260 | # get instance of cache manager configured for a namespace | |
|
261 | cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl) | |
|
262 | log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)', | |
|
263 | plugin_id, plugin_cache_active, cache_ttl) | |
|
264 | ||
|
265 | # for environ based password can be empty, but then the validation is | |
|
266 | # on the server that fills in the env data needed for authentication | |
|
267 | _perm_calc_hash = caches.compute_key_from_params( | |
|
268 | plugin_id, action, user.user_id, repo_name, ip_addr) | |
|
262 | 269 | |
|
263 | if action == 'push': | |
|
264 | if not HasPermissionAnyMiddleware('repository.write', | |
|
265 | 'repository.admin')(user, | |
|
266 | repo_name): | |
|
270 | # _authenticate is a wrapper for .auth() method of plugin. | |
|
271 | # it checks if .auth() sends proper data. | |
|
272 | # For RhodeCodeExternalAuthPlugin it also maps users to | |
|
273 | # Database and maps the attributes returned from .auth() | |
|
274 | # to RhodeCode database. If this function returns data | |
|
275 | # then auth is correct. | |
|
276 | start = time.time() | |
|
277 | log.debug('Running plugin `%s` permissions check', plugin_id) | |
|
278 | ||
|
279 | def perm_func(): | |
|
280 | """ | |
|
281 | This function is used internally in Cache of Beaker to calculate | |
|
282 | Results | |
|
283 | """ | |
|
284 | log.debug('auth: calculating permission access now...') | |
|
285 | # check IP | |
|
286 | inherit = user.inherit_default_permissions | |
|
287 | ip_allowed = AuthUser.check_ip_allowed( | |
|
288 | user.user_id, ip_addr, inherit_from_default=inherit) | |
|
289 | if ip_allowed: | |
|
290 | log.info('Access for IP:%s allowed', ip_addr) | |
|
291 | else: | |
|
267 | 292 | return False |
|
268 | 293 | |
|
294 | if action == 'push': | |
|
295 | perms = ('repository.write', 'repository.admin') | |
|
296 | if not HasPermissionAnyMiddleware(*perms)(user, repo_name): | |
|
297 | return False | |
|
298 | ||
|
299 | else: | |
|
300 | # any other action need at least read permission | |
|
301 | perms = ( | |
|
302 | 'repository.read', 'repository.write', 'repository.admin') | |
|
303 | if not HasPermissionAnyMiddleware(*perms)(user, repo_name): | |
|
304 | return False | |
|
305 | ||
|
306 | return True | |
|
307 | ||
|
308 | if plugin_cache_active: | |
|
309 | log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6]) | |
|
310 | perm_result = cache_manager.get( | |
|
311 | _perm_calc_hash, createfunc=perm_func) | |
|
269 | 312 | else: |
|
270 | # any other action need at least read permission | |
|
271 | if not HasPermissionAnyMiddleware('repository.read', | |
|
272 | 'repository.write', | |
|
273 | 'repository.admin')(user, | |
|
274 | repo_name): | |
|
275 | return False | |
|
313 | perm_result = perm_func() | |
|
276 | 314 | |
|
277 | return True | |
|
315 | auth_time = time.time() - start | |
|
316 | log.debug('Permissions for plugin `%s` completed in %.3fs, ' | |
|
317 | 'expiration time of fetched cache %.1fs.', | |
|
318 | plugin_id, auth_time, cache_ttl) | |
|
319 | ||
|
320 | return perm_result | |
|
278 | 321 | |
|
279 | 322 | def _check_ssl(self, environ, start_response): |
|
280 | 323 | """ |
@@ -376,26 +419,41 b' class SimpleVCS(object):' | |||
|
376 | 419 | if pre_auth and pre_auth.get('username'): |
|
377 | 420 | username = pre_auth['username'] |
|
378 | 421 | log.debug('PRE-AUTH got %s as username', username) |
|
422 | if pre_auth: | |
|
423 | log.debug('PRE-AUTH successful from %s', | |
|
424 | pre_auth.get('auth_data', {}).get('_plugin')) | |
|
379 | 425 | |
|
380 | 426 | # If not authenticated by the container, running basic auth |
|
381 | 427 | # before inject the calling repo_name for special scope checks |
|
382 | 428 | self.authenticate.acl_repo_name = self.acl_repo_name |
|
429 | ||
|
430 | plugin_cache_active, cache_ttl = False, 0 | |
|
431 | plugin = None | |
|
383 | 432 | if not username: |
|
384 | 433 | self.authenticate.realm = self.authenticate.get_rc_realm() |
|
385 | 434 | |
|
386 | 435 | try: |
|
387 | result = self.authenticate(environ) | |
|
436 | auth_result = self.authenticate(environ) | |
|
388 | 437 | except (UserCreationError, NotAllowedToCreateUserError) as e: |
|
389 | 438 | log.error(e) |
|
390 | 439 | reason = safe_str(e) |
|
391 | 440 | return HTTPNotAcceptable(reason)(environ, start_response) |
|
392 | 441 | |
|
393 |
if isinstance(result, |
|
|
442 | if isinstance(auth_result, dict): | |
|
394 | 443 | AUTH_TYPE.update(environ, 'basic') |
|
395 | REMOTE_USER.update(environ, result) | |
|
396 | username = result | |
|
444 | REMOTE_USER.update(environ, auth_result['username']) | |
|
445 | username = auth_result['username'] | |
|
446 | plugin = auth_result.get('auth_data', {}).get('_plugin') | |
|
447 | log.info( | |
|
448 | 'MAIN-AUTH successful for user `%s` from %s plugin', | |
|
449 | username, plugin) | |
|
450 | ||
|
451 | plugin_cache_active, cache_ttl = auth_result.get( | |
|
452 | 'auth_data', {}).get('_ttl_cache') or (False, 0) | |
|
397 | 453 | else: |
|
398 |
return result.wsgi_application( |
|
|
454 | return auth_result.wsgi_application( | |
|
455 | environ, start_response) | |
|
456 | ||
|
399 | 457 | |
|
400 | 458 | # ============================================================== |
|
401 | 459 | # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME |
@@ -417,12 +475,13 b' class SimpleVCS(object):' | |||
|
417 | 475 | |
|
418 | 476 | # check permissions for this repository |
|
419 | 477 | perm = self._check_permission( |
|
420 |
action, user, self.acl_repo_name, ip_addr |
|
|
478 | action, user, self.acl_repo_name, ip_addr, | |
|
479 | plugin, plugin_cache_active, cache_ttl) | |
|
421 | 480 | if not perm: |
|
422 | 481 | return HTTPForbidden()(environ, start_response) |
|
423 | 482 | |
|
424 | 483 | # extras are injected into UI object and later available |
|
425 |
# in hooks executed by |
|
|
484 | # in hooks executed by RhodeCode | |
|
426 | 485 | check_locking = _should_check_locking(environ.get('QUERY_STRING')) |
|
427 | 486 | extras = vcs_operation_context( |
|
428 | 487 | environ, repo_name=self.acl_repo_name, username=username, |
@@ -918,7 +918,7 b' class User(Base, BaseModel):' | |||
|
918 | 918 | """Update user lastactivity""" |
|
919 | 919 | self.last_activity = datetime.datetime.now() |
|
920 | 920 | Session().add(self) |
|
921 | log.debug('updated user %s lastactivity', self.username) | |
|
921 | log.debug('updated user `%s` last activity', self.username) | |
|
922 | 922 | |
|
923 | 923 | def update_password(self, new_password): |
|
924 | 924 | from rhodecode.lib.auth import get_crypt_password |
@@ -27,8 +27,13 b' from rhodecode.authentication.plugins.au' | |||
|
27 | 27 | from rhodecode.model import db |
|
28 | 28 | |
|
29 | 29 | |
|
30 | class TestAuthPlugin(RhodeCodeAuthPluginBase): | |
|
31 | ||
|
32 | def name(self): | |
|
33 | return 'stub_auth' | |
|
34 | ||
|
30 | 35 | def test_authenticate_returns_from_auth(stub_auth_data): |
|
31 |
plugin = |
|
|
36 | plugin = TestAuthPlugin('stub_id') | |
|
32 | 37 | with mock.patch.object(plugin, 'auth') as auth_mock: |
|
33 | 38 | auth_mock.return_value = stub_auth_data |
|
34 | 39 | result = plugin._authenticate(mock.Mock(), 'test', 'password', {}) |
@@ -37,7 +42,7 b' def test_authenticate_returns_from_auth(' | |||
|
37 | 42 | |
|
38 | 43 | def test_authenticate_returns_empty_auth_data(): |
|
39 | 44 | auth_data = {} |
|
40 |
plugin = |
|
|
45 | plugin = TestAuthPlugin('stub_id') | |
|
41 | 46 | with mock.patch.object(plugin, 'auth') as auth_mock: |
|
42 | 47 | auth_mock.return_value = auth_data |
|
43 | 48 | result = plugin._authenticate(mock.Mock(), 'test', 'password', {}) |
@@ -46,7 +51,7 b' def test_authenticate_returns_empty_auth' | |||
|
46 | 51 | |
|
47 | 52 | def test_authenticate_skips_hash_migration_if_mismatch(stub_auth_data): |
|
48 | 53 | stub_auth_data['_hash_migrate'] = 'new-hash' |
|
49 |
plugin = |
|
|
54 | plugin = TestAuthPlugin('stub_id') | |
|
50 | 55 | with mock.patch.object(plugin, 'auth') as auth_mock: |
|
51 | 56 | auth_mock.return_value = stub_auth_data |
|
52 | 57 | result = plugin._authenticate(mock.Mock(), 'test', 'password', {}) |
@@ -60,7 +65,7 b' def test_authenticate_migrates_to_new_ha' | |||
|
60 | 65 | new_password = b'new-password' |
|
61 | 66 | new_hash = _RhodeCodeCryptoBCrypt().hash_create(new_password) |
|
62 | 67 | stub_auth_data['_hash_migrate'] = new_hash |
|
63 |
plugin = |
|
|
68 | plugin = TestAuthPlugin('stub_id') | |
|
64 | 69 | with mock.patch.object(plugin, 'auth') as auth_mock: |
|
65 | 70 | auth_mock.return_value = stub_auth_data |
|
66 | 71 | result = plugin._authenticate( |
General Comments 0
You need to be logged in to leave comments.
Login now