##// END OF EJS Templates
auth-tokens: updated logic of authentication to a common shared user method.
marcink -
r1421:5088d9a7 default
parent child Browse files
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 key has proper role')
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 as e:
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 password in active_tokens:
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, region_invalidate
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=True)
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=True)
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=True)
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=True)
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=True)
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=False):
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 = User.get_first_super_admin().feed_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(url(controller='feed', action='rss',
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(self, test_name,
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 = User.get_first_super_admin().api_key
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(self, test_name,
469 auth_token, code):
470 whitelist = self._get_api_whitelist(
471 ['ChangesetController:changeset_raw'])
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 = User.get_first_super_admin().api_key
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