sessions: added interface to show, and cleanup user auth sessions.

This commit is contained in:
Marcin Kuzminski 2017-01-11 19:35:42 +01:00
parent 1ba0fb5d27
commit fe5871e9df
8 changed files with 251 additions and 2 deletions

View file

@ -83,8 +83,10 @@ class NavigationRegistry(object):
NavEntry('integrations', _('Integrations'),
'global_integrations_home', pyramid=True),
NavEntry('system', _('System Info'), 'admin_settings_system'),
NavEntry('session', _('User Sessions'), 'admin_settings_sessions'),
NavEntry('open_source', _('Open Source Licenses'),
'admin_settings_open_source', pyramid=True),
# TODO: marcink: we disable supervisor now until the supervisor stats
# page is fixed in the nix configuration
# NavEntry('supervisor', _('Supervisor'), 'admin_settings_supervisor'),

View file

@ -509,6 +509,12 @@ def make_map(config):
m.connect('admin_settings_system_update', '/settings/system/updates',
action='settings_system_update', conditions={'method': ['GET']})
m.connect('admin_settings_sessions', '/settings/sessions',
action='settings_sessions', conditions={'method': ['GET']})
m.connect('admin_settings_sessions_cleanup', '/settings/sessions/cleanup',
action='settings_sessions_cleanup', conditions={'method': ['POST']})
m.connect('admin_settings_supervisor', '/settings/supervisor',
action='settings_supervisor', conditions={'method': ['GET']})
m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',

View file

@ -50,6 +50,8 @@ from rhodecode.lib.utils2 import (
from rhodecode.lib.compat import OrderedDict
from rhodecode.lib.ext_json import json
from rhodecode.lib.utils import jsonify
from rhodecode.lib import system_info
from rhodecode.lib import user_sessions
from rhodecode.model.db import RhodeCodeUi, Repository
from rhodecode.model.forms import ApplicationSettingsForm, \
@ -690,6 +692,52 @@ class SettingsController(BaseController):
return render('admin/settings/settings_system_update.mako')
@HasPermissionAllDecorator('hg.admin')
def settings_sessions(self):
# url('admin_settings_sessions')
c.active = 'sessions'
c.cleanup_older_days = 60
older_than_seconds = 24 * 60 * 60 * 24 * c.cleanup_older_days
config = system_info.rhodecode_config().get_value()['value']['config']
c.session_model = user_sessions.get_session_handler(
config.get('beaker.session.type', 'memory'))(config)
c.session_conf = c.session_model.config
c.session_count = c.session_model.get_count()
c.session_expired_count = c.session_model.get_expired_count(
older_than_seconds)
return render('admin/settings/settings.mako')
@HasPermissionAllDecorator('hg.admin')
def settings_sessions_cleanup(self):
# url('admin_settings_sessions_update')
expire_days = safe_int(request.POST.get('expire_days'))
if expire_days is None:
expire_days = 60
older_than_seconds = 24 * 60 * 60 * 24 * expire_days
config = system_info.rhodecode_config().get_value()['value']['config']
session_model = user_sessions.get_session_handler(
config.get('beaker.session.type', 'memory'))(config)
try:
session_model.clean_sessions(
older_than_seconds=older_than_seconds)
h.flash(_('Cleaned up old sessions'), category='success')
except user_sessions.CleanupCommand as msg:
h.flash(msg, category='warning')
except Exception as e:
log.exception('Failed session cleanup')
h.flash(_('Failed to cleanup up old sessions'), category='error')
return redirect(url('admin_settings_sessions'))
@HasPermissionAllDecorator('hg.admin')
def settings_supervisor(self):
c.rhodecode_ini = rhodecode.CONFIG

View file

@ -64,6 +64,9 @@ class SysInfoRes(object):
'human_value': self.human_value,
}
def get_value(self):
return self.__json__()
def __str__(self):
return '<SysInfoRes({})>'.format(self.__json__())

View file

@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2017-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# 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/
import datetime
import dateutil
from rhodecode.model.db import DbSession, Session
class CleanupCommand(Exception):
pass
class BaseAuthSessions(object):
SESSION_TYPE = None
def __init__(self, config):
session_conf = {}
for k, v in config.items():
if k.startswith('beaker.session'):
session_conf[k] = v
self.config = session_conf
def get_count(self):
raise NotImplementedError
def get_expired_count(self):
raise NotImplementedError
def clean_sessions(self, older_than_seconds=None):
raise NotImplementedError
def _seconds_to_date(self, seconds):
return datetime.datetime.utcnow() - dateutil.relativedelta.relativedelta(
seconds=seconds)
class DbAuthSessions(BaseAuthSessions):
SESSION_TYPE = 'ext:database'
def get_count(self):
return DbSession.query().count()
def get_expired_count(self, older_than_seconds=None):
expiry_date = self._seconds_to_date(older_than_seconds)
return DbSession.query().filter(DbSession.accessed < expiry_date).count()
def clean_sessions(self, older_than_seconds=None):
expiry_date = self._seconds_to_date(older_than_seconds)
DbSession.query().filter(DbSession.accessed < expiry_date).delete()
Session().commit()
class FileAuthSessions(BaseAuthSessions):
SESSION_TYPE = 'file sessions'
def get_count(self):
return 'NOT AVAILABLE'
def get_expired_count(self):
return self.get_count()
def clean_sessions(self, older_than_seconds=None):
data_dir = self.config.get('beaker.session.data_dir')
raise CleanupCommand(
'Please execute this command: '
'`find . -mtime +60 -exec rm {{}} \;` inside {} directory'.format(
data_dir))
class MemcachedAuthSessions(BaseAuthSessions):
SESSION_TYPE = 'ext:memcached'
def get_count(self):
return 'NOT AVAILABLE'
def get_expired_count(self):
return self.get_count()
def clean_sessions(self, older_than_seconds=None):
raise CleanupCommand('Cleanup for this session type not yet available')
class MemoryAuthSessions(BaseAuthSessions):
SESSION_TYPE = 'memory'
def get_count(self):
return 'NOT AVAILABLE'
def get_expired_count(self):
return self.get_count()
def clean_sessions(self, older_than_seconds=None):
raise CleanupCommand('Cleanup for this session type not yet available')
def get_session_handler(session_type):
types = {
'file': FileAuthSessions,
'ext:memcached': MemcachedAuthSessions,
'ext:database': DbAuthSessions,
'memory': MemoryAuthSessions
}
try:
return types[session_type]
except KeyError:
raise ValueError(
'This type {} is not supported'.format(session_type))

View file

@ -3812,7 +3812,12 @@ class DbSession(Base, BaseModel):
{'extend_existing': True, 'mysql_engine': 'InnoDB',
'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
)
def __repr__(self):
return '<DB:DbSession({})>'.format(self.id)
id = Column('id', Integer())
namespace = Column('namespace', String(255), primary_key=True)
accessed = Column('accessed', DateTime, nullable=False)
created = Column('created', DateTime, nullable=False)
data = Column('data', PickleType, nullable=False)
data = Column('data', PickleType, nullable=False)

View file

@ -20,7 +20,7 @@
(_('SMTP auth'), c.rhodecode_ini.get('smtp_auth'), ''),
]
%>
<dl class="dl-horizontal">
<dl class="dl-horizontal settings">
%for dt, dd, tt in elems:
<dt >${dt}:</dt>
<dd title="${tt}">${dd}</dd>

View file

@ -0,0 +1,60 @@
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">${_('User Sessions Configuration')}</h3>
</div>
<div class="panel-body">
<%
elems = [
(_('Session type'), c.session_model.SESSION_TYPE, ''),
(_('Session expiration period'), '{} seconds'.format(c.session_conf.get('beaker.session.timeout', 0)), ''),
(_('Total sessions'), c.session_count, ''),
(_('Expired sessions ({} days)').format(c.cleanup_older_days ), c.session_expired_count, ''),
]
%>
<dl class="dl-horizontal settings">
%for dt, dd, tt in elems:
<dt>${dt}:</dt>
<dd title="${tt}">${dd}</dd>
%endfor
</dl>
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">${_('Cleanup Old Sessions')}</h3>
</div>
<div class="panel-body">
${h.secure_form(h.url('admin_settings_sessions_cleanup'), method='post')}
<div style="margin: 0 0 20px 0" class="fake-space">
${_('Cleanup all sessions that were not active during choosen time frame')} <br/>
${_('Picking All will log-out all users in the system, and each user will be required to log in again.')}
</div>
<select id="expire_days" name="expire_days">
% for n in [60, 90, 30, 7, 0]:
<option value="${n}">${'{} days'.format(n) if n != 0 else 'All'}</option>
% endfor
</select>
<button class="btn btn-small" type="submit"
onclick="return confirm('${_('Confirm to cleanup user sessions')}');">
${_('Cleanup sessions')}
</button>
${h.end_form()}
</div>
</div>
<script type="text/javascript">
$(document).ready(function() {
$('#expire_days').select2({
containerCssClass: 'drop-menu',
dropdownCssClass: 'drop-menu-dropdown',
dropdownAutoWidth: true,
minimumResultsForSearch: -1
});
});
</script>