From 9a1b92fbc2c338d8829900845a7f44b7bef14512 Mon Sep 17 00:00:00 2001 From: Khanh Ngo Date: Tue, 17 Dec 2019 23:46:34 +0700 Subject: [PATCH 1/3] Add session timeout warning --- package.json | 5 +- powerdnsadmin/assets.py | 76 ++++++++--------- powerdnsadmin/routes/index.py | 6 ++ powerdnsadmin/static/custom/js/custom.js | 17 ++++ powerdnsadmin/templates/base.html | 102 +++++++++++++++++++++++ yarn.lock | 7 ++ 6 files changed, 170 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index d5bf222..cc73539 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,11 @@ "admin-lte": "2.4.9", "bootstrap": "^3.4.1", "bootstrap-validator": "^0.11.9", + "datatables.net-plugins": "^1.10.19", "icheck": "^1.0.2", "jquery-slimscroll": "^1.3.8", "jquery-ui-dist": "^1.12.1", - "multiselect": "^0.9.12", - "datatables.net-plugins": "^1.10.19" + "jtimeout": "^3.1.0", + "multiselect": "^0.9.12" } } diff --git a/powerdnsadmin/assets.py b/powerdnsadmin/assets.py index 8f76086..cb81ba1 100644 --- a/powerdnsadmin/assets.py +++ b/powerdnsadmin/assets.py @@ -1,5 +1,6 @@ from flask_assets import Bundle, Environment, Filter + class ConcatFilter(Filter): """ Filter that merges files, placing a semicolon between them. @@ -10,28 +11,23 @@ class ConcatFilter(Filter): def concat(self, out, hunks, **kw): out.write(';'.join([h.data() for h, info in hunks])) -css_login = Bundle( - 'node_modules/bootstrap/dist/css/bootstrap.css', - 'node_modules/font-awesome/css/font-awesome.css', - 'node_modules/ionicons/dist/css/ionicons.css', - 'node_modules/icheck/skins/square/blue.css', - 'node_modules/admin-lte/dist/css/AdminLTE.css', - filters=('cssmin','cssrewrite'), - output='generated/login.css' -) -js_login = Bundle( - 'node_modules/jquery/dist/jquery.js', - 'node_modules/bootstrap/dist/js/bootstrap.js', - 'node_modules/icheck/icheck.js', - filters=(ConcatFilter, 'jsmin'), - output='generated/login.js' -) +css_login = Bundle('node_modules/bootstrap/dist/css/bootstrap.css', + 'node_modules/font-awesome/css/font-awesome.css', + 'node_modules/ionicons/dist/css/ionicons.css', + 'node_modules/icheck/skins/square/blue.css', + 'node_modules/admin-lte/dist/css/AdminLTE.css', + filters=('cssmin', 'cssrewrite'), + output='generated/login.css') -js_validation = Bundle( - 'node_modules/bootstrap-validator/dist/validator.js', - output='generated/validation.js' -) +js_login = Bundle('node_modules/jquery/dist/jquery.js', + 'node_modules/bootstrap/dist/js/bootstrap.js', + 'node_modules/icheck/icheck.js', + filters=(ConcatFilter, 'jsmin'), + output='generated/login.js') + +js_validation = Bundle('node_modules/bootstrap-validator/dist/validator.js', + output='generated/validation.js') css_main = Bundle( 'node_modules/bootstrap/dist/css/bootstrap.css', @@ -43,28 +39,26 @@ css_main = Bundle( 'node_modules/admin-lte/dist/css/AdminLTE.css', 'node_modules/admin-lte/dist/css/skins/_all-skins.css', 'custom/css/custom.css', - filters=('cssmin','cssrewrite'), - output='generated/main.css' -) + filters=('cssmin', 'cssrewrite'), + output='generated/main.css') -js_main = Bundle( - 'node_modules/jquery/dist/jquery.js', - 'node_modules/jquery-ui-dist/jquery-ui.js', - 'node_modules/bootstrap/dist/js/bootstrap.js', - 'node_modules/datatables.net/js/jquery.dataTables.js', - 'node_modules/datatables.net-bs/js/dataTables.bootstrap.js', - 'node_modules/jquery-sparkline/jquery.sparkline.js', - 'node_modules/jquery-slimscroll/jquery.slimscroll.js', - 'node_modules/icheck/icheck.js', - 'node_modules/fastclick/lib/fastclick.js', - 'node_modules/moment/moment.js', - 'node_modules/admin-lte/dist/js/adminlte.js', - 'node_modules/multiselect/js/jquery.multi-select.js', - 'node_modules/datatables.net-plugins/sorting/natural.js', - 'custom/js/custom.js', - filters=(ConcatFilter, 'jsmin'), - output='generated/main.js' -) +js_main = Bundle('node_modules/jquery/dist/jquery.js', + 'node_modules/jquery-ui-dist/jquery-ui.js', + 'node_modules/bootstrap/dist/js/bootstrap.js', + 'node_modules/datatables.net/js/jquery.dataTables.js', + 'node_modules/datatables.net-bs/js/dataTables.bootstrap.js', + 'node_modules/jquery-sparkline/jquery.sparkline.js', + 'node_modules/jquery-slimscroll/jquery.slimscroll.js', + 'node_modules/icheck/icheck.js', + 'node_modules/fastclick/lib/fastclick.js', + 'node_modules/moment/moment.js', + 'node_modules/admin-lte/dist/js/adminlte.js', + 'node_modules/multiselect/js/jquery.multi-select.js', + 'node_modules/datatables.net-plugins/sorting/natural.js', + 'node_modules/jtimeout/src/jTimeout.js', + 'custom/js/custom.js', + filters=(ConcatFilter, 'jsmin'), + output='generated/main.js') assets = Environment() assets.register('js_login', js_login) diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py index 8539ef5..de36314 100644 --- a/powerdnsadmin/routes/index.py +++ b/powerdnsadmin/routes/index.py @@ -82,6 +82,12 @@ def index(): return redirect(url_for('dashboard.dashboard')) +@index_bp.route('/ping', methods=['GET']) +@login_required +def ping(): + return make_response('ok') + + @index_bp.route('/google/login') def google_login(): if not Setting().get('google_oauth_enabled') or google is None: diff --git a/powerdnsadmin/static/custom/js/custom.js b/powerdnsadmin/static/custom/js/custom.js index 81d73fb..297cd96 100644 --- a/powerdnsadmin/static/custom/js/custom.js +++ b/powerdnsadmin/static/custom/js/custom.js @@ -268,3 +268,20 @@ json_library = { .replace(jsonLine, json_library.replacer); } }; + +// set count down in second on an element +function timer(elToUpdate, maxTime) { + elToUpdate.text(maxTime + "s"); + + var interval = setInterval(function () { + if (maxTime > 0) { + maxTime--; + elToUpdate.text(maxTime + "s"); + } + else { + clearInterval(interval); + } + }, 1000); + + return interval; +} \ No newline at end of file diff --git a/powerdnsadmin/templates/base.html b/powerdnsadmin/templates/base.html index 7827048..4542d7a 100644 --- a/powerdnsadmin/templates/base.html +++ b/powerdnsadmin/templates/base.html @@ -174,6 +174,84 @@ {% block scripts %} {% assets "js_main" -%} + {%- endassets %} {% endblock %} {% block extrascripts %} @@ -225,6 +303,30 @@ + + + {% endblock %} {% block modals %} {% endblock %} diff --git a/yarn.lock b/yarn.lock index 02be4aa..ed0f963 100644 --- a/yarn.lock +++ b/yarn.lock @@ -701,6 +701,13 @@ jsonparse@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" +jtimeout@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jtimeout/-/jtimeout-3.1.0.tgz#4cd65b28eff8b9f8c61d08889a9ac3abdf5d9893" + integrity sha512-xA2TlImMGj4c0yAiM9BUq+8aAFVYVYUX2tkcC8u8das9qoZSs13SxhVcfWqI4cHOsv3huj2D0VRNHeVCLO3mVQ== + dependencies: + jquery ">=1.7.1 <4.0.0" + jvectormap@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/jvectormap/-/jvectormap-1.2.2.tgz#2e4408b24a60473ff106c1e7243e375ae5ca85da" From 7205b4a01b2b5593b9c7915538a61192ea455df7 Mon Sep 17 00:00:00 2001 From: Khanh Ngo Date: Wed, 18 Dec 2019 15:25:20 +0700 Subject: [PATCH 2/3] User session improvement - Add session handler on other blueprint's before request - Adjustment in using jTimeout to close warning popup on other tabs when we extend the session --- powerdnsadmin/models/setting.py | 1 + powerdnsadmin/routes/admin.py | 16 +++++++++++++-- powerdnsadmin/routes/api.py | 15 +++++++++++--- powerdnsadmin/routes/dashboard.py | 27 ++++++++++++++++++++++--- powerdnsadmin/routes/domain.py | 27 ++++++++++++++++++++++--- powerdnsadmin/routes/user.py | 28 +++++++++++++++++++++++--- powerdnsadmin/templates/base.html | 33 ++++++++++++++++++++++--------- 7 files changed, 124 insertions(+), 23 deletions(-) diff --git a/powerdnsadmin/models/setting.py b/powerdnsadmin/models/setting.py index 6f44663..376dbbd 100644 --- a/powerdnsadmin/models/setting.py +++ b/powerdnsadmin/models/setting.py @@ -26,6 +26,7 @@ class Setting(db.Model): 'bg_domain_updates': False, 'site_name': 'PowerDNS-Admin', 'session_timeout': 10, + 'warn_session_timeout': True, 'pdns_api_url': '', 'pdns_api_key': '', 'pdns_api_timeout': 30, diff --git a/powerdnsadmin/routes/admin.py b/powerdnsadmin/routes/admin.py index defe591..efcaac5 100644 --- a/powerdnsadmin/routes/admin.py +++ b/powerdnsadmin/routes/admin.py @@ -1,7 +1,8 @@ import json +import datetime import traceback from ast import literal_eval -from flask import Blueprint, render_template, make_response, url_for, current_app, request, redirect, jsonify, abort, flash +from flask import Blueprint, render_template, make_response, url_for, current_app, request, redirect, jsonify, abort, flash, session from flask_login import login_required, current_user from ..decorators import operator_role_required, admin_role_required @@ -23,6 +24,16 @@ admin_bp = Blueprint('admin', url_prefix='/admin') +@admin_bp.before_request +def before_request(): + # Manage session timeout + session.permanent = True + current_app.permanent_session_lifetime = datetime.timedelta( + minutes=int(Setting().get('session_timeout'))) + session.modified = True + + + @admin_bp.route('/pdns', methods=['GET']) @login_required @operator_role_required @@ -489,7 +500,8 @@ def setting_basic(): 'default_domain_table_size', 'auto_ptr', 'record_quick_edit', 'pretty_ipv6_ptr', 'dnssec_admins_only', 'allow_user_create_domain', 'bg_domain_updates', 'site_name', - 'session_timeout', 'ttl_options', 'pdns_api_timeout' + 'session_timeout', 'warn_session_timeout', 'ttl_options', + 'pdns_api_timeout' ] return render_template('admin_setting_basic.html', settings=settings) diff --git a/powerdnsadmin/routes/api.py b/powerdnsadmin/routes/api.py index df5567b..517263c 100644 --- a/powerdnsadmin/routes/api.py +++ b/powerdnsadmin/routes/api.py @@ -1,6 +1,6 @@ import json from urllib.parse import urljoin -from flask import Blueprint, g, request, abort, current_app +from flask import Blueprint, g, request, abort, current_app, make_response, jsonify from flask_login import current_user from ..models.base import db @@ -88,7 +88,16 @@ def handle_request_is_not_json(err): @api_bp.before_request @is_json def before_request(): - pass + # Check site is in maintenance mode + maintenance = Setting().get('maintenance') + if maintenance and current_user.is_authenticated and current_user.role.name not in [ + 'Administrator', 'Operator' + ]: + return make_response( + jsonify({ + "status": False, + "msg": "Site is in maintenance mode" + })) @api_bp.route('/pdnsadmin/zones', methods=['POST']) @@ -281,7 +290,7 @@ def api_get_apikeys(domain_name): if current_user.role.name not in ['Administrator', 'Operator']: if domain_name: msg = "Check if domain {0} exists and \ - is allowed for user." .format(domain_name) + is allowed for user." .format(domain_name) current_app.logger.debug(msg) apikeys = current_user.get_apikeys(domain_name) diff --git a/powerdnsadmin/routes/dashboard.py b/powerdnsadmin/routes/dashboard.py index bae0f1e..e04c6de 100644 --- a/powerdnsadmin/routes/dashboard.py +++ b/powerdnsadmin/routes/dashboard.py @@ -1,9 +1,10 @@ -from flask import Blueprint, render_template, url_for, current_app, request, jsonify, redirect -from flask_login import login_required, current_user +import datetime +from flask import Blueprint, render_template, url_for, current_app, request, jsonify, redirect, g, session +from flask_login import login_required, current_user, login_manager from sqlalchemy import not_ from ..lib.utils import customBoxes -from ..models.user import User +from ..models.user import User, Anonymous from ..models.account import Account from ..models.account_user import AccountUser from ..models.domain import Domain @@ -19,6 +20,26 @@ dashboard_bp = Blueprint('dashboard', url_prefix='/dashboard') +@dashboard_bp.before_request +def before_request(): + # Check if user is anonymous + g.user = current_user + login_manager.anonymous_user = Anonymous + + # Check site is in maintenance mode + maintenance = Setting().get('maintenance') + if maintenance and current_user.is_authenticated and current_user.role.name not in [ + 'Administrator', 'Operator' + ]: + return render_template('maintenance.html') + + # Manage session timeout + session.permanent = True + current_app.permanent_session_lifetime = datetime.timedelta( + minutes=int(Setting().get('session_timeout'))) + session.modified = True + + @dashboard_bp.route('/domains-custom/', methods=['GET']) @login_required def domains_custom(boxId): diff --git a/powerdnsadmin/routes/domain.py b/powerdnsadmin/routes/domain.py index b4962f6..ccf5736 100644 --- a/powerdnsadmin/routes/domain.py +++ b/powerdnsadmin/routes/domain.py @@ -1,15 +1,16 @@ import re import json +import datetime import traceback import dns.name import dns.reversename from distutils.version import StrictVersion -from flask import Blueprint, render_template, make_response, url_for, current_app, request, redirect, abort, jsonify -from flask_login import login_required, current_user +from flask import Blueprint, render_template, make_response, url_for, current_app, request, redirect, abort, jsonify, g, session +from flask_login import login_required, current_user, login_manager from ..lib.utils import pretty_json from ..decorators import can_create_domain, operator_role_required, can_access_domain, can_configure_dnssec -from ..models.user import User +from ..models.user import User, Anonymous from ..models.account import Account from ..models.setting import Setting from ..models.history import History @@ -26,6 +27,26 @@ domain_bp = Blueprint('domain', url_prefix='/domain') +@domain_bp.before_request +def before_request(): + # Check if user is anonymous + g.user = current_user + login_manager.anonymous_user = Anonymous + + # Check site is in maintenance mode + maintenance = Setting().get('maintenance') + if maintenance and current_user.is_authenticated and current_user.role.name not in [ + 'Administrator', 'Operator' + ]: + return render_template('maintenance.html') + + # Manage session timeout + session.permanent = True + current_app.permanent_session_lifetime = datetime.timedelta( + minutes=int(Setting().get('session_timeout'))) + session.modified = True + + @domain_bp.route('/', methods=['GET']) @login_required @can_access_domain diff --git a/powerdnsadmin/routes/user.py b/powerdnsadmin/routes/user.py index 4995aad..b623736 100644 --- a/powerdnsadmin/routes/user.py +++ b/powerdnsadmin/routes/user.py @@ -1,10 +1,12 @@ +import datetime import qrcode as qrc import qrcode.image.svg as qrc_svg from io import BytesIO -from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, session -from flask_login import current_user, login_required +from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, g, session, current_app +from flask_login import current_user, login_required, login_manager -from ..models.user import User +from ..models.user import User, Anonymous +from ..models.setting import Setting user_bp = Blueprint('user', __name__, @@ -12,6 +14,26 @@ user_bp = Blueprint('user', url_prefix='/user') +@user_bp.before_request +def before_request(): + # Check if user is anonymous + g.user = current_user + login_manager.anonymous_user = Anonymous + + # Check site is in maintenance mode + maintenance = Setting().get('maintenance') + if maintenance and current_user.is_authenticated and current_user.role.name not in [ + 'Administrator', 'Operator' + ]: + return render_template('maintenance.html') + + # Manage session timeout + session.permanent = True + current_app.permanent_session_lifetime = datetime.timedelta( + minutes=int(Setting().get('session_timeout'))) + session.modified = True + + @user_bp.route('/profile', methods=['GET', 'POST']) @login_required def profile(): diff --git a/powerdnsadmin/templates/base.html b/powerdnsadmin/templates/base.html index 4542d7a..4d9ad6c 100644 --- a/powerdnsadmin/templates/base.html +++ b/powerdnsadmin/templates/base.html @@ -174,8 +174,18 @@ {% block scripts %} {% assets "js_main" -%} + {% if SETTING.get('warn_session_timeout') %} + {% endif %} {%- endassets %} {% endblock %} {% block extrascripts %} From 33c1f642729871e0f768c2690aafa438afc04c17 Mon Sep 17 00:00:00 2001 From: Khanh Ngo Date: Wed, 18 Dec 2019 23:48:46 +0700 Subject: [PATCH 3/3] Update mock test --- tests/unit/zone/test_admin_apikey.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unit/zone/test_admin_apikey.py b/tests/unit/zone/test_admin_apikey.py index 751b037..6f3a9c3 100644 --- a/tests/unit/zone/test_admin_apikey.py +++ b/tests/unit/zone/test_admin_apikey.py @@ -51,6 +51,9 @@ class TestUnitApiZoneAdminApiKey(object): self.mock_hist_patcher = patch( 'powerdnsadmin.routes.api.History', spec=powerdnsadmin.models.history.History) + self.mock_setting_patcher = patch( + 'powerdnsadmin.routes.api.Setting', + spec=powerdnsadmin.models.setting.Setting) data = admin_apikey_data() api_key = ApiKey(desc=data['description'], @@ -71,6 +74,7 @@ class TestUnitApiZoneAdminApiKey(object): ) self.mock_apikey = self.mock_apikey_patcher.start() self.mock_hist = self.mock_hist_patcher.start() + self.mock_setting = self.mock_setting_patcher.start() self.mock_google_setting.return_value.get.side_effect = load_data self.mock_github_setting.return_value.get.side_effect = load_data