diff --git a/rhodecode/admin/navigation.py b/rhodecode/admin/navigation.py --- a/rhodecode/admin/navigation.py +++ b/rhodecode/admin/navigation.py @@ -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'), diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -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', diff --git a/rhodecode/controllers/admin/settings.py b/rhodecode/controllers/admin/settings.py --- a/rhodecode/controllers/admin/settings.py +++ b/rhodecode/controllers/admin/settings.py @@ -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, \ @@ -691,6 +693,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 c.active = 'supervisor' diff --git a/rhodecode/lib/system_info.py b/rhodecode/lib/system_info.py --- a/rhodecode/lib/system_info.py +++ b/rhodecode/lib/system_info.py @@ -64,6 +64,9 @@ class SysInfoRes(object): 'human_value': self.human_value, } + def get_value(self): + return self.__json__() + def __str__(self): return ''.format(self.__json__()) diff --git a/rhodecode/lib/user_sessions.py b/rhodecode/lib/user_sessions.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/user_sessions.py @@ -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 . +# +# 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)) diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -3812,7 +3812,12 @@ class DbSession(Base, BaseModel): {'extend_existing': True, 'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, ) + + def __repr__(self): + return ''.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) \ No newline at end of file + data = Column('data', PickleType, nullable=False) diff --git a/rhodecode/templates/admin/settings/settings_email.mako b/rhodecode/templates/admin/settings/settings_email.mako --- a/rhodecode/templates/admin/settings/settings_email.mako +++ b/rhodecode/templates/admin/settings/settings_email.mako @@ -20,7 +20,7 @@ (_('SMTP auth'), c.rhodecode_ini.get('smtp_auth'), ''), ] %> -
+
%for dt, dd, tt in elems:
${dt}:
${dd}
diff --git a/rhodecode/templates/admin/settings/settings_sessions.mako b/rhodecode/templates/admin/settings/settings_sessions.mako new file mode 100644 --- /dev/null +++ b/rhodecode/templates/admin/settings/settings_sessions.mako @@ -0,0 +1,60 @@ +
+
+

${_('User Sessions Configuration')}

+
+
+ <% + 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, ''), + + ] + %> +
+ %for dt, dd, tt in elems: +
${dt}:
+
${dd}
+ %endfor +
+
+
+ + +
+
+

${_('Cleanup Old Sessions')}

+
+
+ ${h.secure_form(h.url('admin_settings_sessions_cleanup'), method='post')} + +
+ ${_('Cleanup all sessions that were not active during choosen time frame')}
+ ${_('Picking All will log-out all users in the system, and each user will be required to log in again.')} +
+ + + ${h.end_form()} +
+
+ + + \ No newline at end of file