auth-tokens: updated logic of authentication to a common shared user method.
This commit is contained in:
parent
be8db660b1
commit
5c2c266ee9
9 changed files with 156 additions and 58 deletions
|
|
@ -209,18 +209,18 @@ def request_view(request):
|
|||
request.rpc_user = auth_u
|
||||
|
||||
# now check if token is valid for API
|
||||
role = UserApiKeys.ROLE_API
|
||||
extra_auth_tokens = [
|
||||
x.api_key for x in User.extra_valid_auth_tokens(api_user, role=role)]
|
||||
active_tokens = [api_user.api_key] + extra_auth_tokens
|
||||
auth_token = request.rpc_api_key
|
||||
token_match = api_user.authenticate_by_token(
|
||||
auth_token, roles=[UserApiKeys.ROLE_API], include_builtin_token=True)
|
||||
invalid_token = not token_match
|
||||
|
||||
log.debug('Checking if API key has proper role')
|
||||
if request.rpc_api_key not in active_tokens:
|
||||
log.debug('Checking if API KEY is valid with proper role')
|
||||
if invalid_token:
|
||||
return jsonrpc_error(
|
||||
request, retid=request.rpc_id,
|
||||
message='API KEY has bad role for an API call')
|
||||
message='API KEY invalid or, has bad role for an API call')
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
log.exception('Error on API AUTH')
|
||||
return jsonrpc_error(
|
||||
request, retid=request.rpc_id, message='Invalid API KEY')
|
||||
|
|
|
|||
|
|
@ -122,10 +122,10 @@ class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
|
|||
|
||||
log.debug('Authenticating user with args %s', user_attrs)
|
||||
if userobj.active:
|
||||
role = UserApiKeys.ROLE_VCS
|
||||
active_tokens = [x.api_key for x in
|
||||
User.extra_valid_auth_tokens(userobj, role=role)]
|
||||
if userobj.username == username and password in active_tokens:
|
||||
token_match = userobj.authenticate_by_token(
|
||||
password, roles=[UserApiKeys.ROLE_VCS])
|
||||
|
||||
if userobj.username == username and token_match:
|
||||
log.info(
|
||||
'user `%s` successfully authenticated via %s',
|
||||
user_attrs['username'], self.name)
|
||||
|
|
|
|||
|
|
@ -28,12 +28,11 @@ import pytz
|
|||
from pylons import url, response, tmpl_context as c
|
||||
from pylons.i18n.translation import _
|
||||
|
||||
from beaker.cache import cache_region, region_invalidate
|
||||
from beaker.cache import cache_region
|
||||
from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
|
||||
|
||||
from rhodecode.model.db import CacheKey
|
||||
from rhodecode.model.db import CacheKey, UserApiKeys
|
||||
from rhodecode.lib import helpers as h
|
||||
from rhodecode.lib import caches
|
||||
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
|
||||
from rhodecode.lib.base import BaseRepoController
|
||||
from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
|
||||
|
|
@ -62,7 +61,7 @@ class FeedController(BaseRepoController):
|
|||
safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
|
||||
}
|
||||
|
||||
@LoginRequired(auth_token_access=True)
|
||||
@LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
|
||||
def __before__(self):
|
||||
super(FeedController, self).__before__()
|
||||
config = self._get_config()
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ from pylons import request, tmpl_context as c, response, url
|
|||
from pylons.i18n.translation import _
|
||||
|
||||
from rhodecode.controllers.admin.admin import _journal_filter
|
||||
from rhodecode.model.db import UserLog, UserFollowing, User
|
||||
from rhodecode.model.db import UserLog, UserFollowing, User, UserApiKeys
|
||||
from rhodecode.model.meta import Session
|
||||
import rhodecode.lib.helpers as h
|
||||
from rhodecode.lib.helpers import Page
|
||||
|
|
@ -211,7 +211,7 @@ class JournalController(BaseController):
|
|||
|
||||
return render('journal/journal.mako')
|
||||
|
||||
@LoginRequired(auth_token_access=True)
|
||||
@LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
|
||||
@NotAnonymous()
|
||||
def journal_atom(self):
|
||||
"""
|
||||
|
|
@ -223,7 +223,7 @@ class JournalController(BaseController):
|
|||
.all()
|
||||
return self._atom_feed(following, public=False)
|
||||
|
||||
@LoginRequired(auth_token_access=True)
|
||||
@LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
|
||||
@NotAnonymous()
|
||||
def journal_rss(self):
|
||||
"""
|
||||
|
|
@ -281,7 +281,7 @@ class JournalController(BaseController):
|
|||
return c.journal_data
|
||||
return render('journal/public_journal.mako')
|
||||
|
||||
@LoginRequired(auth_token_access=True)
|
||||
@LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
|
||||
def public_journal_atom(self):
|
||||
"""
|
||||
Produce an atom-1.0 feed via feedgenerator module
|
||||
|
|
@ -293,7 +293,7 @@ class JournalController(BaseController):
|
|||
|
||||
return self._atom_feed(c.following)
|
||||
|
||||
@LoginRequired(auth_token_access=True)
|
||||
@LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
|
||||
def public_journal_rss(self):
|
||||
"""
|
||||
Produce an rss2 feed via feedgenerator module
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ class PasswordGenerator(object):
|
|||
|
||||
|
||||
class _RhodeCodeCryptoBase(object):
|
||||
ENC_PREF = None
|
||||
|
||||
def hash_create(self, str_):
|
||||
"""
|
||||
|
|
@ -139,6 +140,7 @@ class _RhodeCodeCryptoBase(object):
|
|||
|
||||
|
||||
class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
|
||||
ENC_PREF = '$2a$10'
|
||||
|
||||
def hash_create(self, str_):
|
||||
self._assert_bytes(str_)
|
||||
|
|
@ -194,6 +196,7 @@ class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
|
|||
|
||||
|
||||
class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
|
||||
ENC_PREF = '_'
|
||||
|
||||
def hash_create(self, str_):
|
||||
self._assert_bytes(str_)
|
||||
|
|
@ -211,6 +214,7 @@ class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
|
|||
|
||||
|
||||
class _RhodeCodeCryptoMd5(_RhodeCodeCryptoBase):
|
||||
ENC_PREF = '_'
|
||||
|
||||
def hash_create(self, str_):
|
||||
self._assert_bytes(str_)
|
||||
|
|
@ -831,10 +835,6 @@ class AuthUser(object):
|
|||
self._permissions_scoped_cache[cache_key] = res
|
||||
return self._permissions_scoped_cache[cache_key]
|
||||
|
||||
@property
|
||||
def auth_tokens(self):
|
||||
return self.get_auth_tokens()
|
||||
|
||||
def get_instance(self):
|
||||
return User.get(self.user_id)
|
||||
|
||||
|
|
@ -925,16 +925,6 @@ class AuthUser(object):
|
|||
log.debug('PERMISSION tree computed %s' % (result_repr,))
|
||||
return result
|
||||
|
||||
def get_auth_tokens(self):
|
||||
auth_tokens = [self.api_key]
|
||||
for api_key in UserApiKeys.query()\
|
||||
.filter(UserApiKeys.user_id == self.user_id)\
|
||||
.filter(or_(UserApiKeys.expires == -1,
|
||||
UserApiKeys.expires >= time.time())).all():
|
||||
auth_tokens.append(api_key.api_key)
|
||||
|
||||
return auth_tokens
|
||||
|
||||
@property
|
||||
def is_default(self):
|
||||
return self.username == User.DEFAULT_USER
|
||||
|
|
@ -1171,7 +1161,7 @@ class LoginRequired(object):
|
|||
:param api_access: if enabled this checks only for valid auth token
|
||||
and grants access based on valid token
|
||||
"""
|
||||
def __init__(self, auth_token_access=False):
|
||||
def __init__(self, auth_token_access=None):
|
||||
self.auth_token_access = auth_token_access
|
||||
|
||||
def __call__(self, func):
|
||||
|
|
@ -1191,7 +1181,7 @@ class LoginRequired(object):
|
|||
ip_access_valid = False
|
||||
|
||||
# check if we used an APIKEY and it's a valid one
|
||||
# defined whitelist of controllers which API access will be enabled
|
||||
# defined white-list of controllers which API access will be enabled
|
||||
_auth_token = request.GET.get(
|
||||
'auth_token', '') or request.GET.get('api_key', '')
|
||||
auth_token_access_valid = allowed_auth_token_access(
|
||||
|
|
@ -1200,8 +1190,20 @@ class LoginRequired(object):
|
|||
# explicit controller is enabled or API is in our whitelist
|
||||
if self.auth_token_access or auth_token_access_valid:
|
||||
log.debug('Checking AUTH TOKEN access for %s' % (cls,))
|
||||
db_user = user.get_instance()
|
||||
|
||||
if _auth_token and _auth_token in user.auth_tokens:
|
||||
if db_user:
|
||||
if self.auth_token_access:
|
||||
roles = self.auth_token_access
|
||||
else:
|
||||
roles = [UserApiKeys.ROLE_HTTP]
|
||||
token_match = db_user.authenticate_by_token(
|
||||
_auth_token, roles=roles, include_builtin_token=True)
|
||||
else:
|
||||
log.debug('Unable to fetch db instance for auth user: %s', user)
|
||||
token_match = False
|
||||
|
||||
if _auth_token and token_match:
|
||||
auth_token_access_valid = True
|
||||
log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -581,15 +581,16 @@ class User(Base, BaseModel):
|
|||
|
||||
@property
|
||||
def feed_token(self):
|
||||
return self.get_feed_token()
|
||||
|
||||
def get_feed_token(self):
|
||||
feed_tokens = UserApiKeys.query()\
|
||||
.filter(UserApiKeys.user == self)\
|
||||
.filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
|
||||
.all()
|
||||
if feed_tokens:
|
||||
return feed_tokens[0].api_key
|
||||
else:
|
||||
# use the main token so we don't end up with nothing...
|
||||
return self.api_key
|
||||
return 'NO_FEED_TOKEN_AVAILABLE'
|
||||
|
||||
@classmethod
|
||||
def extra_valid_auth_tokens(cls, user, role=None):
|
||||
|
|
@ -601,11 +602,57 @@ class User(Base, BaseModel):
|
|||
UserApiKeys.role == UserApiKeys.ROLE_ALL))
|
||||
return tokens.all()
|
||||
|
||||
def authenticate_by_token(self, auth_token, roles=None,
|
||||
include_builtin_token=False):
|
||||
from rhodecode.lib import auth
|
||||
|
||||
log.debug('Trying to authenticate user: %s via auth-token, '
|
||||
'and roles: %s', self, roles)
|
||||
|
||||
if not auth_token:
|
||||
return False
|
||||
|
||||
crypto_backend = auth.crypto_backend()
|
||||
|
||||
roles = (roles or []) + [UserApiKeys.ROLE_ALL]
|
||||
tokens_q = UserApiKeys.query()\
|
||||
.filter(UserApiKeys.user_id == self.user_id)\
|
||||
.filter(or_(UserApiKeys.expires == -1,
|
||||
UserApiKeys.expires >= time.time()))
|
||||
|
||||
tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
|
||||
|
||||
maybe_builtin = []
|
||||
if include_builtin_token:
|
||||
maybe_builtin = [AttributeDict({'api_key': self.api_key})]
|
||||
|
||||
plain_tokens = []
|
||||
hash_tokens = []
|
||||
|
||||
for token in tokens_q.all() + maybe_builtin:
|
||||
if token.api_key.startswith(crypto_backend.ENC_PREF):
|
||||
hash_tokens.append(token.api_key)
|
||||
else:
|
||||
plain_tokens.append(token.api_key)
|
||||
|
||||
is_plain_match = auth_token in plain_tokens
|
||||
if is_plain_match:
|
||||
return True
|
||||
|
||||
for hashed in hash_tokens:
|
||||
# marcink: this is expensive to calculate, but the most secure
|
||||
match = crypto_backend.hash_check(auth_token, hashed)
|
||||
if match:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def builtin_token_roles(self):
|
||||
return map(UserApiKeys._get_role_name, [
|
||||
roles = [
|
||||
UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
|
||||
])
|
||||
]
|
||||
return map(UserApiKeys._get_role_name, roles)
|
||||
|
||||
@property
|
||||
def ip_addresses(self):
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
# This program is dual-licensed. If you wish to learn more about the
|
||||
# RhodeCode Enterprise Edition, including its added features, Support services,
|
||||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||
|
||||
from rhodecode.model.auth_token import AuthTokenModel
|
||||
from rhodecode.model.db import User
|
||||
from rhodecode.tests import *
|
||||
|
||||
|
|
@ -32,15 +32,36 @@ class TestFeedController(TestController):
|
|||
assert response.content_type == "application/rss+xml"
|
||||
assert """<rss version="2.0">""" in response
|
||||
|
||||
def test_rss_with_auth_token(self, backend):
|
||||
auth_token = User.get_first_super_admin().feed_token
|
||||
def test_rss_with_auth_token(self, backend, user_admin):
|
||||
auth_token = user_admin.feed_token
|
||||
assert auth_token != ''
|
||||
response = self.app.get(url(controller='feed', action='rss',
|
||||
repo_name=backend.repo_name, auth_token=auth_token))
|
||||
response = self.app.get(
|
||||
url(controller='feed', action='rss',
|
||||
repo_name=backend.repo_name, auth_token=auth_token,
|
||||
status=200))
|
||||
|
||||
assert response.content_type == "application/rss+xml"
|
||||
assert """<rss version="2.0">""" in response
|
||||
|
||||
def test_rss_with_auth_token_of_wrong_type(self, backend, user_util):
|
||||
user = user_util.create_user()
|
||||
auth_token = AuthTokenModel().create(
|
||||
user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API)
|
||||
auth_token = auth_token.api_key
|
||||
|
||||
self.app.get(
|
||||
url(controller='feed', action='rss',
|
||||
repo_name=backend.repo_name, auth_token=auth_token),
|
||||
status=302)
|
||||
|
||||
auth_token = AuthTokenModel().create(
|
||||
user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
|
||||
auth_token = auth_token.api_key
|
||||
self.app.get(
|
||||
url(controller='feed', action='rss',
|
||||
repo_name=backend.repo_name, auth_token=auth_token),
|
||||
status=200)
|
||||
|
||||
def test_atom(self, backend):
|
||||
self.log_user()
|
||||
response = self.app.get(url(controller='feed', action='atom',
|
||||
|
|
|
|||
|
|
@ -443,14 +443,15 @@ class TestLoginController:
|
|||
('fake_number', '123456'),
|
||||
('proper_auth_token', None)
|
||||
])
|
||||
def test_access_not_whitelisted_page_via_auth_token(self, test_name,
|
||||
auth_token):
|
||||
def test_access_not_whitelisted_page_via_auth_token(
|
||||
self, test_name, auth_token, user_admin):
|
||||
|
||||
whitelist = self._get_api_whitelist([])
|
||||
with mock.patch.dict('rhodecode.CONFIG', whitelist):
|
||||
assert [] == whitelist['api_access_controllers_whitelist']
|
||||
if test_name == 'proper_auth_token':
|
||||
# use builtin if api_key is None
|
||||
auth_token = User.get_first_super_admin().api_key
|
||||
auth_token = user_admin.api_key
|
||||
|
||||
with fixture.anon_access(False):
|
||||
self.app.get(url(controller='changeset',
|
||||
|
|
@ -465,15 +466,17 @@ class TestLoginController:
|
|||
('fake_number', '123456', 302),
|
||||
('proper_auth_token', None, 200)
|
||||
])
|
||||
def test_access_whitelisted_page_via_auth_token(self, test_name,
|
||||
auth_token, code):
|
||||
whitelist = self._get_api_whitelist(
|
||||
['ChangesetController:changeset_raw'])
|
||||
def test_access_whitelisted_page_via_auth_token(
|
||||
self, test_name, auth_token, code, user_admin):
|
||||
|
||||
whitelist_entry = ['ChangesetController:changeset_raw']
|
||||
whitelist = self._get_api_whitelist(whitelist_entry)
|
||||
|
||||
with mock.patch.dict('rhodecode.CONFIG', whitelist):
|
||||
assert ['ChangesetController:changeset_raw'] == \
|
||||
whitelist['api_access_controllers_whitelist']
|
||||
assert whitelist_entry == whitelist['api_access_controllers_whitelist']
|
||||
|
||||
if test_name == 'proper_auth_token':
|
||||
auth_token = User.get_first_super_admin().api_key
|
||||
auth_token = user_admin.api_key
|
||||
|
||||
with fixture.anon_access(False):
|
||||
self.app.get(url(controller='changeset',
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from mock import patch
|
|||
|
||||
from rhodecode.lib import auth
|
||||
from rhodecode.lib.utils2 import md5
|
||||
from rhodecode.model.auth_token import AuthTokenModel
|
||||
from rhodecode.model.db import User
|
||||
from rhodecode.model.repo import RepoModel
|
||||
from rhodecode.model.user import UserModel
|
||||
|
|
@ -580,3 +581,28 @@ class TestGenerateAuthToken(object):
|
|||
result = auth.generate_auth_token(user_name)
|
||||
expected_result = sha1(user_name + random_salt).hexdigest()
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize("test_token, test_roles, auth_result, expected_tokens", [
|
||||
('', None, False,
|
||||
[]),
|
||||
('wrongtoken', None, False,
|
||||
[]),
|
||||
('abracadabra_vcs', [AuthTokenModel.cls.ROLE_API], False,
|
||||
[('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1)]),
|
||||
('abracadabra_api', [AuthTokenModel.cls.ROLE_API], True,
|
||||
[('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1)]),
|
||||
('abracadabra_api', [AuthTokenModel.cls.ROLE_API], True,
|
||||
[('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1),
|
||||
('abracadabra_http', AuthTokenModel.cls.ROLE_HTTP, -1)]),
|
||||
])
|
||||
def test_auth_by_token(test_token, test_roles, auth_result, expected_tokens,
|
||||
user_util):
|
||||
user = user_util.create_user()
|
||||
user_id = user.user_id
|
||||
for token, role, expires in expected_tokens:
|
||||
new_token = AuthTokenModel().create(user_id, 'test-token', expires, role)
|
||||
new_token.api_key = token # inject known name for testing...
|
||||
|
||||
assert auth_result == user.authenticate_by_token(
|
||||
test_token, roles=test_roles, include_builtin_token=True)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue