Show More
@@ -209,18 +209,18 b' def request_view(request):' | |||
|
209 | 209 | request.rpc_user = auth_u |
|
210 | 210 | |
|
211 | 211 | # now check if token is valid for API |
|
212 | role = UserApiKeys.ROLE_API | |
|
213 | extra_auth_tokens = [ | |
|
214 | x.api_key for x in User.extra_valid_auth_tokens(api_user, role=role)] | |
|
215 | active_tokens = [api_user.api_key] + extra_auth_tokens | |
|
212 | auth_token = request.rpc_api_key | |
|
213 | token_match = api_user.authenticate_by_token( | |
|
214 | auth_token, roles=[UserApiKeys.ROLE_API], include_builtin_token=True) | |
|
215 | invalid_token = not token_match | |
|
216 | 216 | |
|
217 |
log.debug('Checking if API |
|
|
218 | if request.rpc_api_key not in active_tokens: | |
|
217 | log.debug('Checking if API KEY is valid with proper role') | |
|
218 | if invalid_token: | |
|
219 | 219 | return jsonrpc_error( |
|
220 | 220 | request, retid=request.rpc_id, |
|
221 | message='API KEY has bad role for an API call') | |
|
221 | message='API KEY invalid or, has bad role for an API call') | |
|
222 | 222 | |
|
223 |
except Exception |
|
|
223 | except Exception: | |
|
224 | 224 | log.exception('Error on API AUTH') |
|
225 | 225 | return jsonrpc_error( |
|
226 | 226 | request, retid=request.rpc_id, message='Invalid API KEY') |
@@ -122,10 +122,10 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP' | |||
|
122 | 122 | |
|
123 | 123 | log.debug('Authenticating user with args %s', user_attrs) |
|
124 | 124 | if userobj.active: |
|
125 | role = UserApiKeys.ROLE_VCS | |
|
126 | active_tokens = [x.api_key for x in | |
|
127 | User.extra_valid_auth_tokens(userobj, role=role)] | |
|
128 |
if userobj.username == username and |
|
|
125 | token_match = userobj.authenticate_by_token( | |
|
126 | password, roles=[UserApiKeys.ROLE_VCS]) | |
|
127 | ||
|
128 | if userobj.username == username and token_match: | |
|
129 | 129 | log.info( |
|
130 | 130 | 'user `%s` successfully authenticated via %s', |
|
131 | 131 | user_attrs['username'], self.name) |
@@ -28,12 +28,11 b' import pytz' | |||
|
28 | 28 | from pylons import url, response, tmpl_context as c |
|
29 | 29 | from pylons.i18n.translation import _ |
|
30 | 30 | |
|
31 |
from beaker.cache import cache_region |
|
|
31 | from beaker.cache import cache_region | |
|
32 | 32 | from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed |
|
33 | 33 | |
|
34 | from rhodecode.model.db import CacheKey | |
|
34 | from rhodecode.model.db import CacheKey, UserApiKeys | |
|
35 | 35 | from rhodecode.lib import helpers as h |
|
36 | from rhodecode.lib import caches | |
|
37 | 36 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator |
|
38 | 37 | from rhodecode.lib.base import BaseRepoController |
|
39 | 38 | from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer |
@@ -62,7 +61,7 b' class FeedController(BaseRepoController)' | |||
|
62 | 61 | safe_int(config.get('rss_cut_off_limit', 32 * 1024)), |
|
63 | 62 | } |
|
64 | 63 | |
|
65 |
@LoginRequired(auth_token_access= |
|
|
64 | @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED]) | |
|
66 | 65 | def __before__(self): |
|
67 | 66 | super(FeedController, self).__before__() |
|
68 | 67 | config = self._get_config() |
@@ -35,7 +35,7 b' from pylons import request, tmpl_context' | |||
|
35 | 35 | from pylons.i18n.translation import _ |
|
36 | 36 | |
|
37 | 37 | from rhodecode.controllers.admin.admin import _journal_filter |
|
38 | from rhodecode.model.db import UserLog, UserFollowing, User | |
|
38 | from rhodecode.model.db import UserLog, UserFollowing, User, UserApiKeys | |
|
39 | 39 | from rhodecode.model.meta import Session |
|
40 | 40 | import rhodecode.lib.helpers as h |
|
41 | 41 | from rhodecode.lib.helpers import Page |
@@ -211,7 +211,7 b' class JournalController(BaseController):' | |||
|
211 | 211 | |
|
212 | 212 | return render('journal/journal.mako') |
|
213 | 213 | |
|
214 |
@LoginRequired(auth_token_access= |
|
|
214 | @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED]) | |
|
215 | 215 | @NotAnonymous() |
|
216 | 216 | def journal_atom(self): |
|
217 | 217 | """ |
@@ -223,7 +223,7 b' class JournalController(BaseController):' | |||
|
223 | 223 | .all() |
|
224 | 224 | return self._atom_feed(following, public=False) |
|
225 | 225 | |
|
226 |
@LoginRequired(auth_token_access= |
|
|
226 | @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED]) | |
|
227 | 227 | @NotAnonymous() |
|
228 | 228 | def journal_rss(self): |
|
229 | 229 | """ |
@@ -281,7 +281,7 b' class JournalController(BaseController):' | |||
|
281 | 281 | return c.journal_data |
|
282 | 282 | return render('journal/public_journal.mako') |
|
283 | 283 | |
|
284 |
@LoginRequired(auth_token_access= |
|
|
284 | @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED]) | |
|
285 | 285 | def public_journal_atom(self): |
|
286 | 286 | """ |
|
287 | 287 | Produce an atom-1.0 feed via feedgenerator module |
@@ -293,7 +293,7 b' class JournalController(BaseController):' | |||
|
293 | 293 | |
|
294 | 294 | return self._atom_feed(c.following) |
|
295 | 295 | |
|
296 |
@LoginRequired(auth_token_access= |
|
|
296 | @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED]) | |
|
297 | 297 | def public_journal_rss(self): |
|
298 | 298 | """ |
|
299 | 299 | Produce an rss2 feed via feedgenerator module |
@@ -99,6 +99,7 b' class PasswordGenerator(object):' | |||
|
99 | 99 | |
|
100 | 100 | |
|
101 | 101 | class _RhodeCodeCryptoBase(object): |
|
102 | ENC_PREF = None | |
|
102 | 103 | |
|
103 | 104 | def hash_create(self, str_): |
|
104 | 105 | """ |
@@ -139,6 +140,7 b' class _RhodeCodeCryptoBase(object):' | |||
|
139 | 140 | |
|
140 | 141 | |
|
141 | 142 | class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase): |
|
143 | ENC_PREF = '$2a$10' | |
|
142 | 144 | |
|
143 | 145 | def hash_create(self, str_): |
|
144 | 146 | self._assert_bytes(str_) |
@@ -194,6 +196,7 b' class _RhodeCodeCryptoBCrypt(_RhodeCodeC' | |||
|
194 | 196 | |
|
195 | 197 | |
|
196 | 198 | class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase): |
|
199 | ENC_PREF = '_' | |
|
197 | 200 | |
|
198 | 201 | def hash_create(self, str_): |
|
199 | 202 | self._assert_bytes(str_) |
@@ -211,6 +214,7 b' class _RhodeCodeCryptoSha256(_RhodeCodeC' | |||
|
211 | 214 | |
|
212 | 215 | |
|
213 | 216 | class _RhodeCodeCryptoMd5(_RhodeCodeCryptoBase): |
|
217 | ENC_PREF = '_' | |
|
214 | 218 | |
|
215 | 219 | def hash_create(self, str_): |
|
216 | 220 | self._assert_bytes(str_) |
@@ -831,10 +835,6 b' class AuthUser(object):' | |||
|
831 | 835 | self._permissions_scoped_cache[cache_key] = res |
|
832 | 836 | return self._permissions_scoped_cache[cache_key] |
|
833 | 837 | |
|
834 | @property | |
|
835 | def auth_tokens(self): | |
|
836 | return self.get_auth_tokens() | |
|
837 | ||
|
838 | 838 | def get_instance(self): |
|
839 | 839 | return User.get(self.user_id) |
|
840 | 840 | |
@@ -925,16 +925,6 b' class AuthUser(object):' | |||
|
925 | 925 | log.debug('PERMISSION tree computed %s' % (result_repr,)) |
|
926 | 926 | return result |
|
927 | 927 | |
|
928 | def get_auth_tokens(self): | |
|
929 | auth_tokens = [self.api_key] | |
|
930 | for api_key in UserApiKeys.query()\ | |
|
931 | .filter(UserApiKeys.user_id == self.user_id)\ | |
|
932 | .filter(or_(UserApiKeys.expires == -1, | |
|
933 | UserApiKeys.expires >= time.time())).all(): | |
|
934 | auth_tokens.append(api_key.api_key) | |
|
935 | ||
|
936 | return auth_tokens | |
|
937 | ||
|
938 | 928 | @property |
|
939 | 929 | def is_default(self): |
|
940 | 930 | return self.username == User.DEFAULT_USER |
@@ -1171,7 +1161,7 b' class LoginRequired(object):' | |||
|
1171 | 1161 | :param api_access: if enabled this checks only for valid auth token |
|
1172 | 1162 | and grants access based on valid token |
|
1173 | 1163 | """ |
|
1174 |
def __init__(self, auth_token_access= |
|
|
1164 | def __init__(self, auth_token_access=None): | |
|
1175 | 1165 | self.auth_token_access = auth_token_access |
|
1176 | 1166 | |
|
1177 | 1167 | def __call__(self, func): |
@@ -1191,7 +1181,7 b' class LoginRequired(object):' | |||
|
1191 | 1181 | ip_access_valid = False |
|
1192 | 1182 | |
|
1193 | 1183 | # check if we used an APIKEY and it's a valid one |
|
1194 | # defined whitelist of controllers which API access will be enabled | |
|
1184 | # defined white-list of controllers which API access will be enabled | |
|
1195 | 1185 | _auth_token = request.GET.get( |
|
1196 | 1186 | 'auth_token', '') or request.GET.get('api_key', '') |
|
1197 | 1187 | auth_token_access_valid = allowed_auth_token_access( |
@@ -1200,8 +1190,20 b' class LoginRequired(object):' | |||
|
1200 | 1190 | # explicit controller is enabled or API is in our whitelist |
|
1201 | 1191 | if self.auth_token_access or auth_token_access_valid: |
|
1202 | 1192 | log.debug('Checking AUTH TOKEN access for %s' % (cls,)) |
|
1193 | db_user = user.get_instance() | |
|
1203 | 1194 | |
|
1204 | if _auth_token and _auth_token in user.auth_tokens: | |
|
1195 | if db_user: | |
|
1196 | if self.auth_token_access: | |
|
1197 | roles = self.auth_token_access | |
|
1198 | else: | |
|
1199 | roles = [UserApiKeys.ROLE_HTTP] | |
|
1200 | token_match = db_user.authenticate_by_token( | |
|
1201 | _auth_token, roles=roles, include_builtin_token=True) | |
|
1202 | else: | |
|
1203 | log.debug('Unable to fetch db instance for auth user: %s', user) | |
|
1204 | token_match = False | |
|
1205 | ||
|
1206 | if _auth_token and token_match: | |
|
1205 | 1207 | auth_token_access_valid = True |
|
1206 | 1208 | log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],)) |
|
1207 | 1209 | else: |
@@ -581,15 +581,16 b' class User(Base, BaseModel):' | |||
|
581 | 581 | |
|
582 | 582 | @property |
|
583 | 583 | def feed_token(self): |
|
584 | return self.get_feed_token() | |
|
585 | ||
|
586 | def get_feed_token(self): | |
|
584 | 587 | feed_tokens = UserApiKeys.query()\ |
|
585 | 588 | .filter(UserApiKeys.user == self)\ |
|
586 | 589 | .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\ |
|
587 | 590 | .all() |
|
588 | 591 | if feed_tokens: |
|
589 | 592 | return feed_tokens[0].api_key |
|
590 | else: | |
|
591 | # use the main token so we don't end up with nothing... | |
|
592 | return self.api_key | |
|
593 | return 'NO_FEED_TOKEN_AVAILABLE' | |
|
593 | 594 | |
|
594 | 595 | @classmethod |
|
595 | 596 | def extra_valid_auth_tokens(cls, user, role=None): |
@@ -601,11 +602,57 b' class User(Base, BaseModel):' | |||
|
601 | 602 | UserApiKeys.role == UserApiKeys.ROLE_ALL)) |
|
602 | 603 | return tokens.all() |
|
603 | 604 | |
|
605 | def authenticate_by_token(self, auth_token, roles=None, | |
|
606 | include_builtin_token=False): | |
|
607 | from rhodecode.lib import auth | |
|
608 | ||
|
609 | log.debug('Trying to authenticate user: %s via auth-token, ' | |
|
610 | 'and roles: %s', self, roles) | |
|
611 | ||
|
612 | if not auth_token: | |
|
613 | return False | |
|
614 | ||
|
615 | crypto_backend = auth.crypto_backend() | |
|
616 | ||
|
617 | roles = (roles or []) + [UserApiKeys.ROLE_ALL] | |
|
618 | tokens_q = UserApiKeys.query()\ | |
|
619 | .filter(UserApiKeys.user_id == self.user_id)\ | |
|
620 | .filter(or_(UserApiKeys.expires == -1, | |
|
621 | UserApiKeys.expires >= time.time())) | |
|
622 | ||
|
623 | tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles)) | |
|
624 | ||
|
625 | maybe_builtin = [] | |
|
626 | if include_builtin_token: | |
|
627 | maybe_builtin = [AttributeDict({'api_key': self.api_key})] | |
|
628 | ||
|
629 | plain_tokens = [] | |
|
630 | hash_tokens = [] | |
|
631 | ||
|
632 | for token in tokens_q.all() + maybe_builtin: | |
|
633 | if token.api_key.startswith(crypto_backend.ENC_PREF): | |
|
634 | hash_tokens.append(token.api_key) | |
|
635 | else: | |
|
636 | plain_tokens.append(token.api_key) | |
|
637 | ||
|
638 | is_plain_match = auth_token in plain_tokens | |
|
639 | if is_plain_match: | |
|
640 | return True | |
|
641 | ||
|
642 | for hashed in hash_tokens: | |
|
643 | # marcink: this is expensive to calculate, but the most secure | |
|
644 | match = crypto_backend.hash_check(auth_token, hashed) | |
|
645 | if match: | |
|
646 | return True | |
|
647 | ||
|
648 | return False | |
|
649 | ||
|
604 | 650 | @property |
|
605 | 651 | def builtin_token_roles(self): |
|
606 | return map(UserApiKeys._get_role_name, [ | |
|
652 | roles = [ | |
|
607 | 653 | UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP |
|
608 |
] |
|
|
654 | ] | |
|
655 | return map(UserApiKeys._get_role_name, roles) | |
|
609 | 656 | |
|
610 | 657 | @property |
|
611 | 658 | def ip_addresses(self): |
@@ -17,7 +17,7 b'' | |||
|
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 | from rhodecode.model.auth_token import AuthTokenModel | |
|
21 | 21 | from rhodecode.model.db import User |
|
22 | 22 | from rhodecode.tests import * |
|
23 | 23 | |
@@ -32,15 +32,36 b' class TestFeedController(TestController)' | |||
|
32 | 32 | assert response.content_type == "application/rss+xml" |
|
33 | 33 | assert """<rss version="2.0">""" in response |
|
34 | 34 | |
|
35 | def test_rss_with_auth_token(self, backend): | |
|
36 |
auth_token = |
|
|
35 | def test_rss_with_auth_token(self, backend, user_admin): | |
|
36 | auth_token = user_admin.feed_token | |
|
37 | 37 | assert auth_token != '' |
|
38 |
response = self.app.get( |
|
|
39 | repo_name=backend.repo_name, auth_token=auth_token)) | |
|
38 | response = self.app.get( | |
|
39 | url(controller='feed', action='rss', | |
|
40 | repo_name=backend.repo_name, auth_token=auth_token, | |
|
41 | status=200)) | |
|
40 | 42 | |
|
41 | 43 | assert response.content_type == "application/rss+xml" |
|
42 | 44 | assert """<rss version="2.0">""" in response |
|
43 | 45 | |
|
46 | def test_rss_with_auth_token_of_wrong_type(self, backend, user_util): | |
|
47 | user = user_util.create_user() | |
|
48 | auth_token = AuthTokenModel().create( | |
|
49 | user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API) | |
|
50 | auth_token = auth_token.api_key | |
|
51 | ||
|
52 | self.app.get( | |
|
53 | url(controller='feed', action='rss', | |
|
54 | repo_name=backend.repo_name, auth_token=auth_token), | |
|
55 | status=302) | |
|
56 | ||
|
57 | auth_token = AuthTokenModel().create( | |
|
58 | user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_FEED) | |
|
59 | auth_token = auth_token.api_key | |
|
60 | self.app.get( | |
|
61 | url(controller='feed', action='rss', | |
|
62 | repo_name=backend.repo_name, auth_token=auth_token), | |
|
63 | status=200) | |
|
64 | ||
|
44 | 65 | def test_atom(self, backend): |
|
45 | 66 | self.log_user() |
|
46 | 67 | response = self.app.get(url(controller='feed', action='atom', |
@@ -443,14 +443,15 b' class TestLoginController:' | |||
|
443 | 443 | ('fake_number', '123456'), |
|
444 | 444 | ('proper_auth_token', None) |
|
445 | 445 | ]) |
|
446 |
def test_access_not_whitelisted_page_via_auth_token( |
|
|
447 | auth_token): | |
|
446 | def test_access_not_whitelisted_page_via_auth_token( | |
|
447 | self, test_name, auth_token, user_admin): | |
|
448 | ||
|
448 | 449 | whitelist = self._get_api_whitelist([]) |
|
449 | 450 | with mock.patch.dict('rhodecode.CONFIG', whitelist): |
|
450 | 451 | assert [] == whitelist['api_access_controllers_whitelist'] |
|
451 | 452 | if test_name == 'proper_auth_token': |
|
452 | 453 | # use builtin if api_key is None |
|
453 |
auth_token = |
|
|
454 | auth_token = user_admin.api_key | |
|
454 | 455 | |
|
455 | 456 | with fixture.anon_access(False): |
|
456 | 457 | self.app.get(url(controller='changeset', |
@@ -465,15 +466,17 b' class TestLoginController:' | |||
|
465 | 466 | ('fake_number', '123456', 302), |
|
466 | 467 | ('proper_auth_token', None, 200) |
|
467 | 468 | ]) |
|
468 |
def test_access_whitelisted_page_via_auth_token( |
|
|
469 | auth_token, code): | |
|
470 | whitelist = self._get_api_whitelist( | |
|
471 |
|
|
|
469 | def test_access_whitelisted_page_via_auth_token( | |
|
470 | self, test_name, auth_token, code, user_admin): | |
|
471 | ||
|
472 | whitelist_entry = ['ChangesetController:changeset_raw'] | |
|
473 | whitelist = self._get_api_whitelist(whitelist_entry) | |
|
474 | ||
|
472 | 475 | with mock.patch.dict('rhodecode.CONFIG', whitelist): |
|
473 | assert ['ChangesetController:changeset_raw'] == \ | |
|
474 | whitelist['api_access_controllers_whitelist'] | |
|
476 | assert whitelist_entry == whitelist['api_access_controllers_whitelist'] | |
|
477 | ||
|
475 | 478 | if test_name == 'proper_auth_token': |
|
476 |
auth_token = |
|
|
479 | auth_token = user_admin.api_key | |
|
477 | 480 | |
|
478 | 481 | with fixture.anon_access(False): |
|
479 | 482 | self.app.get(url(controller='changeset', |
@@ -26,6 +26,7 b' from mock import patch' | |||
|
26 | 26 | |
|
27 | 27 | from rhodecode.lib import auth |
|
28 | 28 | from rhodecode.lib.utils2 import md5 |
|
29 | from rhodecode.model.auth_token import AuthTokenModel | |
|
29 | 30 | from rhodecode.model.db import User |
|
30 | 31 | from rhodecode.model.repo import RepoModel |
|
31 | 32 | from rhodecode.model.user import UserModel |
@@ -580,3 +581,28 b' class TestGenerateAuthToken(object):' | |||
|
580 | 581 | result = auth.generate_auth_token(user_name) |
|
581 | 582 | expected_result = sha1(user_name + random_salt).hexdigest() |
|
582 | 583 | assert result == expected_result |
|
584 | ||
|
585 | ||
|
586 | @pytest.mark.parametrize("test_token, test_roles, auth_result, expected_tokens", [ | |
|
587 | ('', None, False, | |
|
588 | []), | |
|
589 | ('wrongtoken', None, False, | |
|
590 | []), | |
|
591 | ('abracadabra_vcs', [AuthTokenModel.cls.ROLE_API], False, | |
|
592 | [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1)]), | |
|
593 | ('abracadabra_api', [AuthTokenModel.cls.ROLE_API], True, | |
|
594 | [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1)]), | |
|
595 | ('abracadabra_api', [AuthTokenModel.cls.ROLE_API], True, | |
|
596 | [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1), | |
|
597 | ('abracadabra_http', AuthTokenModel.cls.ROLE_HTTP, -1)]), | |
|
598 | ]) | |
|
599 | def test_auth_by_token(test_token, test_roles, auth_result, expected_tokens, | |
|
600 | user_util): | |
|
601 | user = user_util.create_user() | |
|
602 | user_id = user.user_id | |
|
603 | for token, role, expires in expected_tokens: | |
|
604 | new_token = AuthTokenModel().create(user_id, 'test-token', expires, role) | |
|
605 | new_token.api_key = token # inject known name for testing... | |
|
606 | ||
|
607 | assert auth_result == user.authenticate_by_token( | |
|
608 | test_token, roles=test_roles, include_builtin_token=True) |
General Comments 0
You need to be logged in to leave comments.
Login now