auth-tokens: updated logic of authentication to a common shared user method.

This commit is contained in:
Marcin Kuzminski 2017-02-13 14:36:48 +01:00
parent be8db660b1
commit 5c2c266ee9
9 changed files with 156 additions and 58 deletions

View file

@ -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')

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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:

View file

@ -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):

View file

@ -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',

View file

@ -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',

View file

@ -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)