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
parent
d59816054f
commit
8c3bba65e5
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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'})
|
||||
|
||||
|
|
|
@ -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};
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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'),
|
||||
|
|
|
@ -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'),
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue