From 52b6966c83a1c0d53557af2362c6d2cd8c1549df Mon Sep 17 00:00:00 2001 From: Khanh Ngo Date: Thu, 12 Apr 2018 11:18:44 +0700 Subject: [PATCH] Check zone serial before allowing user to submit their change. #183 --- app/__init__.py | 4 +++ app/models.py | 45 ++++++++++++++++++++-------------- app/static/custom/js/custom.js | 31 +++++++++++++++++++++++ app/templates/domain.html | 11 ++++++--- app/views.py | 35 ++++++++++++++++++++------ 5 files changed, 97 insertions(+), 29 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 897db56..c309b78 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -7,6 +7,10 @@ app = Flask(__name__) app.config.from_object('config') app.wsgi_app = ProxyFix(app.wsgi_app) +#### CONFIGURE LOGGER #### +from app.lib.log import logger +logging = logger('powerdns-admin', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config() + login_manager = LoginManager() login_manager.init_app(app) db = SQLAlchemy(app) diff --git a/app/models.py b/app/models.py index 0758aa8..0901c5a 100644 --- a/app/models.py +++ b/app/models.py @@ -10,6 +10,7 @@ import pyotp import re import dns.reversename import sys +import logging as logger from datetime import datetime from urllib.parse import urljoin @@ -19,10 +20,8 @@ from flask_login import AnonymousUserMixin from app import app, db from app.lib import utils -from app.lib.log import logger -# LOG CONFIGS -logging = logger('MODEL', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config() +logging = logger.getLogger(__name__) if 'LDAP_TYPE' in app.config.keys(): LDAP_URI = app.config['LDAP_URI'] @@ -510,24 +509,18 @@ class Domain(db.Model): logging.error('Can not create setting {0} for domain {1}. {2}'.format(setting, self.name, e)) return False + def get_domain_info(self, domain_name): + """ + Get all domains which has in PowerDNS + """ + headers = {} + headers['X-API-Key'] = PDNS_API_KEY + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain_name)), headers=headers) + return jdata + def get_domains(self): """ Get all domains which has in PowerDNS - jdata example: - [ - { - "id": "example.org.", - "url": "/servers/localhost/zones/example.org.", - "name": "example.org", - "kind": "Native", - "dnssec": false, - "account": "", - "masters": [], - "serial": 2015101501, - "notified_serial": 0, - "last_check": 0 - } - ] """ headers = {} headers['X-API-Key'] = PDNS_API_KEY @@ -886,6 +879,7 @@ class Domain(db.Model): else: return {'status': 'error', 'msg': 'This domain doesnot exist'} + class DomainUser(db.Model): __tablename__ = 'domain_user' id = db.Column(db.Integer, primary_key = True) @@ -1183,6 +1177,7 @@ class Record(object): return {'status': 'error', 'msg': jdata2['error']} else: self.auto_ptr(domain, new_records, deleted_records) + self.update_db_serial(domain) logging.info('Record was applied successfully.') return {'status': 'ok', 'msg': 'Record was applied successfully'} except Exception as e: @@ -1335,6 +1330,20 @@ class Record(object): logging.error("Cannot add record {0}/{1}/{2} to domain {3}. DETAIL: {4}".format(self.name, self.type, self.data, domain, e)) return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'} + def update_db_serial(self, domain): + headers = {} + headers['X-API-Key'] = PDNS_API_KEY + jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='GET') + serial = jdata['serial'] + + domain = Domain.query.filter(Domain.name==domain).first() + if domain: + domain.serial = serial + db.session.commit() + return {'status': True, 'msg': 'Synced local serial for domain name {0}'.format(domain)} + else: + return {'status': False, 'msg': 'Could not find domain name {0} in local db'.format(domain)} + class Server(object): """ diff --git a/app/static/custom/js/custom.js b/app/static/custom/js/custom.js index 936b8ee..279463d 100644 --- a/app/static/custom/js/custom.js +++ b/app/static/custom/js/custom.js @@ -30,6 +30,37 @@ function applyChanges(data, url, showResult, refreshPage) { }); } +function applyRecordChanges(data, domain) { + var success = false; + $.ajax({ + type : "POST", + url : $SCRIPT_ROOT + '/domain/' + domain + '/apply', + data : JSON.stringify(data),// now data come in this function + contentType : "application/json; charset=utf-8", + crossDomain : true, + dataType : "json", + success : function(data, status, jqXHR) { + // update Apply button value + $.getJSON($SCRIPT_ROOT + '/domain/' + domain + '/info', function(data) { + $(".button_apply_changes").val(data['serial']); + }); + + console.log("Applied changes successfully.") + var modal = $("#modal_success"); + modal.find('.modal-body p').text("Applied changes successfully"); + modal.modal('show'); + }, + + error : function(jqXHR, status) { + console.log(jqXHR); + var modal = $("#modal_error"); + var responseJson = jQuery.parseJSON(jqXHR.responseText); + modal.find('.modal-body p').text(responseJson['msg']); + modal.modal('show'); + } + }); +} + function getTableData(table) { var rData = [] diff --git a/app/templates/domain.html b/app/templates/domain.html index f91d374..203a8f1 100644 --- a/app/templates/domain.html +++ b/app/templates/domain.html @@ -25,7 +25,7 @@ - {% else %} @@ -196,11 +196,14 @@ var modal = $("#modal_apply_changes"); var table = $("#tbl_records").DataTable(); var domain = $(this).prop('id'); + var serial = $(".button_apply_changes").val(); var info = "Are you sure you want to apply your changes?"; modal.find('.modal-body p').text(info); - modal.find('#button_apply_confirm').click(function() { - var data = getTableData(table); - applyChanges(data, $SCRIPT_ROOT + '/domain/' + domain + '/apply', true); + + // following unbind("click") is to avoid multiple times execution + modal.find('#button_apply_confirm').unbind("click").click(function() { + var data = {'serial': serial, 'record': getTableData(table)}; + applyRecordChanges(data, domain); modal.modal('hide'); }) modal.modal('show'); diff --git a/app/views.py b/app/views.py index ffa01aa..70b2f78 100644 --- a/app/views.py +++ b/app/views.py @@ -1,5 +1,6 @@ import base64 import json +import logging as logger import os import traceback import re @@ -19,15 +20,13 @@ from werkzeug.security import gen_salt from .models import User, Domain, Record, Server, History, Anonymous, Setting, DomainSetting, DomainTemplate, DomainTemplateRecord from app import app, login_manager, github, google from app.lib import utils -from app.lib.log import logger from app.decorators import admin_role_required, can_access_domain if app.config['SAML_ENABLED']: from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.utils import OneLogin_Saml2_Utils -# LOG CONFIG -logging = logger('MODEL', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config() +logging = logger.getLogger(__name__) # FILTERS jinja2.filters.FILTERS['display_record_name'] = utils.display_record_name @@ -541,7 +540,6 @@ def dashboard_domains(): @app.route('/domain/', methods=['GET', 'POST']) -@app.route('/domain', methods=['GET', 'POST']) @login_required @can_access_domain def domain(domain_name): @@ -580,7 +578,7 @@ def domain(domain_name): editable_records = app.config['FORWARD_RECORDS_ALLOW_EDIT'] else: editable_records = app.config['REVERSE_RECORDS_ALLOW_EDIT'] - return render_template('domain.html', domain=domain, records=records, editable_records=editable_records,pdns_version=app.config['PDNS_VERSION']) + return render_template('domain.html', domain=domain, records=records, editable_records=editable_records, pdns_version=app.config['PDNS_VERSION']) @app.route('/admin/domain/add', methods=['GET', 'POST']) @@ -593,7 +591,6 @@ def domain_add(): domain_name = request.form.getlist('domain_name')[0] domain_type = request.form.getlist('radio_type')[0] domain_template = request.form.getlist('domain_template')[0] - logging.info("Selected template ==== {0}".format(domain_template)) soa_edit_api = request.form.getlist('radio_type_soa_edit_api')[0] if ' ' in domain_name or not domain_name or not domain_type: @@ -716,11 +713,26 @@ def record_apply(domain_name): example jdata: {u'record_ttl': u'1800', u'record_type': u'CNAME', u'record_name': u'test4', u'record_status': u'Active', u'record_data': u'duykhanh.me'} """ #TODO: filter removed records / name modified records. + try: jdata = request.json + submitted_serial = jdata['serial'] + submitted_record = jdata['record'] + + domain = Domain.query.filter(Domain.name==domain_name).first() + + logging.debug('Your submitted serial: {0}'.format(submitted_serial)) + logging.debug('Current domain serial: {0}'.format(domain.serial)) + + if domain: + if int(submitted_serial) != domain.serial: + return make_response(jsonify( {'status': 'error', 'msg': 'The zone has been changed by another session or user. Please refresh this web page to load updated records.'} ), 500) + else: + return make_response(jsonify( {'status': 'error', 'msg': 'Domain name {0} does not exist'.format(domain_name)} ), 404) + r = Record() - result = r.apply(domain_name, jdata) + result = r.apply(domain_name, submitted_record) if result['status'] == 'ok': history = History(msg='Apply record changes to domain {0}'.format(domain_name), detail=str(jdata), created_by=current_user.username) history.add() @@ -770,6 +782,15 @@ def record_delete(domain_name, record_name, record_type): return redirect(url_for('domain', domain_name=domain_name)) +@app.route('/domain//info', methods=['GET']) +@login_required +@can_access_domain +def domain_info(domain_name): + domain = Domain() + domain_info = domain.get_domain_info(domain_name) + return make_response(jsonify(domain_info), 200) + + @app.route('/domain//dnssec', methods=['GET']) @login_required @can_access_domain