##// 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 request.rpc_user = auth_u
209 request.rpc_user = auth_u
210
210
211 # now check if token is valid for API
211 # now check if token is valid for API
212 role = UserApiKeys.ROLE_API
212 auth_token = request.rpc_api_key
213 extra_auth_tokens = [
213 token_match = api_user.authenticate_by_token(
214 x.api_key for x in User.extra_valid_auth_tokens(api_user, role=role)]
214 auth_token, roles=[UserApiKeys.ROLE_API], include_builtin_token=True)
215 active_tokens = [api_user.api_key] + extra_auth_tokens
215 invalid_token = not token_match
216
216
217 log.debug('Checking if API key has proper role')
217 log.debug('Checking if API KEY is valid with proper role')
218 if request.rpc_api_key not in active_tokens:
218 if invalid_token:
219 return jsonrpc_error(
219 return jsonrpc_error(
220 request, retid=request.rpc_id,
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 log.exception('Error on API AUTH')
224 log.exception('Error on API AUTH')
225 return jsonrpc_error(
225 return jsonrpc_error(
226 request, retid=request.rpc_id, message='Invalid API KEY')
226 request, retid=request.rpc_id, message='Invalid API KEY')
@@ -122,10 +122,10 b' class RhodeCodeAuthPlugin(RhodeCodeAuthP'
122
122
123 log.debug('Authenticating user with args %s', user_attrs)
123 log.debug('Authenticating user with args %s', user_attrs)
124 if userobj.active:
124 if userobj.active:
125 role = UserApiKeys.ROLE_VCS
125 token_match = userobj.authenticate_by_token(
126 active_tokens = [x.api_key for x in
126 password, roles=[UserApiKeys.ROLE_VCS])
127 User.extra_valid_auth_tokens(userobj, role=role)]
127
128 if userobj.username == username and password in active_tokens:
128 if userobj.username == username and token_match:
129 log.info(
129 log.info(
130 'user `%s` successfully authenticated via %s',
130 'user `%s` successfully authenticated via %s',
131 user_attrs['username'], self.name)
131 user_attrs['username'], self.name)
@@ -28,12 +28,11 b' import pytz'
28 from pylons import url, response, tmpl_context as c
28 from pylons import url, response, tmpl_context as c
29 from pylons.i18n.translation import _
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 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
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 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.lib import caches
37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 from rhodecode.lib.base import BaseRepoController
37 from rhodecode.lib.base import BaseRepoController
39 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
38 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
@@ -62,7 +61,7 b' class FeedController(BaseRepoController)'
62 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
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 def __before__(self):
65 def __before__(self):
67 super(FeedController, self).__before__()
66 super(FeedController, self).__before__()
68 config = self._get_config()
67 config = self._get_config()
@@ -35,7 +35,7 b' from pylons import request, tmpl_context'
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36
36
37 from rhodecode.controllers.admin.admin import _journal_filter
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 from rhodecode.model.meta import Session
39 from rhodecode.model.meta import Session
40 import rhodecode.lib.helpers as h
40 import rhodecode.lib.helpers as h
41 from rhodecode.lib.helpers import Page
41 from rhodecode.lib.helpers import Page
@@ -211,7 +211,7 b' class JournalController(BaseController):'
211
211
212 return render('journal/journal.mako')
212 return render('journal/journal.mako')
213
213
214 @LoginRequired(auth_token_access=True)
214 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
215 @NotAnonymous()
215 @NotAnonymous()
216 def journal_atom(self):
216 def journal_atom(self):
217 """
217 """
@@ -223,7 +223,7 b' class JournalController(BaseController):'
223 .all()
223 .all()
224 return self._atom_feed(following, public=False)
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 @NotAnonymous()
227 @NotAnonymous()
228 def journal_rss(self):
228 def journal_rss(self):
229 """
229 """
@@ -281,7 +281,7 b' class JournalController(BaseController):'
281 return c.journal_data
281 return c.journal_data
282 return render('journal/public_journal.mako')
282 return render('journal/public_journal.mako')
283
283
284 @LoginRequired(auth_token_access=True)
284 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
285 def public_journal_atom(self):
285 def public_journal_atom(self):
286 """
286 """
287 Produce an atom-1.0 feed via feedgenerator module
287 Produce an atom-1.0 feed via feedgenerator module
@@ -293,7 +293,7 b' class JournalController(BaseController):'
293
293
294 return self._atom_feed(c.following)
294 return self._atom_feed(c.following)
295
295
296 @LoginRequired(auth_token_access=True)
296 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
297 def public_journal_rss(self):
297 def public_journal_rss(self):
298 """
298 """
299 Produce an rss2 feed via feedgenerator module
299 Produce an rss2 feed via feedgenerator module
@@ -99,6 +99,7 b' class PasswordGenerator(object):'
99
99
100
100
101 class _RhodeCodeCryptoBase(object):
101 class _RhodeCodeCryptoBase(object):
102 ENC_PREF = None
102
103
103 def hash_create(self, str_):
104 def hash_create(self, str_):
104 """
105 """
@@ -139,6 +140,7 b' class _RhodeCodeCryptoBase(object):'
139
140
140
141
141 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
142 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
143 ENC_PREF = '$2a$10'
142
144
143 def hash_create(self, str_):
145 def hash_create(self, str_):
144 self._assert_bytes(str_)
146 self._assert_bytes(str_)
@@ -194,6 +196,7 b' class _RhodeCodeCryptoBCrypt(_RhodeCodeC'
194
196
195
197
196 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
198 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
199 ENC_PREF = '_'
197
200
198 def hash_create(self, str_):
201 def hash_create(self, str_):
199 self._assert_bytes(str_)
202 self._assert_bytes(str_)
@@ -211,6 +214,7 b' class _RhodeCodeCryptoSha256(_RhodeCodeC'
211
214
212
215
213 class _RhodeCodeCryptoMd5(_RhodeCodeCryptoBase):
216 class _RhodeCodeCryptoMd5(_RhodeCodeCryptoBase):
217 ENC_PREF = '_'
214
218
215 def hash_create(self, str_):
219 def hash_create(self, str_):
216 self._assert_bytes(str_)
220 self._assert_bytes(str_)
@@ -831,10 +835,6 b' class AuthUser(object):'
831 self._permissions_scoped_cache[cache_key] = res
835 self._permissions_scoped_cache[cache_key] = res
832 return self._permissions_scoped_cache[cache_key]
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 def get_instance(self):
838 def get_instance(self):
839 return User.get(self.user_id)
839 return User.get(self.user_id)
840
840
@@ -925,16 +925,6 b' class AuthUser(object):'
925 log.debug('PERMISSION tree computed %s' % (result_repr,))
925 log.debug('PERMISSION tree computed %s' % (result_repr,))
926 return result
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 @property
928 @property
939 def is_default(self):
929 def is_default(self):
940 return self.username == User.DEFAULT_USER
930 return self.username == User.DEFAULT_USER
@@ -1171,7 +1161,7 b' class LoginRequired(object):'
1171 :param api_access: if enabled this checks only for valid auth token
1161 :param api_access: if enabled this checks only for valid auth token
1172 and grants access based on valid token
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 self.auth_token_access = auth_token_access
1165 self.auth_token_access = auth_token_access
1176
1166
1177 def __call__(self, func):
1167 def __call__(self, func):
@@ -1191,7 +1181,7 b' class LoginRequired(object):'
1191 ip_access_valid = False
1181 ip_access_valid = False
1192
1182
1193 # check if we used an APIKEY and it's a valid one
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 _auth_token = request.GET.get(
1185 _auth_token = request.GET.get(
1196 'auth_token', '') or request.GET.get('api_key', '')
1186 'auth_token', '') or request.GET.get('api_key', '')
1197 auth_token_access_valid = allowed_auth_token_access(
1187 auth_token_access_valid = allowed_auth_token_access(
@@ -1200,8 +1190,20 b' class LoginRequired(object):'
1200 # explicit controller is enabled or API is in our whitelist
1190 # explicit controller is enabled or API is in our whitelist
1201 if self.auth_token_access or auth_token_access_valid:
1191 if self.auth_token_access or auth_token_access_valid:
1202 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
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 auth_token_access_valid = True
1207 auth_token_access_valid = True
1206 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1208 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1207 else:
1209 else:
@@ -581,15 +581,16 b' class User(Base, BaseModel):'
581
581
582 @property
582 @property
583 def feed_token(self):
583 def feed_token(self):
584 return self.get_feed_token()
585
586 def get_feed_token(self):
584 feed_tokens = UserApiKeys.query()\
587 feed_tokens = UserApiKeys.query()\
585 .filter(UserApiKeys.user == self)\
588 .filter(UserApiKeys.user == self)\
586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
589 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
587 .all()
590 .all()
588 if feed_tokens:
591 if feed_tokens:
589 return feed_tokens[0].api_key
592 return feed_tokens[0].api_key
590 else:
593 return 'NO_FEED_TOKEN_AVAILABLE'
591 # use the main token so we don't end up with nothing...
592 return self.api_key
593
594
594 @classmethod
595 @classmethod
595 def extra_valid_auth_tokens(cls, user, role=None):
596 def extra_valid_auth_tokens(cls, user, role=None):
@@ -601,11 +602,57 b' class User(Base, BaseModel):'
601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
602 UserApiKeys.role == UserApiKeys.ROLE_ALL))
602 return tokens.all()
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 @property
650 @property
605 def builtin_token_roles(self):
651 def builtin_token_roles(self):
606 return map(UserApiKeys._get_role_name, [
652 roles = [
607 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
653 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
608 ])
654 ]
655 return map(UserApiKeys._get_role_name, roles)
609
656
610 @property
657 @property
611 def ip_addresses(self):
658 def ip_addresses(self):
@@ -17,7 +17,7 b''
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20 from rhodecode.model.auth_token import AuthTokenModel
21 from rhodecode.model.db import User
21 from rhodecode.model.db import User
22 from rhodecode.tests import *
22 from rhodecode.tests import *
23
23
@@ -32,15 +32,36 b' class TestFeedController(TestController)'
32 assert response.content_type == "application/rss+xml"
32 assert response.content_type == "application/rss+xml"
33 assert """<rss version="2.0">""" in response
33 assert """<rss version="2.0">""" in response
34
34
35 def test_rss_with_auth_token(self, backend):
35 def test_rss_with_auth_token(self, backend, user_admin):
36 auth_token = User.get_first_super_admin().feed_token
36 auth_token = user_admin.feed_token
37 assert auth_token != ''
37 assert auth_token != ''
38 response = self.app.get(url(controller='feed', action='rss',
38 response = self.app.get(
39 repo_name=backend.repo_name, auth_token=auth_token))
39 url(controller='feed', action='rss',
40 repo_name=backend.repo_name, auth_token=auth_token,
41 status=200))
40
42
41 assert response.content_type == "application/rss+xml"
43 assert response.content_type == "application/rss+xml"
42 assert """<rss version="2.0">""" in response
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 def test_atom(self, backend):
65 def test_atom(self, backend):
45 self.log_user()
66 self.log_user()
46 response = self.app.get(url(controller='feed', action='atom',
67 response = self.app.get(url(controller='feed', action='atom',
@@ -443,14 +443,15 b' class TestLoginController:'
443 ('fake_number', '123456'),
443 ('fake_number', '123456'),
444 ('proper_auth_token', None)
444 ('proper_auth_token', None)
445 ])
445 ])
446 def test_access_not_whitelisted_page_via_auth_token(self, test_name,
446 def test_access_not_whitelisted_page_via_auth_token(
447 auth_token):
447 self, test_name, auth_token, user_admin):
448
448 whitelist = self._get_api_whitelist([])
449 whitelist = self._get_api_whitelist([])
449 with mock.patch.dict('rhodecode.CONFIG', whitelist):
450 with mock.patch.dict('rhodecode.CONFIG', whitelist):
450 assert [] == whitelist['api_access_controllers_whitelist']
451 assert [] == whitelist['api_access_controllers_whitelist']
451 if test_name == 'proper_auth_token':
452 if test_name == 'proper_auth_token':
452 # use builtin if api_key is None
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 with fixture.anon_access(False):
456 with fixture.anon_access(False):
456 self.app.get(url(controller='changeset',
457 self.app.get(url(controller='changeset',
@@ -465,15 +466,17 b' class TestLoginController:'
465 ('fake_number', '123456', 302),
466 ('fake_number', '123456', 302),
466 ('proper_auth_token', None, 200)
467 ('proper_auth_token', None, 200)
467 ])
468 ])
468 def test_access_whitelisted_page_via_auth_token(self, test_name,
469 def test_access_whitelisted_page_via_auth_token(
469 auth_token, code):
470 self, test_name, auth_token, code, user_admin):
470 whitelist = self._get_api_whitelist(
471
471 ['ChangesetController:changeset_raw'])
472 whitelist_entry = ['ChangesetController:changeset_raw']
473 whitelist = self._get_api_whitelist(whitelist_entry)
474
472 with mock.patch.dict('rhodecode.CONFIG', whitelist):
475 with mock.patch.dict('rhodecode.CONFIG', whitelist):
473 assert ['ChangesetController:changeset_raw'] == \
476 assert whitelist_entry == whitelist['api_access_controllers_whitelist']
474 whitelist['api_access_controllers_whitelist']
477
475 if test_name == 'proper_auth_token':
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 with fixture.anon_access(False):
481 with fixture.anon_access(False):
479 self.app.get(url(controller='changeset',
482 self.app.get(url(controller='changeset',
@@ -26,6 +26,7 b' from mock import patch'
26
26
27 from rhodecode.lib import auth
27 from rhodecode.lib import auth
28 from rhodecode.lib.utils2 import md5
28 from rhodecode.lib.utils2 import md5
29 from rhodecode.model.auth_token import AuthTokenModel
29 from rhodecode.model.db import User
30 from rhodecode.model.db import User
30 from rhodecode.model.repo import RepoModel
31 from rhodecode.model.repo import RepoModel
31 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
@@ -580,3 +581,28 b' class TestGenerateAuthToken(object):'
580 result = auth.generate_auth_token(user_name)
581 result = auth.generate_auth_token(user_name)
581 expected_result = sha1(user_name + random_salt).hexdigest()
582 expected_result = sha1(user_name + random_salt).hexdigest()
582 assert result == expected_result
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