Logout the pgAdmin session when no user activity of mouse move, click or keypress. Fixes #5000.

Introduced two config params:
1. USER_INACTIVITY_TIMEOUT - Interval in seconds for the timeout. Default is 0-Zero which means disabled.
2. OVERRIDE_USER_INACTIVITY_TIMEOUT - If set to true, tools like query tool or debugger will override USER_INACTIVITY_TIMEOUT
   and will not allow the application to timeout if a query is running for a long time.
pull/27/head
Aditya Toshniwal 2020-01-15 18:07:46 +05:30 committed by Akshay Joshi
parent d59816054f
commit 8c3bba65e5
12 changed files with 346 additions and 3 deletions

View File

@ -22,5 +22,6 @@ Bug fixes
| `Issue #3812 <https://redmine.postgresql.org/issues/3812>`_ - Ensure that path file name should not disappear when changing ext from the dropdown in file explorer dialog.
| `Issue #4827 <https://redmine.postgresql.org/issues/4827>`_ - Fix column resizable issue in the file explorer dialog.
| `Issue #5000 <https://redmine.postgresql.org/issues/5000>`_ - Logout the pgAdmin session when no user activity of mouse move, click or keypress.
| `Issue #5025 <https://redmine.postgresql.org/issues/5025>`_ - Fix an issue where setting STORAGE_DIR to empty should show all the volumes on Windows in server mode.
| `Issue #5074 <https://redmine.postgresql.org/issues/5074>`_ - Fix an issue where select, insert and update scripts on tables throwing an error.

View File

@ -447,6 +447,22 @@ SESSION_EXPIRATION_TIME = 1
# the session files for cleanup after specified number of *hours*.
CHECK_SESSION_FILES_INTERVAL = 24
# USER_INACTIVITY_TIMEOUT is interval in Seconds. If the pgAdmin screen is left
# unattended for <USER_INACTIVITY_TIMEOUT> seconds then the user will
# be logged out. When set to 0, the timeout will be disabled.
# If pgAdmin doesn't detect any activity in the time specified (in seconds),
# the user will be forcibly logged out from pgAdmin. Set to zero to disable
# the timeout.
# Note: This is applicable only for SERVER_MODE=True.
USER_INACTIVITY_TIMEOUT = 0
# OVERRIDE_USER_INACTIVITY_TIMEOUT when set to True will override
# USER_INACTIVITY_TIMEOUT when long running queries in the Query Tool
# or Debugger are running. When the queries complete, the inactivity timer
# will restart in this case. If set to False, user inactivity may cause
# transactions or in-process debugging sessions to be aborted.
OVERRIDE_USER_INACTIVITY_TIMEOUT = True
##########################################################################
# SSH Tunneling supports only for Python 2.7 and 3.4+
##########################################################################
@ -495,3 +511,7 @@ if (SUPPORT_SSH_TUNNEL is True and
(sys.version_info[0] == 3 and sys.version_info[1] < 4))):
SUPPORT_SSH_TUNNEL = False
ALLOW_SAVE_TUNNEL_PASSWORD = False
# Disable USER_INACTIVITY_TIMEOUT when SERVER_MODE=False
if not SERVER_MODE:
USER_INACTIVITY_TIMEOUT = 0

View File

@ -520,6 +520,11 @@ class BrowserPluginModule(PgAdminModule):
)
def _get_logout_url():
return '{0}?next={1}'.format(
url_for('security.logout'), url_for('browser.index'))
@blueprint.route("/")
@pgCSRFProtect.exempt
@login_required
@ -579,6 +584,7 @@ def index():
MODULE_NAME + "/index.html",
username=current_user.email,
is_admin=current_user.has_role("Administrator"),
logout_url=_get_logout_url(),
_=gettext
))
@ -666,7 +672,8 @@ def utils():
editor_indent_with_tabs=editor_indent_with_tabs,
app_name=config.APP_NAME,
pg_libpq_version=pg_libpq_version,
support_ssh_tunnel=config.SUPPORT_SSH_TUNNEL
support_ssh_tunnel=config.SUPPORT_SSH_TUNNEL,
logout_url=_get_logout_url()
),
200, {'Content-Type': 'application/javascript'})

View File

@ -0,0 +1,114 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import $ from 'jquery';
import _ from 'underscore';
import pgAdmin from 'sources/pgadmin';
import pgWindow from 'sources/window';
import {getEpoch} from 'sources/utils';
const pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};
const MIN_ACTIVITY_TIME_UNIT = 1000; /* in seconds */
/*
* User UI activity related functions.
*/
_.extend(pgBrowser, {
inactivity_timeout_at: null,
logging_activity: false,
inactivity_timeout_daemon_running: false,
is_pgadmin_timedout: function() {
return !pgWindow.pgAdmin;
},
is_inactivity_timeout: function() {
return pgWindow.pgAdmin.Browser.inactivity_timeout_at < this.get_epoch_now();
},
get_epoch_now: function(){
return getEpoch();
},
log_activity: function() {
if(!this.logging_activity) {
this.logging_activity = true;
this.inactivity_timeout_at = this.get_epoch_now() + pgAdmin.user_inactivity_timeout;
/* No need to log events till next MIN_ACTIVITY_TIME_UNIT second as the
* value of inactivity_timeout_at won't change
*/
setTimeout(()=>{
this.logging_activity = false;
}, MIN_ACTIVITY_TIME_UNIT);
}
},
/* Call this to register element for acitivity monitoring
* Generally, document is passed to cover all.
*/
register_to_activity_listener: function(target, timeout_callback) {
let inactivity_events = ['mousemove', 'mousedown', 'keydown'];
let self = this;
inactivity_events.forEach((event)=>{
/* Bind events in the event capture phase, the bubble phase might stop propagation */
let eventHandler = function() {
if(self.is_pgadmin_timedout()) {
/* If the main page has logged out then remove the listener and call the timeout callback */
inactivity_events.forEach((event)=>{
target.removeEventListener(event, eventHandler, true);
});
timeout_callback();
} else {
pgWindow.pgAdmin.Browser.log_activity();
}
};
target.addEventListener(event, eventHandler, true);
});
},
/*
* This function can be used by tools like sqleditor where
* poll call is as good as user activity. Decorate such functions
* with this to consider them as events. Note that, this is controlled
* by override_user_inactivity_timeout.
*/
override_activity_event_decorator: function(input_func) {
return function() {
/* Log only if override_user_inactivity_timeout true */
if(pgAdmin.override_user_inactivity_timeout) {
pgWindow.pgAdmin.Browser.log_activity();
}
return input_func.apply(this, arguments);
};
},
logout_inactivity_user: function() {
window.location.href = pgBrowser.utils.logout_url;
},
/* The daemon will track and logout when timeout occurs */
start_inactivity_timeout_daemon: function() {
let self = this;
if(pgAdmin.user_inactivity_timeout > 0 && !self.inactivity_timeout_daemon_running) {
let timeout_daemon_id = setInterval(()=>{
self.inactivity_timeout_daemon_running = true;
if(self.is_inactivity_timeout()) {
clearInterval(timeout_daemon_id);
self.inactivity_timeout_daemon_running = false;
$(window).off('beforeunload');
self.logout_inactivity_user();
}
}, MIN_ACTIVITY_TIME_UNIT);
}
},
});
export {pgBrowser};

View File

@ -17,7 +17,7 @@ define('pgadmin.browser', [
'pgadmin.browser.preferences', 'pgadmin.browser.messages',
'pgadmin.browser.menu', 'pgadmin.browser.panel', 'pgadmin.browser.layout',
'pgadmin.browser.error', 'pgadmin.browser.frame',
'pgadmin.browser.node', 'pgadmin.browser.collection',
'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity',
'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state',
], function(
@ -547,6 +547,11 @@ define('pgadmin.browser', [
obj.Events.on('pgadmin-browser:tree:loadfail', obj.onLoadFailNode, obj);
obj.bind_beforeunload();
/* User UI activity */
obj.log_activity(); /* The starting point */
obj.register_to_activity_listener(document);
obj.start_inactivity_timeout_daemon();
},
init_master_password: function() {

View File

@ -154,7 +154,7 @@ window.onload = function(e){
<li><a class="dropdown-item" href="#" onclick="pgAdmin.Browser.UserManagement.show_users()">{{ _('Users') }}</a></li>
<li class="dropdown-divider"></li>
{% endif %}
<li><a class="dropdown-item" href="{{ url_for('security.logout') }}?next={{url_for('browser.index')}}">{{ _('Logout') }}</a></li>
<li><a class="dropdown-item" href="{{ logout_url }}">{{ _('Logout') }}</a></li>
</ul>
</li>
</ul>

View File

@ -45,6 +45,10 @@ define('pgadmin.browser.utils',
pgAdmin['csrf_token_header'] = '{{ current_app.config.get('WTF_CSRF_HEADERS')[0] }}';
pgAdmin['csrf_token'] = '{{ csrf_token() }}';
/* Get the inactivity related config */
pgAdmin['user_inactivity_timeout'] = {{ current_app.config.get('USER_INACTIVITY_TIMEOUT') }};
pgAdmin['override_user_inactivity_timeout'] = '{{ current_app.config.get('OVERRIDE_USER_INACTIVITY_TIMEOUT') }}' == 'True';
// Define list of nodes on which Query tool option doesn't appears
var unsupported_nodes = pgAdmin.unsupported_nodes = [
'server_group', 'server', 'coll-tablespace', 'tablespace',
@ -65,6 +69,7 @@ define('pgadmin.browser.utils',
app_name: '{{ app_name }}',
pg_libpq_version: {{pg_libpq_version|e}},
support_ssh_tunnel: '{{ support_ssh_tunnel }}' == 'True',
logout_url: '{{logout_url}}',
counter: {total: 0, loaded: 0},
registerScripts: function (ctx) {

View File

@ -1891,6 +1891,14 @@ define([
pgWindow.default.pgAdmin.Browser.onPreferencesChange('debugger', function() {
self.reflectPreferences();
});
/* Register to log the activity */
pgBrowser.register_to_activity_listener(document, ()=>{
Alertify.alert(gettext('Timeout'), gettext('Your session has timed out due to inactivity. Kindly close the window and login again.'));
});
controller.poll_result = pgBrowser.override_activity_event_decorator(controller.poll_result).bind(controller);
controller.poll_end_execution_result = pgBrowser.override_activity_event_decorator(controller.poll_end_execution_result).bind(controller);
},
reflectPreferences: function() {
let self = this,

View File

@ -659,6 +659,11 @@ define('tools.querytool', [
}
}, 1000);
}
/* Register to log the activity */
pgBrowser.register_to_activity_listener(document, ()=>{
alertify.alert(gettext('Timeout'), gettext('Your session has timed out due to inactivity. Kindly close the window and login again.'));
});
},
/* Regarding SlickGrid usage in render_grid function.
@ -2508,6 +2513,7 @@ define('tools.querytool', [
}
const executeQuery = new ExecuteQuery.ExecuteQuery(this, pgAdmin.Browser.UserManagement);
executeQuery.poll = pgBrowser.override_activity_event_decorator(executeQuery.poll).bind(executeQuery);
executeQuery.execute(sql, explain_prefix, shouldReconnect);
},

View File

@ -0,0 +1,174 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import {pgBrowser} from 'pgadmin.browser.activity';
import { getEpoch } from 'sources/utils';
import pgAdmin from 'sources/pgadmin';
describe('For Activity', function(){
beforeEach(function(){
pgAdmin.user_inactivity_timeout = 60;
pgAdmin.override_user_inactivity_timeout = true;
/* pgBrowser here is same as main window Browser */
window.pgAdmin = {
Browser: pgBrowser,
};
});
describe('is_pgadmin_timedout', function(){
it('when not timedout', function(){
expect(pgBrowser.is_pgadmin_timedout()).toEqual(false);
});
it('when timedout', function(){
window.pgAdmin = undefined;
expect(pgBrowser.is_pgadmin_timedout()).toEqual(true);
});
});
describe('is_inactivity_timeout', function(){
it('when there is activity', function(){
window.pgAdmin.Browser.inactivity_timeout_at = getEpoch() + 30;
expect(pgBrowser.is_inactivity_timeout()).toEqual(false);
});
it('when there is no activity', function(){
window.pgAdmin.Browser.inactivity_timeout_at = getEpoch() - 30;
expect(pgBrowser.is_inactivity_timeout()).toEqual(true);
});
});
describe('log_activity', function(){
beforeEach(function(){
spyOn(pgBrowser, 'get_epoch_now').and.callThrough();
spyOn(pgBrowser, 'log_activity').and.callThrough();
pgBrowser.logging_activity = false;
});
it('initial log activity', function(){
pgBrowser.log_activity();
expect(window.pgAdmin.Browser.inactivity_timeout_at).not.toBe(null);
expect(pgBrowser.get_epoch_now).toHaveBeenCalled();
});
it('multiple log activity within a second', function(){
/* First call */
pgBrowser.log_activity();
expect(pgBrowser.get_epoch_now).toHaveBeenCalled();
expect(pgBrowser.logging_activity).toEqual(true);
/* Second call */
pgBrowser.get_epoch_now.calls.reset();
pgBrowser.log_activity();
expect(pgBrowser.get_epoch_now).not.toHaveBeenCalled();
});
it('set loggin to false after timeout', function(done){
pgBrowser.log_activity();
expect(pgBrowser.logging_activity).toEqual(true);
setTimeout(()=>{
expect(pgBrowser.logging_activity).toEqual(false);
done();
}, 1001);
});
});
describe('register_to_activity_listener', function(){
let target = document;
let timeout_callback = jasmine.createSpy();
let event = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
view: window,
});
beforeEach(function(){
spyOn(pgBrowser, 'log_activity');
spyOn(target, 'addEventListener').and.callThrough();
spyOn(target, 'removeEventListener').and.callThrough();
pgBrowser.register_to_activity_listener(target, timeout_callback);
});
it('function called', function(){
expect(target.addEventListener).toHaveBeenCalled();
});
it('event triggered', function(done){
target.dispatchEvent(event);
setTimeout(()=>{
expect(pgBrowser.log_activity).toHaveBeenCalled();
done();
}, 250);
});
it('is timed out', function(done){
spyOn(pgBrowser, 'is_pgadmin_timedout').and.returnValue(true);
target.dispatchEvent(event);
setTimeout(()=>{
expect(timeout_callback).toHaveBeenCalled();
expect(target.removeEventListener).toHaveBeenCalled();
done();
}, 250);
});
});
describe('override_activity_event_decorator', function(){
let input_func = jasmine.createSpy('input_func');
let decorate_func = pgBrowser.override_activity_event_decorator(input_func);
beforeEach(function(){
spyOn(pgBrowser, 'log_activity').and.callThrough();
});
it('call the input_func', function(){
decorate_func();
expect(input_func).toHaveBeenCalled();
});
it('log activity when override_user_inactivity_timeout true', function(){
decorate_func();
expect(pgBrowser.log_activity).toHaveBeenCalled();
});
it('do not log activity when override_user_inactivity_timeout true', function(){
pgAdmin.override_user_inactivity_timeout = false;
decorate_func();
expect(pgBrowser.log_activity).not.toHaveBeenCalled();
});
});
describe('start_inactivity_timeout_daemon', function(){
beforeEach(function(){
spyOn(pgBrowser, 'logout_inactivity_user');
});
it('start the daemon', function(done){
spyOn(pgBrowser, 'is_inactivity_timeout').and.returnValue(false);
pgBrowser.inactivity_timeout_daemon_running = false;
pgBrowser.start_inactivity_timeout_daemon();
setTimeout(()=>{
expect(pgBrowser.inactivity_timeout_daemon_running).toEqual(true);
done();
}, 1001);
});
it('stop the daemon', function(done){
spyOn(pgBrowser, 'is_inactivity_timeout').and.returnValue(true);
pgBrowser.inactivity_timeout_daemon_running = false;
pgBrowser.start_inactivity_timeout_daemon();
setTimeout(()=>{
expect(pgBrowser.inactivity_timeout_daemon_running).toEqual(false);
expect(pgBrowser.logout_inactivity_user).toHaveBeenCalled();
done();
}, 1001);
});
});
});

View File

@ -189,6 +189,7 @@ var webpackShimConfig = {
'pgadmin.browser.layout': path.join(__dirname, './pgadmin/browser/static/js/layout'),
'pgadmin.browser.preferences': path.join(__dirname, './pgadmin/browser/static/js/preferences'),
'pgadmin.browser.menu': path.join(__dirname, './pgadmin/browser/static/js/menu'),
'pgadmin.browser.activity': path.join(__dirname, './pgadmin/browser/static/js/activity'),
'pgadmin.browser.messages': '/browser/js/messages',
'pgadmin.browser.node': path.join(__dirname, './pgadmin/browser/static/js/node'),
'pgadmin.browser.node.ui': path.join(__dirname, './pgadmin/browser/static/js/node.ui'),

View File

@ -16,6 +16,7 @@ const nodeModulesDir = path.resolve(__dirname, 'node_modules');
const regressionDir = path.resolve(__dirname, 'regression');
module.exports = {
mode: 'development',
devtool: 'inline-source-map',
plugins: [
new webpack.ProvidePlugin({
@ -102,6 +103,7 @@ module.exports = {
'pgadmin.schema.dir': path.resolve(__dirname, 'pgadmin/browser/server_groups/servers/databases/schemas/static/js'),
'pgadmin.browser.layout': path.join(__dirname, './pgadmin/browser/static/js/layout'),
'pgadmin.browser.preferences': path.join(__dirname, './pgadmin/browser/static/js/preferences'),
'pgadmin.browser.activity': path.join(__dirname, './pgadmin/browser/static/js/activity'),
'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'),
},
},