ssh-keys: added admin panel for managing globally all SSH Keys.
- added also option to manually update the keyfile - fetch and show all user keys for audit purpose
This commit is contained in:
parent
42f25bce8e
commit
3ea8958a92
6 changed files with 254 additions and 5 deletions
|
|
@ -106,6 +106,16 @@ def admin_routes(config):
|
|||
name='admin_permissions_auth_token_access',
|
||||
pattern='/permissions/auth_token_access')
|
||||
|
||||
config.add_route(
|
||||
name='admin_permissions_ssh_keys',
|
||||
pattern='/permissions/ssh_keys')
|
||||
config.add_route(
|
||||
name='admin_permissions_ssh_keys_data',
|
||||
pattern='/permissions/ssh_keys/data')
|
||||
config.add_route(
|
||||
name='admin_permissions_ssh_keys_update',
|
||||
pattern='/permissions/ssh_keys/update')
|
||||
|
||||
# users admin
|
||||
config.add_route(
|
||||
name='users',
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@
|
|||
|
||||
import pytest
|
||||
from rhodecode.model.db import User, UserIpMap
|
||||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.permission import PermissionModel
|
||||
from rhodecode.model.ssh_key import SshKeyModel
|
||||
from rhodecode.tests import (
|
||||
TestController, clear_all_caches, assert_session_flash)
|
||||
|
||||
|
|
@ -55,7 +57,14 @@ def route_path(name, params=None, **kwargs):
|
|||
'admin_permissions_ips':
|
||||
ADMIN_PREFIX + '/permissions/ips',
|
||||
'admin_permissions_overview':
|
||||
ADMIN_PREFIX + '/permissions/overview'
|
||||
ADMIN_PREFIX + '/permissions/overview',
|
||||
|
||||
'admin_permissions_ssh_keys':
|
||||
ADMIN_PREFIX + '/permissions/ssh_keys',
|
||||
'admin_permissions_ssh_keys_data':
|
||||
ADMIN_PREFIX + '/permissions/ssh_keys/data',
|
||||
'admin_permissions_ssh_keys_update':
|
||||
ADMIN_PREFIX + '/permissions/ssh_keys/update'
|
||||
|
||||
}[name].format(**kwargs)
|
||||
|
||||
|
|
@ -248,3 +257,30 @@ class TestAdminPermissionsController(TestController):
|
|||
def test_index_overview(self):
|
||||
self.log_user()
|
||||
self.app.get(route_path('admin_permissions_overview'))
|
||||
|
||||
def test_ssh_keys(self):
|
||||
self.log_user()
|
||||
self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
|
||||
|
||||
def test_ssh_keys_data(self, user_util, xhr_header):
|
||||
self.log_user()
|
||||
response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
|
||||
extra_environ=xhr_header)
|
||||
assert response.json == {u'data': [], u'draw': None,
|
||||
u'recordsFiltered': 0, u'recordsTotal': 0}
|
||||
|
||||
dummy_user = user_util.create_user()
|
||||
SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
|
||||
Session().commit()
|
||||
response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
|
||||
extra_environ=xhr_header)
|
||||
assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
|
||||
|
||||
def test_ssh_keys_update(self):
|
||||
self.log_user()
|
||||
response = self.app.post(
|
||||
route_path('admin_permissions_ssh_keys_update'),
|
||||
dict(csrf_token=self.csrf_token), status=302)
|
||||
|
||||
assert_session_flash(
|
||||
response, 'SSH key support is disabled in .ini file')
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
import re
|
||||
import logging
|
||||
import formencode
|
||||
import datetime
|
||||
from pyramid.interfaces import IRoutesMapper
|
||||
|
||||
from pyramid.view import view_config
|
||||
|
|
@ -28,13 +29,15 @@ from pyramid.httpexceptions import HTTPFound
|
|||
from pyramid.renderers import render
|
||||
from pyramid.response import Response
|
||||
|
||||
from rhodecode.apps._base import BaseAppView
|
||||
from rhodecode.apps._base import BaseAppView, DataGridAppView
|
||||
from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
|
||||
from rhodecode.events import trigger
|
||||
|
||||
from rhodecode.lib import helpers as h
|
||||
from rhodecode.lib.auth import (
|
||||
LoginRequired, HasPermissionAllDecorator, CSRFRequired)
|
||||
from rhodecode.lib.utils2 import aslist
|
||||
from rhodecode.model.db import User, UserIpMap
|
||||
from rhodecode.lib.utils2 import aslist, safe_unicode
|
||||
from rhodecode.model.db import or_, joinedload, coalesce, User, UserIpMap, UserSshKeys
|
||||
from rhodecode.model.forms import (
|
||||
ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
|
||||
from rhodecode.model.meta import Session
|
||||
|
|
@ -45,7 +48,7 @@ from rhodecode.model.settings import SettingsModel
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminPermissionsView(BaseAppView):
|
||||
class AdminPermissionsView(BaseAppView, DataGridAppView):
|
||||
def load_default_context(self):
|
||||
c = self._get_local_tmpl_context()
|
||||
|
||||
|
|
@ -367,3 +370,106 @@ class AdminPermissionsView(BaseAppView):
|
|||
|
||||
c.whitelist_views = whitelist_views
|
||||
return self._get_template_context(c)
|
||||
|
||||
@LoginRequired()
|
||||
@HasPermissionAllDecorator('hg.admin')
|
||||
@view_config(
|
||||
route_name='admin_permissions_ssh_keys', request_method='GET',
|
||||
renderer='rhodecode:templates/admin/permissions/permissions.mako')
|
||||
def ssh_keys(self):
|
||||
c = self.load_default_context()
|
||||
c.active = 'ssh_keys'
|
||||
return self._get_template_context(c)
|
||||
|
||||
@LoginRequired()
|
||||
@HasPermissionAllDecorator('hg.admin')
|
||||
@view_config(
|
||||
route_name='admin_permissions_ssh_keys_data', request_method='GET',
|
||||
renderer='json_ext', xhr=True)
|
||||
def ssh_keys_data(self):
|
||||
_ = self.request.translate
|
||||
column_map = {
|
||||
'fingerprint': 'ssh_key_fingerprint',
|
||||
'username': User.username
|
||||
}
|
||||
draw, start, limit = self._extract_chunk(self.request)
|
||||
search_q, order_by, order_dir = self._extract_ordering(
|
||||
self.request, column_map=column_map)
|
||||
|
||||
ssh_keys_data_total_count = UserSshKeys.query()\
|
||||
.count()
|
||||
|
||||
# json generate
|
||||
base_q = UserSshKeys.query().join(UserSshKeys.user)
|
||||
|
||||
if search_q:
|
||||
like_expression = u'%{}%'.format(safe_unicode(search_q))
|
||||
base_q = base_q.filter(or_(
|
||||
User.username.ilike(like_expression),
|
||||
UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
|
||||
))
|
||||
|
||||
users_data_total_filtered_count = base_q.count()
|
||||
|
||||
sort_col = self._get_order_col(order_by, UserSshKeys)
|
||||
if sort_col:
|
||||
if order_dir == 'asc':
|
||||
# handle null values properly to order by NULL last
|
||||
if order_by in ['created_on']:
|
||||
sort_col = coalesce(sort_col, datetime.date.max)
|
||||
sort_col = sort_col.asc()
|
||||
else:
|
||||
# handle null values properly to order by NULL last
|
||||
if order_by in ['created_on']:
|
||||
sort_col = coalesce(sort_col, datetime.date.min)
|
||||
sort_col = sort_col.desc()
|
||||
|
||||
base_q = base_q.order_by(sort_col)
|
||||
base_q = base_q.offset(start).limit(limit)
|
||||
|
||||
ssh_keys = base_q.all()
|
||||
|
||||
ssh_keys_data = []
|
||||
for ssh_key in ssh_keys:
|
||||
ssh_keys_data.append({
|
||||
"username": h.gravatar_with_user(self.request, ssh_key.user.username),
|
||||
"fingerprint": ssh_key.ssh_key_fingerprint,
|
||||
"description": ssh_key.description,
|
||||
"created_on": h.format_date(ssh_key.created_on),
|
||||
"action": h.link_to(
|
||||
_('Edit'), h.route_path('edit_user_ssh_keys',
|
||||
user_id=ssh_key.user.user_id))
|
||||
})
|
||||
|
||||
data = ({
|
||||
'draw': draw,
|
||||
'data': ssh_keys_data,
|
||||
'recordsTotal': ssh_keys_data_total_count,
|
||||
'recordsFiltered': users_data_total_filtered_count,
|
||||
})
|
||||
|
||||
return data
|
||||
|
||||
@LoginRequired()
|
||||
@HasPermissionAllDecorator('hg.admin')
|
||||
@CSRFRequired()
|
||||
@view_config(
|
||||
route_name='admin_permissions_ssh_keys_update', request_method='POST',
|
||||
renderer='rhodecode:templates/admin/permissions/permissions.mako')
|
||||
def ssh_keys_update(self):
|
||||
_ = self.request.translate
|
||||
self.load_default_context()
|
||||
|
||||
ssh_enabled = self.request.registry.settings.get(
|
||||
'ssh.generate_authorized_keyfile')
|
||||
key_file = self.request.registry.settings.get(
|
||||
'ssh.authorized_keys_file_path')
|
||||
if ssh_enabled:
|
||||
trigger(SshKeyFileChangeEvent(), self.request.registry)
|
||||
h.flash(_('Updated SSH keys file: {}').format(key_file),
|
||||
category='success')
|
||||
else:
|
||||
h.flash(_('SSH key support is disabled in .ini file'),
|
||||
category='warning')
|
||||
|
||||
raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@ function registerRCRoutes() {
|
|||
pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
|
||||
pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
|
||||
pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
|
||||
pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
|
||||
pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
|
||||
pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
|
||||
pyroutes.register('users', '/_admin/users', []);
|
||||
pyroutes.register('users_data', '/_admin/users_data', []);
|
||||
pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@
|
|||
<li class="${'active' if c.active=='auth_token_access' else ''}">
|
||||
<a href="${h.route_path('admin_permissions_auth_token_access')}">${_('AuthToken Access')}</a>
|
||||
</li>
|
||||
<li class="${'active' if c.active=='ssh_keys' else ''}">
|
||||
<a href="${h.route_path('admin_permissions_ssh_keys')}">${_('SSH Keys')}</a>
|
||||
</li>
|
||||
<li class="${'active' if c.active=='perms' else ''}">
|
||||
<a href="${h.route_path('admin_permissions_overview')}">${_('Overview')}</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">${_('SSH Keys')} - <span id="ssh_keys_count"></span></h3>
|
||||
|
||||
${h.secure_form(h.route_path('admin_permissions_ssh_keys_update'), method='POST', request=request)}
|
||||
<button class="btn btn-link pull-right" type="submit">${_('Update SSH keys file')}</button>
|
||||
${h.end_form()}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
|
||||
|
||||
<div id="repos_list_wrap">
|
||||
<table id="ssh_keys_table" class="display"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
$(document).ready(function() {
|
||||
var $sshKeyListTable = $('#ssh_keys_table');
|
||||
|
||||
var getDatatableCount = function(){
|
||||
var table = $sshKeyListTable.dataTable();
|
||||
var page = table.api().page.info();
|
||||
var active = page.recordsDisplay;
|
||||
var total = page.recordsTotal;
|
||||
|
||||
var _text = _gettext("{0} out of {1} ssh keys").format(active, total);
|
||||
$('#ssh_keys_count').text(_text);
|
||||
};
|
||||
|
||||
// user list
|
||||
$sshKeyListTable.DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
ajax: "${h.route_path('admin_permissions_ssh_keys_data')}",
|
||||
dom: 'rtp',
|
||||
pageLength: ${c.visual.admin_grid_items},
|
||||
order: [[ 0, "asc" ]],
|
||||
columns: [
|
||||
{ data: {"_": "username",
|
||||
"sort": "username"}, title: "${_('Username')}", className: "td-user" },
|
||||
{ data: {"_": "fingerprint",
|
||||
"sort": "fingerprint"}, title: "${_('Fingerprint')}", className: "td-type" },
|
||||
{ data: {"_": "description",
|
||||
"sort": "description"}, title: "${_('Description')}", className: "td-type" },
|
||||
{ data: {"_": "created_on",
|
||||
"sort": "created_on"}, title: "${_('Created on')}", className: "td-time" },
|
||||
{ data: {"_": "action",
|
||||
"sort": "action"}, title: "${_('Action')}", className: "td-action", orderable: false }
|
||||
],
|
||||
language: {
|
||||
paginate: DEFAULT_GRID_PAGINATION,
|
||||
sProcessing: _gettext('loading...'),
|
||||
emptyTable: _gettext("No ssh keys available yet.")
|
||||
},
|
||||
|
||||
"createdRow": function ( row, data, index ) {
|
||||
if (!data['active_raw']){
|
||||
$(row).addClass('closed')
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$sshKeyListTable.on('xhr.dt', function(e, settings, json, xhr){
|
||||
$sshKeyListTable.css('opacity', 1);
|
||||
});
|
||||
|
||||
$sshKeyListTable.on('preXhr.dt', function(e, settings, data){
|
||||
$sshKeyListTable.css('opacity', 0.3);
|
||||
});
|
||||
|
||||
// refresh counters on draw
|
||||
$sshKeyListTable.on('draw.dt', function(){
|
||||
getDatatableCount();
|
||||
});
|
||||
|
||||
// filter
|
||||
$('#q_filter').on('keyup',
|
||||
$.debounce(250, function() {
|
||||
$sshKeyListTable.DataTable().search(
|
||||
$('#q_filter').val()
|
||||
).draw();
|
||||
})
|
||||
);
|
||||
|
||||
});
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue