diff --git a/rhodecode/authentication/base.py b/rhodecode/authentication/base.py --- a/rhodecode/authentication/base.py +++ b/rhodecode/authentication/base.py @@ -91,6 +91,11 @@ class RhodeCodeAuthPluginBase(object): # set on authenticate() method and via set_auth_type func. auth_type = None + # set on authenticate() method and via set_calling_scope_repo, this is a + # calling scope repository when doing authentication most likely on VCS + # operations + acl_repo_name = None + # List of setting names to store encrypted. Plugins may override this list # to store settings encrypted. _settings_encrypted = [] @@ -268,6 +273,9 @@ class RhodeCodeAuthPluginBase(object): def set_auth_type(self, auth_type): self.auth_type = auth_type + def set_calling_scope_repo(self, acl_repo_name): + self.acl_repo_name = acl_repo_name + def allows_authentication_from( self, user, allows_non_existing_user=True, allowed_auth_plugins=None, allowed_auth_sources=None): @@ -520,7 +528,7 @@ def get_auth_cache_manager(custom_ttl=No def authenticate(username, password, environ=None, auth_type=None, - skip_missing=False, registry=None): + skip_missing=False, registry=None, acl_repo_name=None): """ Authentication function used for access control, It tries to authenticate based on enabled authentication modules. @@ -540,6 +548,7 @@ def authenticate(username, password, env authn_registry = get_authn_registry(registry) for plugin in authn_registry.get_plugins_for_authentication(): plugin.set_auth_type(auth_type) + plugin.set_calling_scope_repo(acl_repo_name) user = plugin.get_user(username) display_user = user.username if user else username diff --git a/rhodecode/authentication/plugins/auth_token.py b/rhodecode/authentication/plugins/auth_token.py --- a/rhodecode/authentication/plugins/auth_token.py +++ b/rhodecode/authentication/plugins/auth_token.py @@ -28,7 +28,7 @@ from rhodecode.translation import _ from rhodecode.authentication.base import ( RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property) from rhodecode.authentication.routes import AuthnPluginResourceBase -from rhodecode.model.db import User, UserApiKeys +from rhodecode.model.db import User, UserApiKeys, Repository log = logging.getLogger(__name__) @@ -121,8 +121,15 @@ class RhodeCodeAuthPlugin(RhodeCodeAuthP log.debug('Authenticating user with args %s', user_attrs) if userobj.active: + # calling context repo for token scopes + scope_repo_id = None + if self.acl_repo_name: + repo = Repository.get_by_repo_name(self.acl_repo_name) + scope_repo_id = repo.repo_id if repo else None + token_match = userobj.authenticate_by_token( - password, roles=[UserApiKeys.ROLE_VCS]) + password, roles=[UserApiKeys.ROLE_VCS], + scope_repo_id=scope_repo_id) if userobj.username == username and token_match: log.info( diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -209,11 +209,12 @@ def vcs_operation_context( class BasicAuth(AuthBasicAuthenticator): def __init__(self, realm, authfunc, registry, auth_http_code=None, - initial_call_detection=False): + initial_call_detection=False, acl_repo_name=None): self.realm = realm self.initial_call = initial_call_detection self.authfunc = authfunc self.registry = registry + self.acl_repo_name = acl_repo_name self._rc_auth_http_code = auth_http_code def _get_response_from_code(self, http_code): @@ -247,7 +248,7 @@ class BasicAuth(AuthBasicAuthenticator): username, password = _parts if self.authfunc( username, password, environ, VCS_TYPE, - registry=self.registry): + registry=self.registry, acl_repo_name=self.acl_repo_name): return username if username and password: # we mark that we actually executed authentication once, at diff --git a/rhodecode/lib/middleware/simplevcs.py b/rhodecode/lib/middleware/simplevcs.py --- a/rhodecode/lib/middleware/simplevcs.py +++ b/rhodecode/lib/middleware/simplevcs.py @@ -360,12 +360,15 @@ class SimpleVCS(object): # try to auth based on environ, container auth methods log.debug('Running PRE-AUTH for container based authentication') pre_auth = authenticate( - '', '', environ, VCS_TYPE, registry=self.registry) + '', '', environ, VCS_TYPE, registry=self.registry, + acl_repo_name=self.acl_repo_name) if pre_auth and pre_auth.get('username'): username = pre_auth['username'] log.debug('PRE-AUTH got %s as username', username) # If not authenticated by the container, running basic auth + # before inject the calling repo_name for special scope checks + self.authenticate.acl_repo_name = self.acl_repo_name if not username: self.authenticate.realm = get_rhodecode_realm() diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -623,7 +623,7 @@ class User(Base, BaseModel): UserApiKeys.role == UserApiKeys.ROLE_ALL)) return tokens.all() - def authenticate_by_token(self, auth_token, roles=None): + def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None): from rhodecode.lib import auth log.debug('Trying to authenticate user: %s via auth-token, ' @@ -646,6 +646,17 @@ class User(Base, BaseModel): hash_tokens = [] for token in tokens_q.all(): + # verify scope first + if token.repo_id: + # token has a scope, we need to verify it + if scope_repo_id != token.repo_id: + log.debug( + 'Scope mismatch: token has a set repo scope: %s, ' + 'and calling scope is:%s, skipping further checks', + token.repo, scope_repo_id) + # token has a scope, and it doesn't match, skip token + continue + if token.api_key.startswith(crypto_backend.ENC_PREF): hash_tokens.append(token.api_key) else: @@ -656,7 +667,7 @@ class User(Base, BaseModel): return True for hashed in hash_tokens: - # marcink: this is expensive to calculate, but the most secure + # TODO(marcink): this is expensive to calculate, but most secure match = crypto_backend.hash_check(auth_token, hashed) if match: return True diff --git a/rhodecode/tests/other/vcs_operations/conftest.py b/rhodecode/tests/other/vcs_operations/conftest.py --- a/rhodecode/tests/other/vcs_operations/conftest.py +++ b/rhodecode/tests/other/vcs_operations/conftest.py @@ -132,11 +132,14 @@ def repos(request, pylonsapp): @pytest.fixture(scope="module") -def rc_web_server_config(pylons_config): +def rc_web_server_config(testini_factory): """ Configuration file used for the fixture `rc_web_server`. """ - return pylons_config + CUSTOM_PARAMS = [ + {'handler_console': {'level': 'DEBUG'}}, + ] + return testini_factory(CUSTOM_PARAMS) @pytest.fixture(scope="module") @@ -150,7 +153,8 @@ def rc_web_server( env = os.environ.copy() env['RC_NO_TMP_PATH'] = '1' - server_out = open(RC_LOG, 'w') + rc_log = RC_LOG + server_out = open(rc_log, 'w') # TODO: Would be great to capture the output and err of the subprocess # and make it available in a section of the py.test report in case of an @@ -158,11 +162,11 @@ def rc_web_server( host_url = 'http://' + get_host_url(rc_web_server_config) assert_no_running_instance(host_url) - command = ['rcserver', rc_web_server_config] + command = ['pserve', rc_web_server_config] print('Starting rcserver: {}'.format(host_url)) print('Command: {}'.format(command)) - print('Logfile: {}'.format(RC_LOG)) + print('Logfile: {}'.format(rc_log)) proc = subprocess32.Popen( command, bufsize=0, env=env, stdout=server_out, stderr=server_out) @@ -173,8 +177,9 @@ def rc_web_server( def stop_web_server(): # TODO: Find out how to integrate with the reporting of py.test to # make this information available. - print "\nServer log file written to %s" % (RC_LOG, ) + print("\nServer log file written to %s" % (rc_log, )) proc.kill() + server_out.flush() server_out.close() return RcWebServer(rc_web_server_config) @@ -210,12 +215,17 @@ def enable_auth_plugins(request, pylonsa override = override or {} params = { 'auth_plugins': ','.join(plugins_list), - 'csrf_token': csrf_token, + } + + # helper translate some names to others + name_map = { + 'token': 'authtoken' } for module in plugins_list: - plugin = rhodecode.authentication.base.loadplugin(module) - plugin_name = plugin.name + plugin_name = module.partition('#')[-1] + if plugin_name in name_map: + plugin_name = name_map[plugin_name] enabled_plugin = 'auth_%s_enabled' % plugin_name cache_ttl = 'auth_%s_cache_ttl' % plugin_name diff --git a/rhodecode/tests/other/vcs_operations/test_vcs_operations.py b/rhodecode/tests/other/vcs_operations/test_vcs_operations.py --- a/rhodecode/tests/other/vcs_operations/test_vcs_operations.py +++ b/rhodecode/tests/other/vcs_operations/test_vcs_operations.py @@ -35,6 +35,7 @@ import pytest from rhodecode.lib.vcs.backends.git.repository import GitRepository from rhodecode.lib.vcs.nodes import FileNode +from rhodecode.model.auth_token import AuthTokenModel from rhodecode.model.db import Repository, UserIpMap, CacheKey from rhodecode.model.meta import Session from rhodecode.model.user import UserModel @@ -46,7 +47,7 @@ from rhodecode.tests.other.vcs_operation @pytest.mark.usefixtures("disable_locking") -class TestVCSOperations: +class TestVCSOperations(object): def test_clone_hg_repo_by_admin(self, rc_web_server, tmpdir): clone_url = rc_web_server.repo_clone_url(HG_REPO) @@ -322,6 +323,115 @@ class TestVCSOperations: cmd.assert_returncode_success() _check_proper_clone(stdout, stderr, 'git') + def test_clone_by_auth_token( + self, rc_web_server, tmpdir, user_util, enable_auth_plugins): + enable_auth_plugins(['egg:rhodecode-enterprise-ce#token', + 'egg:rhodecode-enterprise-ce#rhodecode']) + + user = user_util.create_user() + token = user.auth_tokens[1] + + clone_url = rc_web_server.repo_clone_url( + HG_REPO, user=user.username, passwd=token) + + stdout, stderr = Command('/tmp').execute( + 'hg clone', clone_url, tmpdir.strpath) + _check_proper_clone(stdout, stderr, 'hg') + + def test_clone_by_auth_token_expired( + self, rc_web_server, tmpdir, user_util, enable_auth_plugins): + enable_auth_plugins(['egg:rhodecode-enterprise-ce#token', + 'egg:rhodecode-enterprise-ce#rhodecode']) + + user = user_util.create_user() + auth_token = AuthTokenModel().create( + user.user_id, 'test-token', -10, AuthTokenModel.cls.ROLE_VCS) + token = auth_token.api_key + + clone_url = rc_web_server.repo_clone_url( + HG_REPO, user=user.username, passwd=token) + + stdout, stderr = Command('/tmp').execute( + 'hg clone', clone_url, tmpdir.strpath) + assert 'abort: authorization failed' in stderr + + def test_clone_by_auth_token_bad_role( + self, rc_web_server, tmpdir, user_util, enable_auth_plugins): + enable_auth_plugins(['egg:rhodecode-enterprise-ce#token', + 'egg:rhodecode-enterprise-ce#rhodecode']) + + user = user_util.create_user() + auth_token = AuthTokenModel().create( + user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API) + token = auth_token.api_key + + clone_url = rc_web_server.repo_clone_url( + HG_REPO, user=user.username, passwd=token) + + stdout, stderr = Command('/tmp').execute( + 'hg clone', clone_url, tmpdir.strpath) + assert 'abort: authorization failed' in stderr + + def test_clone_by_auth_token_user_disabled( + self, rc_web_server, tmpdir, user_util, enable_auth_plugins): + enable_auth_plugins(['egg:rhodecode-enterprise-ce#token', + 'egg:rhodecode-enterprise-ce#rhodecode']) + user = user_util.create_user() + user.active = False + Session().add(user) + Session().commit() + token = user.auth_tokens[1] + + clone_url = rc_web_server.repo_clone_url( + HG_REPO, user=user.username, passwd=token) + + stdout, stderr = Command('/tmp').execute( + 'hg clone', clone_url, tmpdir.strpath) + assert 'abort: authorization failed' in stderr + + + def test_clone_by_auth_token_with_scope( + self, rc_web_server, tmpdir, user_util, enable_auth_plugins): + enable_auth_plugins(['egg:rhodecode-enterprise-ce#token', + 'egg:rhodecode-enterprise-ce#rhodecode']) + user = user_util.create_user() + auth_token = AuthTokenModel().create( + user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_VCS) + token = auth_token.api_key + + # manually set scope + auth_token.repo = Repository.get_by_repo_name(HG_REPO) + Session().add(auth_token) + Session().commit() + + clone_url = rc_web_server.repo_clone_url( + HG_REPO, user=user.username, passwd=token) + + stdout, stderr = Command('/tmp').execute( + 'hg clone', clone_url, tmpdir.strpath) + _check_proper_clone(stdout, stderr, 'hg') + + def test_clone_by_auth_token_with_wrong_scope( + self, rc_web_server, tmpdir, user_util, enable_auth_plugins): + enable_auth_plugins(['egg:rhodecode-enterprise-ce#token', + 'egg:rhodecode-enterprise-ce#rhodecode']) + user = user_util.create_user() + auth_token = AuthTokenModel().create( + user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_VCS) + token = auth_token.api_key + + # manually set scope + auth_token.repo = Repository.get_by_repo_name(GIT_REPO) + Session().add(auth_token) + Session().commit() + + clone_url = rc_web_server.repo_clone_url( + HG_REPO, user=user.username, passwd=token) + + stdout, stderr = Command('/tmp').execute( + 'hg clone', clone_url, tmpdir.strpath) + assert 'abort: authorization failed' in stderr + def test_git_sets_default_branch_if_not_master( backend_git, tmpdir, disable_locking, rc_web_server):