Check zone serial before allowing user to submit their change. #183

This commit is contained in:
Khanh Ngo 2018-04-12 11:18:44 +07:00
parent 84d4bfaed0
commit 52b6966c83
5 changed files with 97 additions and 29 deletions

View File

@ -7,6 +7,10 @@ app = Flask(__name__)
app.config.from_object('config') app.config.from_object('config')
app.wsgi_app = ProxyFix(app.wsgi_app) 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 = LoginManager()
login_manager.init_app(app) login_manager.init_app(app)
db = SQLAlchemy(app) db = SQLAlchemy(app)

View File

@ -10,6 +10,7 @@ import pyotp
import re import re
import dns.reversename import dns.reversename
import sys import sys
import logging as logger
from datetime import datetime from datetime import datetime
from urllib.parse import urljoin from urllib.parse import urljoin
@ -19,10 +20,8 @@ from flask_login import AnonymousUserMixin
from app import app, db from app import app, db
from app.lib import utils from app.lib import utils
from app.lib.log import logger
# LOG CONFIGS logging = logger.getLogger(__name__)
logging = logger('MODEL', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config()
if 'LDAP_TYPE' in app.config.keys(): if 'LDAP_TYPE' in app.config.keys():
LDAP_URI = app.config['LDAP_URI'] 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)) logging.error('Can not create setting {0} for domain {1}. {2}'.format(setting, self.name, e))
return False 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): def get_domains(self):
""" """
Get all domains which has in PowerDNS 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 = {}
headers['X-API-Key'] = PDNS_API_KEY headers['X-API-Key'] = PDNS_API_KEY
@ -886,6 +879,7 @@ class Domain(db.Model):
else: else:
return {'status': 'error', 'msg': 'This domain doesnot exist'} return {'status': 'error', 'msg': 'This domain doesnot exist'}
class DomainUser(db.Model): class DomainUser(db.Model):
__tablename__ = 'domain_user' __tablename__ = 'domain_user'
id = db.Column(db.Integer, primary_key = True) id = db.Column(db.Integer, primary_key = True)
@ -1183,6 +1177,7 @@ class Record(object):
return {'status': 'error', 'msg': jdata2['error']} return {'status': 'error', 'msg': jdata2['error']}
else: else:
self.auto_ptr(domain, new_records, deleted_records) self.auto_ptr(domain, new_records, deleted_records)
self.update_db_serial(domain)
logging.info('Record was applied successfully.') logging.info('Record was applied successfully.')
return {'status': 'ok', 'msg': 'Record was applied successfully'} return {'status': 'ok', 'msg': 'Record was applied successfully'}
except Exception as e: 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)) 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'} 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): class Server(object):
""" """

View File

@ -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) { function getTableData(table) {
var rData = [] var rData = []

View File

@ -25,7 +25,7 @@
<button type="button" class="btn btn-flat btn-primary pull-left button_add_record" id="{{ domain.name }}"> <button type="button" class="btn btn-flat btn-primary pull-left button_add_record" id="{{ domain.name }}">
Add Record&nbsp;<i class="fa fa-plus"></i> Add Record&nbsp;<i class="fa fa-plus"></i>
</button> </button>
<button type="button" class="btn btn-flat btn-primary pull-right button_apply_changes" id="{{ domain.name }}"> <button type="button" class="btn btn-flat btn-primary pull-right button_apply_changes" id="{{ domain.name }}" value="{{ domain.serial }}">
Apply Changes&nbsp;<i class="fa fa-floppy-o"></i> Apply Changes&nbsp;<i class="fa fa-floppy-o"></i>
</button> </button>
{% else %} {% else %}
@ -196,11 +196,14 @@
var modal = $("#modal_apply_changes"); var modal = $("#modal_apply_changes");
var table = $("#tbl_records").DataTable(); var table = $("#tbl_records").DataTable();
var domain = $(this).prop('id'); var domain = $(this).prop('id');
var serial = $(".button_apply_changes").val();
var info = "Are you sure you want to apply your changes?"; var info = "Are you sure you want to apply your changes?";
modal.find('.modal-body p').text(info); modal.find('.modal-body p').text(info);
modal.find('#button_apply_confirm').click(function() {
var data = getTableData(table); // following unbind("click") is to avoid multiple times execution
applyChanges(data, $SCRIPT_ROOT + '/domain/' + domain + '/apply', true); modal.find('#button_apply_confirm').unbind("click").click(function() {
var data = {'serial': serial, 'record': getTableData(table)};
applyRecordChanges(data, domain);
modal.modal('hide'); modal.modal('hide');
}) })
modal.modal('show'); modal.modal('show');

View File

@ -1,5 +1,6 @@
import base64 import base64
import json import json
import logging as logger
import os import os
import traceback import traceback
import re 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 .models import User, Domain, Record, Server, History, Anonymous, Setting, DomainSetting, DomainTemplate, DomainTemplateRecord
from app import app, login_manager, github, google from app import app, login_manager, github, google
from app.lib import utils from app.lib import utils
from app.lib.log import logger
from app.decorators import admin_role_required, can_access_domain from app.decorators import admin_role_required, can_access_domain
if app.config['SAML_ENABLED']: if app.config['SAML_ENABLED']:
from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.utils import OneLogin_Saml2_Utils from onelogin.saml2.utils import OneLogin_Saml2_Utils
# LOG CONFIG logging = logger.getLogger(__name__)
logging = logger('MODEL', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config()
# FILTERS # FILTERS
jinja2.filters.FILTERS['display_record_name'] = utils.display_record_name jinja2.filters.FILTERS['display_record_name'] = utils.display_record_name
@ -541,7 +540,6 @@ def dashboard_domains():
@app.route('/domain/<path:domain_name>', methods=['GET', 'POST']) @app.route('/domain/<path:domain_name>', methods=['GET', 'POST'])
@app.route('/domain', methods=['GET', 'POST'])
@login_required @login_required
@can_access_domain @can_access_domain
def domain(domain_name): def domain(domain_name):
@ -580,7 +578,7 @@ def domain(domain_name):
editable_records = app.config['FORWARD_RECORDS_ALLOW_EDIT'] editable_records = app.config['FORWARD_RECORDS_ALLOW_EDIT']
else: else:
editable_records = app.config['REVERSE_RECORDS_ALLOW_EDIT'] 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']) @app.route('/admin/domain/add', methods=['GET', 'POST'])
@ -593,7 +591,6 @@ def domain_add():
domain_name = request.form.getlist('domain_name')[0] domain_name = request.form.getlist('domain_name')[0]
domain_type = request.form.getlist('radio_type')[0] domain_type = request.form.getlist('radio_type')[0]
domain_template = request.form.getlist('domain_template')[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] soa_edit_api = request.form.getlist('radio_type_soa_edit_api')[0]
if ' ' in domain_name or not domain_name or not domain_type: 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'} 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. #TODO: filter removed records / name modified records.
try: try:
jdata = request.json 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() r = Record()
result = r.apply(domain_name, jdata) result = r.apply(domain_name, submitted_record)
if result['status'] == 'ok': 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 = History(msg='Apply record changes to domain {0}'.format(domain_name), detail=str(jdata), created_by=current_user.username)
history.add() history.add()
@ -770,6 +782,15 @@ def record_delete(domain_name, record_name, record_type):
return redirect(url_for('domain', domain_name=domain_name)) return redirect(url_for('domain', domain_name=domain_name))
@app.route('/domain/<path:domain_name>/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/<path:domain_name>/dnssec', methods=['GET']) @app.route('/domain/<path:domain_name>/dnssec', methods=['GET'])
@login_required @login_required
@can_access_domain @can_access_domain