Show More
@@ -91,6 +91,11 b' class RhodeCodeAuthPluginBase(object):' | |||||
91 | # set on authenticate() method and via set_auth_type func. |
|
91 | # set on authenticate() method and via set_auth_type func. | |
92 | auth_type = None |
|
92 | auth_type = None | |
93 |
|
93 | |||
|
94 | # set on authenticate() method and via set_calling_scope_repo, this is a | |||
|
95 | # calling scope repository when doing authentication most likely on VCS | |||
|
96 | # operations | |||
|
97 | acl_repo_name = None | |||
|
98 | ||||
94 | # List of setting names to store encrypted. Plugins may override this list |
|
99 | # List of setting names to store encrypted. Plugins may override this list | |
95 | # to store settings encrypted. |
|
100 | # to store settings encrypted. | |
96 | _settings_encrypted = [] |
|
101 | _settings_encrypted = [] | |
@@ -268,6 +273,9 b' class RhodeCodeAuthPluginBase(object):' | |||||
268 | def set_auth_type(self, auth_type): |
|
273 | def set_auth_type(self, auth_type): | |
269 | self.auth_type = auth_type |
|
274 | self.auth_type = auth_type | |
270 |
|
275 | |||
|
276 | def set_calling_scope_repo(self, acl_repo_name): | |||
|
277 | self.acl_repo_name = acl_repo_name | |||
|
278 | ||||
271 | def allows_authentication_from( |
|
279 | def allows_authentication_from( | |
272 | self, user, allows_non_existing_user=True, |
|
280 | self, user, allows_non_existing_user=True, | |
273 | allowed_auth_plugins=None, allowed_auth_sources=None): |
|
281 | allowed_auth_plugins=None, allowed_auth_sources=None): | |
@@ -520,7 +528,7 b' def get_auth_cache_manager(custom_ttl=No' | |||||
520 |
|
528 | |||
521 |
|
529 | |||
522 | def authenticate(username, password, environ=None, auth_type=None, |
|
530 | def authenticate(username, password, environ=None, auth_type=None, | |
523 | skip_missing=False, registry=None): |
|
531 | skip_missing=False, registry=None, acl_repo_name=None): | |
524 | """ |
|
532 | """ | |
525 | Authentication function used for access control, |
|
533 | Authentication function used for access control, | |
526 | It tries to authenticate based on enabled authentication modules. |
|
534 | It tries to authenticate based on enabled authentication modules. | |
@@ -540,6 +548,7 b' def authenticate(username, password, env' | |||||
540 | authn_registry = get_authn_registry(registry) |
|
548 | authn_registry = get_authn_registry(registry) | |
541 | for plugin in authn_registry.get_plugins_for_authentication(): |
|
549 | for plugin in authn_registry.get_plugins_for_authentication(): | |
542 | plugin.set_auth_type(auth_type) |
|
550 | plugin.set_auth_type(auth_type) | |
|
551 | plugin.set_calling_scope_repo(acl_repo_name) | |||
543 | user = plugin.get_user(username) |
|
552 | user = plugin.get_user(username) | |
544 | display_user = user.username if user else username |
|
553 | display_user = user.username if user else username | |
545 |
|
554 |
@@ -28,7 +28,7 b' from rhodecode.translation import _' | |||||
28 | from rhodecode.authentication.base import ( |
|
28 | from rhodecode.authentication.base import ( | |
29 | RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property) |
|
29 | RhodeCodeAuthPluginBase, VCS_TYPE, hybrid_property) | |
30 | from rhodecode.authentication.routes import AuthnPluginResourceBase |
|
30 | from rhodecode.authentication.routes import AuthnPluginResourceBase | |
31 | from rhodecode.model.db import User, UserApiKeys |
|
31 | from rhodecode.model.db import User, UserApiKeys, Repository | |
32 |
|
32 | |||
33 |
|
33 | |||
34 | log = logging.getLogger(__name__) |
|
34 | log = logging.getLogger(__name__) | |
@@ -121,8 +121,15 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP' | |||||
121 |
|
121 | |||
122 | log.debug('Authenticating user with args %s', user_attrs) |
|
122 | log.debug('Authenticating user with args %s', user_attrs) | |
123 | if userobj.active: |
|
123 | if userobj.active: | |
|
124 | # calling context repo for token scopes | |||
|
125 | scope_repo_id = None | |||
|
126 | if self.acl_repo_name: | |||
|
127 | repo = Repository.get_by_repo_name(self.acl_repo_name) | |||
|
128 | scope_repo_id = repo.repo_id if repo else None | |||
|
129 | ||||
124 | token_match = userobj.authenticate_by_token( |
|
130 | token_match = userobj.authenticate_by_token( | |
125 |
password, roles=[UserApiKeys.ROLE_VCS] |
|
131 | password, roles=[UserApiKeys.ROLE_VCS], | |
|
132 | scope_repo_id=scope_repo_id) | |||
126 |
|
133 | |||
127 | if userobj.username == username and token_match: |
|
134 | if userobj.username == username and token_match: | |
128 | log.info( |
|
135 | log.info( |
@@ -209,11 +209,12 b' def vcs_operation_context(' | |||||
209 | class BasicAuth(AuthBasicAuthenticator): |
|
209 | class BasicAuth(AuthBasicAuthenticator): | |
210 |
|
210 | |||
211 | def __init__(self, realm, authfunc, registry, auth_http_code=None, |
|
211 | def __init__(self, realm, authfunc, registry, auth_http_code=None, | |
212 | initial_call_detection=False): |
|
212 | initial_call_detection=False, acl_repo_name=None): | |
213 | self.realm = realm |
|
213 | self.realm = realm | |
214 | self.initial_call = initial_call_detection |
|
214 | self.initial_call = initial_call_detection | |
215 | self.authfunc = authfunc |
|
215 | self.authfunc = authfunc | |
216 | self.registry = registry |
|
216 | self.registry = registry | |
|
217 | self.acl_repo_name = acl_repo_name | |||
217 | self._rc_auth_http_code = auth_http_code |
|
218 | self._rc_auth_http_code = auth_http_code | |
218 |
|
219 | |||
219 | def _get_response_from_code(self, http_code): |
|
220 | def _get_response_from_code(self, http_code): | |
@@ -247,7 +248,7 b' class BasicAuth(AuthBasicAuthenticator):' | |||||
247 | username, password = _parts |
|
248 | username, password = _parts | |
248 | if self.authfunc( |
|
249 | if self.authfunc( | |
249 | username, password, environ, VCS_TYPE, |
|
250 | username, password, environ, VCS_TYPE, | |
250 | registry=self.registry): |
|
251 | registry=self.registry, acl_repo_name=self.acl_repo_name): | |
251 | return username |
|
252 | return username | |
252 | if username and password: |
|
253 | if username and password: | |
253 | # we mark that we actually executed authentication once, at |
|
254 | # we mark that we actually executed authentication once, at |
@@ -360,12 +360,15 b' class SimpleVCS(object):' | |||||
360 | # try to auth based on environ, container auth methods |
|
360 | # try to auth based on environ, container auth methods | |
361 | log.debug('Running PRE-AUTH for container based authentication') |
|
361 | log.debug('Running PRE-AUTH for container based authentication') | |
362 | pre_auth = authenticate( |
|
362 | pre_auth = authenticate( | |
363 |
'', '', environ, VCS_TYPE, registry=self.registry |
|
363 | '', '', environ, VCS_TYPE, registry=self.registry, | |
|
364 | acl_repo_name=self.acl_repo_name) | |||
364 | if pre_auth and pre_auth.get('username'): |
|
365 | if pre_auth and pre_auth.get('username'): | |
365 | username = pre_auth['username'] |
|
366 | username = pre_auth['username'] | |
366 | log.debug('PRE-AUTH got %s as username', username) |
|
367 | log.debug('PRE-AUTH got %s as username', username) | |
367 |
|
368 | |||
368 | # If not authenticated by the container, running basic auth |
|
369 | # If not authenticated by the container, running basic auth | |
|
370 | # before inject the calling repo_name for special scope checks | |||
|
371 | self.authenticate.acl_repo_name = self.acl_repo_name | |||
369 | if not username: |
|
372 | if not username: | |
370 | self.authenticate.realm = get_rhodecode_realm() |
|
373 | self.authenticate.realm = get_rhodecode_realm() | |
371 |
|
374 |
@@ -623,7 +623,7 b' class User(Base, BaseModel):' | |||||
623 | UserApiKeys.role == UserApiKeys.ROLE_ALL)) |
|
623 | UserApiKeys.role == UserApiKeys.ROLE_ALL)) | |
624 | return tokens.all() |
|
624 | return tokens.all() | |
625 |
|
625 | |||
626 | def authenticate_by_token(self, auth_token, roles=None): |
|
626 | def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None): | |
627 | from rhodecode.lib import auth |
|
627 | from rhodecode.lib import auth | |
628 |
|
628 | |||
629 | log.debug('Trying to authenticate user: %s via auth-token, ' |
|
629 | log.debug('Trying to authenticate user: %s via auth-token, ' | |
@@ -646,6 +646,17 b' class User(Base, BaseModel):' | |||||
646 | hash_tokens = [] |
|
646 | hash_tokens = [] | |
647 |
|
647 | |||
648 | for token in tokens_q.all(): |
|
648 | for token in tokens_q.all(): | |
|
649 | # verify scope first | |||
|
650 | if token.repo_id: | |||
|
651 | # token has a scope, we need to verify it | |||
|
652 | if scope_repo_id != token.repo_id: | |||
|
653 | log.debug( | |||
|
654 | 'Scope mismatch: token has a set repo scope: %s, ' | |||
|
655 | 'and calling scope is:%s, skipping further checks', | |||
|
656 | token.repo, scope_repo_id) | |||
|
657 | # token has a scope, and it doesn't match, skip token | |||
|
658 | continue | |||
|
659 | ||||
649 | if token.api_key.startswith(crypto_backend.ENC_PREF): |
|
660 | if token.api_key.startswith(crypto_backend.ENC_PREF): | |
650 | hash_tokens.append(token.api_key) |
|
661 | hash_tokens.append(token.api_key) | |
651 | else: |
|
662 | else: | |
@@ -656,7 +667,7 b' class User(Base, BaseModel):' | |||||
656 | return True |
|
667 | return True | |
657 |
|
668 | |||
658 | for hashed in hash_tokens: |
|
669 | for hashed in hash_tokens: | |
659 |
# marcink: this is expensive to calculate, but |
|
670 | # TODO(marcink): this is expensive to calculate, but most secure | |
660 | match = crypto_backend.hash_check(auth_token, hashed) |
|
671 | match = crypto_backend.hash_check(auth_token, hashed) | |
661 | if match: |
|
672 | if match: | |
662 | return True |
|
673 | return True |
@@ -132,11 +132,14 b' def repos(request, pylonsapp):' | |||||
132 |
|
132 | |||
133 |
|
133 | |||
134 | @pytest.fixture(scope="module") |
|
134 | @pytest.fixture(scope="module") | |
135 |
def rc_web_server_config( |
|
135 | def rc_web_server_config(testini_factory): | |
136 | """ |
|
136 | """ | |
137 | Configuration file used for the fixture `rc_web_server`. |
|
137 | Configuration file used for the fixture `rc_web_server`. | |
138 | """ |
|
138 | """ | |
139 | return pylons_config |
|
139 | CUSTOM_PARAMS = [ | |
|
140 | {'handler_console': {'level': 'DEBUG'}}, | |||
|
141 | ] | |||
|
142 | return testini_factory(CUSTOM_PARAMS) | |||
140 |
|
143 | |||
141 |
|
144 | |||
142 | @pytest.fixture(scope="module") |
|
145 | @pytest.fixture(scope="module") | |
@@ -150,7 +153,8 b' def rc_web_server(' | |||||
150 | env = os.environ.copy() |
|
153 | env = os.environ.copy() | |
151 | env['RC_NO_TMP_PATH'] = '1' |
|
154 | env['RC_NO_TMP_PATH'] = '1' | |
152 |
|
155 | |||
153 |
|
|
156 | rc_log = RC_LOG | |
|
157 | server_out = open(rc_log, 'w') | |||
154 |
|
158 | |||
155 | # TODO: Would be great to capture the output and err of the subprocess |
|
159 | # TODO: Would be great to capture the output and err of the subprocess | |
156 | # and make it available in a section of the py.test report in case of an |
|
160 | # and make it available in a section of the py.test report in case of an | |
@@ -158,11 +162,11 b' def rc_web_server(' | |||||
158 |
|
162 | |||
159 | host_url = 'http://' + get_host_url(rc_web_server_config) |
|
163 | host_url = 'http://' + get_host_url(rc_web_server_config) | |
160 | assert_no_running_instance(host_url) |
|
164 | assert_no_running_instance(host_url) | |
161 |
command = [' |
|
165 | command = ['pserve', rc_web_server_config] | |
162 |
|
166 | |||
163 | print('Starting rcserver: {}'.format(host_url)) |
|
167 | print('Starting rcserver: {}'.format(host_url)) | |
164 | print('Command: {}'.format(command)) |
|
168 | print('Command: {}'.format(command)) | |
165 |
print('Logfile: {}'.format( |
|
169 | print('Logfile: {}'.format(rc_log)) | |
166 |
|
170 | |||
167 | proc = subprocess32.Popen( |
|
171 | proc = subprocess32.Popen( | |
168 | command, bufsize=0, env=env, stdout=server_out, stderr=server_out) |
|
172 | command, bufsize=0, env=env, stdout=server_out, stderr=server_out) | |
@@ -173,8 +177,9 b' def rc_web_server(' | |||||
173 | def stop_web_server(): |
|
177 | def stop_web_server(): | |
174 | # TODO: Find out how to integrate with the reporting of py.test to |
|
178 | # TODO: Find out how to integrate with the reporting of py.test to | |
175 | # make this information available. |
|
179 | # make this information available. | |
176 |
print |
|
180 | print("\nServer log file written to %s" % (rc_log, )) | |
177 | proc.kill() |
|
181 | proc.kill() | |
|
182 | server_out.flush() | |||
178 | server_out.close() |
|
183 | server_out.close() | |
179 |
|
184 | |||
180 | return RcWebServer(rc_web_server_config) |
|
185 | return RcWebServer(rc_web_server_config) | |
@@ -210,12 +215,17 b' def enable_auth_plugins(request, pylonsa' | |||||
210 | override = override or {} |
|
215 | override = override or {} | |
211 | params = { |
|
216 | params = { | |
212 | 'auth_plugins': ','.join(plugins_list), |
|
217 | 'auth_plugins': ','.join(plugins_list), | |
213 | 'csrf_token': csrf_token, |
|
218 | } | |
|
219 | ||||
|
220 | # helper translate some names to others | |||
|
221 | name_map = { | |||
|
222 | 'token': 'authtoken' | |||
214 | } |
|
223 | } | |
215 |
|
224 | |||
216 | for module in plugins_list: |
|
225 | for module in plugins_list: | |
217 | plugin = rhodecode.authentication.base.loadplugin(module) |
|
226 | plugin_name = module.partition('#')[-1] | |
218 |
plugin_name |
|
227 | if plugin_name in name_map: | |
|
228 | plugin_name = name_map[plugin_name] | |||
219 | enabled_plugin = 'auth_%s_enabled' % plugin_name |
|
229 | enabled_plugin = 'auth_%s_enabled' % plugin_name | |
220 | cache_ttl = 'auth_%s_cache_ttl' % plugin_name |
|
230 | cache_ttl = 'auth_%s_cache_ttl' % plugin_name | |
221 |
|
231 |
@@ -35,6 +35,7 b' import pytest' | |||||
35 |
|
35 | |||
36 | from rhodecode.lib.vcs.backends.git.repository import GitRepository |
|
36 | from rhodecode.lib.vcs.backends.git.repository import GitRepository | |
37 | from rhodecode.lib.vcs.nodes import FileNode |
|
37 | from rhodecode.lib.vcs.nodes import FileNode | |
|
38 | from rhodecode.model.auth_token import AuthTokenModel | |||
38 | from rhodecode.model.db import Repository, UserIpMap, CacheKey |
|
39 | from rhodecode.model.db import Repository, UserIpMap, CacheKey | |
39 | from rhodecode.model.meta import Session |
|
40 | from rhodecode.model.meta import Session | |
40 | from rhodecode.model.user import UserModel |
|
41 | from rhodecode.model.user import UserModel | |
@@ -46,7 +47,7 b' from rhodecode.tests.other.vcs_operation' | |||||
46 |
|
47 | |||
47 |
|
48 | |||
48 | @pytest.mark.usefixtures("disable_locking") |
|
49 | @pytest.mark.usefixtures("disable_locking") | |
49 | class TestVCSOperations: |
|
50 | class TestVCSOperations(object): | |
50 |
|
51 | |||
51 | def test_clone_hg_repo_by_admin(self, rc_web_server, tmpdir): |
|
52 | def test_clone_hg_repo_by_admin(self, rc_web_server, tmpdir): | |
52 | clone_url = rc_web_server.repo_clone_url(HG_REPO) |
|
53 | clone_url = rc_web_server.repo_clone_url(HG_REPO) | |
@@ -322,6 +323,115 b' class TestVCSOperations:' | |||||
322 | cmd.assert_returncode_success() |
|
323 | cmd.assert_returncode_success() | |
323 | _check_proper_clone(stdout, stderr, 'git') |
|
324 | _check_proper_clone(stdout, stderr, 'git') | |
324 |
|
325 | |||
|
326 | def test_clone_by_auth_token( | |||
|
327 | self, rc_web_server, tmpdir, user_util, enable_auth_plugins): | |||
|
328 | enable_auth_plugins(['egg:rhodecode-enterprise-ce#token', | |||
|
329 | 'egg:rhodecode-enterprise-ce#rhodecode']) | |||
|
330 | ||||
|
331 | user = user_util.create_user() | |||
|
332 | token = user.auth_tokens[1] | |||
|
333 | ||||
|
334 | clone_url = rc_web_server.repo_clone_url( | |||
|
335 | HG_REPO, user=user.username, passwd=token) | |||
|
336 | ||||
|
337 | stdout, stderr = Command('/tmp').execute( | |||
|
338 | 'hg clone', clone_url, tmpdir.strpath) | |||
|
339 | _check_proper_clone(stdout, stderr, 'hg') | |||
|
340 | ||||
|
341 | def test_clone_by_auth_token_expired( | |||
|
342 | self, rc_web_server, tmpdir, user_util, enable_auth_plugins): | |||
|
343 | enable_auth_plugins(['egg:rhodecode-enterprise-ce#token', | |||
|
344 | 'egg:rhodecode-enterprise-ce#rhodecode']) | |||
|
345 | ||||
|
346 | user = user_util.create_user() | |||
|
347 | auth_token = AuthTokenModel().create( | |||
|
348 | user.user_id, 'test-token', -10, AuthTokenModel.cls.ROLE_VCS) | |||
|
349 | token = auth_token.api_key | |||
|
350 | ||||
|
351 | clone_url = rc_web_server.repo_clone_url( | |||
|
352 | HG_REPO, user=user.username, passwd=token) | |||
|
353 | ||||
|
354 | stdout, stderr = Command('/tmp').execute( | |||
|
355 | 'hg clone', clone_url, tmpdir.strpath) | |||
|
356 | assert 'abort: authorization failed' in stderr | |||
|
357 | ||||
|
358 | def test_clone_by_auth_token_bad_role( | |||
|
359 | self, rc_web_server, tmpdir, user_util, enable_auth_plugins): | |||
|
360 | enable_auth_plugins(['egg:rhodecode-enterprise-ce#token', | |||
|
361 | 'egg:rhodecode-enterprise-ce#rhodecode']) | |||
|
362 | ||||
|
363 | user = user_util.create_user() | |||
|
364 | auth_token = AuthTokenModel().create( | |||
|
365 | user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API) | |||
|
366 | token = auth_token.api_key | |||
|
367 | ||||
|
368 | clone_url = rc_web_server.repo_clone_url( | |||
|
369 | HG_REPO, user=user.username, passwd=token) | |||
|
370 | ||||
|
371 | stdout, stderr = Command('/tmp').execute( | |||
|
372 | 'hg clone', clone_url, tmpdir.strpath) | |||
|
373 | assert 'abort: authorization failed' in stderr | |||
|
374 | ||||
|
375 | def test_clone_by_auth_token_user_disabled( | |||
|
376 | self, rc_web_server, tmpdir, user_util, enable_auth_plugins): | |||
|
377 | enable_auth_plugins(['egg:rhodecode-enterprise-ce#token', | |||
|
378 | 'egg:rhodecode-enterprise-ce#rhodecode']) | |||
|
379 | user = user_util.create_user() | |||
|
380 | user.active = False | |||
|
381 | Session().add(user) | |||
|
382 | Session().commit() | |||
|
383 | token = user.auth_tokens[1] | |||
|
384 | ||||
|
385 | clone_url = rc_web_server.repo_clone_url( | |||
|
386 | HG_REPO, user=user.username, passwd=token) | |||
|
387 | ||||
|
388 | stdout, stderr = Command('/tmp').execute( | |||
|
389 | 'hg clone', clone_url, tmpdir.strpath) | |||
|
390 | assert 'abort: authorization failed' in stderr | |||
|
391 | ||||
|
392 | ||||
|
393 | def test_clone_by_auth_token_with_scope( | |||
|
394 | self, rc_web_server, tmpdir, user_util, enable_auth_plugins): | |||
|
395 | enable_auth_plugins(['egg:rhodecode-enterprise-ce#token', | |||
|
396 | 'egg:rhodecode-enterprise-ce#rhodecode']) | |||
|
397 | user = user_util.create_user() | |||
|
398 | auth_token = AuthTokenModel().create( | |||
|
399 | user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_VCS) | |||
|
400 | token = auth_token.api_key | |||
|
401 | ||||
|
402 | # manually set scope | |||
|
403 | auth_token.repo = Repository.get_by_repo_name(HG_REPO) | |||
|
404 | Session().add(auth_token) | |||
|
405 | Session().commit() | |||
|
406 | ||||
|
407 | clone_url = rc_web_server.repo_clone_url( | |||
|
408 | HG_REPO, user=user.username, passwd=token) | |||
|
409 | ||||
|
410 | stdout, stderr = Command('/tmp').execute( | |||
|
411 | 'hg clone', clone_url, tmpdir.strpath) | |||
|
412 | _check_proper_clone(stdout, stderr, 'hg') | |||
|
413 | ||||
|
414 | def test_clone_by_auth_token_with_wrong_scope( | |||
|
415 | self, rc_web_server, tmpdir, user_util, enable_auth_plugins): | |||
|
416 | enable_auth_plugins(['egg:rhodecode-enterprise-ce#token', | |||
|
417 | 'egg:rhodecode-enterprise-ce#rhodecode']) | |||
|
418 | user = user_util.create_user() | |||
|
419 | auth_token = AuthTokenModel().create( | |||
|
420 | user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_VCS) | |||
|
421 | token = auth_token.api_key | |||
|
422 | ||||
|
423 | # manually set scope | |||
|
424 | auth_token.repo = Repository.get_by_repo_name(GIT_REPO) | |||
|
425 | Session().add(auth_token) | |||
|
426 | Session().commit() | |||
|
427 | ||||
|
428 | clone_url = rc_web_server.repo_clone_url( | |||
|
429 | HG_REPO, user=user.username, passwd=token) | |||
|
430 | ||||
|
431 | stdout, stderr = Command('/tmp').execute( | |||
|
432 | 'hg clone', clone_url, tmpdir.strpath) | |||
|
433 | assert 'abort: authorization failed' in stderr | |||
|
434 | ||||
325 |
|
435 | |||
326 | def test_git_sets_default_branch_if_not_master( |
|
436 | def test_git_sets_default_branch_if_not_master( | |
327 | backend_git, tmpdir, disable_locking, rc_web_server): |
|
437 | backend_git, tmpdir, disable_locking, rc_web_server): |
General Comments 0
You need to be logged in to leave comments.
Login now