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:
parent
fa30900469
commit
4ac845a76d
11 changed files with 148 additions and 14 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -352,7 +352,7 @@ table.dataTable {
|
|||
margin-bottom: @padding;
|
||||
|
||||
table.rctable td:first-child {
|
||||
width: 340px;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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', []);
|
||||
|
|
|
|||
|
|
@ -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: ''
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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}')"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue