users: added more secure way for fetching authentication tokens.

- we don't expose ALL on single page
- request needs a validation before viewing of each token
- will later add 2nd layer of auth.
This commit is contained in:
Marcin Kuzminski 2020-04-20 15:59:06 +02:00
parent fa30900469
commit 4ac845a76d
11 changed files with 148 additions and 14 deletions

View file

@ -301,6 +301,10 @@ def admin_routes(config):
name='edit_user_auth_tokens',
pattern='/users/{user_id:\d+}/edit/auth_tokens',
user_route=True)
config.add_route(
name='edit_user_auth_tokens_view',
pattern='/users/{user_id:\d+}/edit/auth_tokens/view',
user_route=True)
config.add_route(
name='edit_user_auth_tokens_add',
pattern='/users/{user_id:\d+}/edit/auth_tokens/new',

View file

@ -143,7 +143,7 @@ class TestAdminUsersView(TestController):
response = self.app.get(
route_path('edit_user_auth_tokens', user_id=user_id))
for token in auth_tokens:
response.mustcontain(token)
response.mustcontain(token[:4])
response.mustcontain('never')
@pytest.mark.parametrize("desc, lifetime", [
@ -165,7 +165,7 @@ class TestAdminUsersView(TestController):
response = response.follow()
user = User.get(user_id)
for auth_token in user.auth_tokens:
response.mustcontain(auth_token)
response.mustcontain(auth_token[:4])
def test_delete_auth_token(self, user_util):
self.log_user()

View file

@ -804,6 +804,25 @@ class UsersView(UserAppView):
c.role_vcs = AuthTokenModel.cls.ROLE_VCS
return self._get_template_context(c)
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@view_config(
route_name='edit_user_auth_tokens_view', request_method='POST',
renderer='json_ext', xhr=True)
def auth_tokens_view(self):
_ = self.request.translate
c = self.load_default_context()
c.user = self.db_user
auth_token_id = self.request.POST.get('auth_token_id')
if auth_token_id:
token = UserApiKeys.get_or_404(auth_token_id)
return {
'auth_token': token.api_key
}
def maybe_attach_token_scope(self, token):
# implemented in EE edition
pass

View file

@ -49,6 +49,9 @@ def includeme(config):
config.add_route(
name='my_account_auth_tokens',
pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
config.add_route(
name='my_account_auth_tokens_view',
pattern=ADMIN_PREFIX + '/my_account/auth_tokens/view')
config.add_route(
name='my_account_auth_tokens_add',
pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new')

View file

@ -49,7 +49,7 @@ class TestMyAccountAuthTokens(TestController):
user = User.get(usr['user_id'])
response = self.app.get(route_path('my_account_auth_tokens'))
for token in user.auth_tokens:
response.mustcontain(token)
response.mustcontain(token[:4])
response.mustcontain('never')
def test_my_account_add_auth_tokens_wrong_csrf(self, user_util):
@ -79,7 +79,7 @@ class TestMyAccountAuthTokens(TestController):
response = response.follow()
user = User.get(user_id)
for auth_token in user.auth_tokens:
response.mustcontain(auth_token)
response.mustcontain(auth_token[:4])
def test_my_account_delete_auth_token(self, user_util):
user = user_util.create_user(password='qweqwe')

View file

@ -25,7 +25,7 @@ import string
import formencode
import formencode.htmlfill
import peppercorn
from pyramid.httpexceptions import HTTPFound
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
from pyramid.view import view_config
from rhodecode.apps._base import BaseAppView, DataGridAppView
@ -164,6 +164,27 @@ class MyAccountView(BaseAppView, DataGridAppView):
c.role_vcs = AuthTokenModel.cls.ROLE_VCS
return self._get_template_context(c)
@LoginRequired()
@NotAnonymous()
@CSRFRequired()
@view_config(
route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
renderer='json_ext')
def my_account_auth_tokens_view(self):
_ = self.request.translate
c = self.load_default_context()
auth_token_id = self.request.POST.get('auth_token_id')
if auth_token_id:
token = UserApiKeys.get_or_404(auth_token_id)
if token.user.user_id != c.user.user_id:
raise HTTPNotFound()
return {
'auth_token': token.api_key
}
def maybe_attach_token_scope(self, token):
# implemented in EE edition
pass

View file

@ -352,7 +352,7 @@ table.dataTable {
margin-bottom: @padding;
table.rctable td:first-child {
width: 340px;
width: 120px;
}
}

View file

@ -111,6 +111,7 @@ function registerRCRoutes() {
pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
pyroutes.register('user_notice_dismiss', '/_admin/users/%(user_id)s/notice_dismiss', ['user_id']);
pyroutes.register('edit_user_auth_tokens_view', '/_admin/users/%(user_id)s/edit/auth_tokens/view', ['user_id']);
pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
@ -312,6 +313,7 @@ function registerRCRoutes() {
pyroutes.register('my_account_update', '/_admin/my_account/update', []);
pyroutes.register('my_account_password', '/_admin/my_account/password', []);
pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);

View file

@ -47,3 +47,72 @@ var UsersAutoComplete = function(input_id, user_id) {
lookupFilter: autocompleteFilterResult
});
};
var _showAuthToken = function (authTokenId, showUrl) {
Swal.fire({
title: _gettext('Show this authentication token?'),
showCancelButton: true,
confirmButtonColor: '#84a5d2',
cancelButtonColor: '#e85e4d',
showClass: {
popup: 'swal2-noanimation',
backdrop: 'swal2-noanimation'
},
hideClass: {
popup: '',
backdrop: ''
},
confirmButtonText: _gettext('Show'),
showLoaderOnConfirm: true,
allowOutsideClick: function () {
!Swal.isLoading()
},
preConfirm: function () {
var postData = {
'auth_token_id': authTokenId,
'csrf_token': CSRF_TOKEN
};
return new Promise(function (resolve, reject) {
$.ajax({
type: 'POST',
data: postData,
url: showUrl,
headers: {'X-PARTIAL-XHR': true}
})
.done(function (data) {
resolve(data);
})
.fail(function (jqXHR, textStatus, errorThrown) {
//reject("Failed to fetch Authentication Token")
var message = formatErrorMessage(jqXHR, textStatus, errorThrown)
Swal.showValidationMessage('Request failed: {0}'.format(message)
)
});
})
}
})
.then(function (result) {
if (result.value) {
var tmpl = ('<code>{0}</code>' +
'<i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="{1}" title="Copy Token"></i>');
Swal.fire({
title: _gettext('Authentication Token'),
html: tmpl.format(result.value.auth_token, result.value.auth_token),
confirmButtonColor: '#84a5d2',
cancelButtonColor: '#e85e4d',
showClass: {
popup: 'swal2-noanimation',
backdrop: 'swal2-noanimation'
},
hideClass: {
popup: '',
backdrop: ''
},
})
}
})
}

View file

@ -1,4 +1,10 @@
<div class="panel panel-default">
<script>
var showAuthToken = function(authTokenId) {
return _showAuthToken(authTokenId, pyroutes.url('my_account_auth_tokens_view'))
}
</script>
<div class="panel-heading">
<h3 class="panel-title">${_('Authentication Tokens')}</h3>
</div>
@ -21,9 +27,11 @@
%if c.user_auth_tokens:
%for auth_token in c.user_auth_tokens:
<tr class="${('expired' if auth_token.expired else '')}">
<td class="truncate-wrap td-authtoken">
<div class="user_auth_tokens truncate autoexpand">
<code>${auth_token.api_key}</code>
<td class="td-authtoken">
<div class="user_auth_tokens">
<code class="cursor-pointer" onclick="showAuthToken(${auth_token.user_api_key_id})">
${auth_token.token_obfuscated}
</code>
</div>
</td>
<td class="td-wrap">${auth_token.description}</td>
@ -44,7 +52,7 @@
</td>
<td class="td-action">
${h.secure_form(h.route_path('my_account_auth_tokens_delete'), request=request)}
${h.hidden('del_auth_token', auth_token.user_api_key_id)}
<input name="del_auth_token" type="hidden" value="${auth_token.user_api_key_id}">
<button class="btn btn-link btn-danger" type="submit"
onclick="submitConfirm(event, this, _gettext('Confirm to delete this auth token'), _gettext('Delete'), '${auth_token.token_obfuscated}')"
>

View file

@ -1,6 +1,12 @@
<%namespace name="base" file="/base/base.mako"/>
<div class="panel panel-default">
<script>
var showAuthToken = function(authTokenId) {
return _showAuthToken(authTokenId, pyroutes.url('edit_user_auth_tokens_view', {'user_id': '${c.user.user_id}'}))
}
</script>
<div class="panel-heading">
<h3 class="panel-title">
${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')}
@ -26,9 +32,11 @@
%if c.user_auth_tokens:
%for auth_token in c.user_auth_tokens:
<tr class="${('expired' if auth_token.expired else '')}">
<td class="truncate-wrap td-authtoken">
<div class="user_auth_tokens truncate autoexpand">
<code>${auth_token.api_key}</code>
<td class="td-authtoken">
<div class="user_auth_tokens">
<code class="cursor-pointer" onclick="showAuthToken(${auth_token.user_api_key_id})">
${auth_token.token_obfuscated}
</code>
</div>
</td>
<td class="td-wrap">${auth_token.description}</td>
@ -49,7 +57,7 @@
</td>
<td class="td-action">
${h.secure_form(h.route_path('edit_user_auth_tokens_delete', user_id=c.user.user_id), request=request)}
${h.hidden('del_auth_token', auth_token.user_api_key_id)}
<input name="del_auth_token" type="hidden" value="${auth_token.user_api_key_id}">
<button class="btn btn-link btn-danger" type="submit"
onclick="return confirm('${_('Confirm to remove this auth token: %s') % auth_token.token_obfuscated}');">
${_('Delete')}