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/lib/utils.py b/app/lib/utils.py index 84c4a75..934ee4d 100644 --- a/app/lib/utils.py +++ b/app/lib/utils.py @@ -130,7 +130,7 @@ def display_record_name(data): if record_name == domain_name: return '@' else: - return record_name.replace('.'+domain_name, '') + return re.sub('\.{}$'.format(domain_name), '', record_name) def display_master_name(data): @@ -196,6 +196,9 @@ def email_to_gravatar_url(email="", size=100): """ AD doesn't necessarily have email """ + if email is None: + email = "" + hash_string = hashlib.md5(email.encode('utf-8')).hexdigest() return "https://s.gravatar.com/avatar/{0}?s={1}".format(hash_string, size) diff --git a/app/models.py b/app/models.py index 25024b7..b0e0fcf 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'] @@ -135,6 +134,9 @@ class User(db.Model): def get_hashed_password(self, plain_text_password=None): # Hash a password for the first time # (Using bcrypt, the salt is saved into the hash itself) + if plain_text_password == None: + return plain_text_password + pw = plain_text_password if plain_text_password else self.plain_text_password return bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) @@ -316,6 +318,9 @@ class User(db.Model): self.password = self.get_hashed_password(self.plain_text_password) + if self.password: + self.password = self.password.decode("utf-8") + db.session.add(self) db.session.commit() return {'status': True, 'msg': 'Created user successfully'} @@ -335,7 +340,9 @@ class User(db.Model): user.password = self.get_hashed_password(self.plain_text_password) if self.plain_text_password else user.password user.avatar = self.avatar if self.avatar else user.avatar - user.otp_secret = "" + if enable_otp is not None: + user.otp_secret = "" + if enable_otp == True: # generate the opt secret key user.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') @@ -502,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 @@ -878,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) @@ -1175,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: @@ -1327,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): """ @@ -1544,7 +1561,7 @@ class DomainTemplateRecord(db.Model): name = db.Column(db.String(255)) type = db.Column(db.String(64)) ttl = db.Column(db.Integer) - data = db.Column(db.String(255)) + data = db.Column(db.Text) status = db.Column(db.Boolean) template_id = db.Column(db.Integer, db.ForeignKey('domain_template.id')) template = db.relationship('DomainTemplate', back_populates='records') 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/admin_history.html b/app/templates/admin_history.html index 59773ac..6ce9469 100644 --- a/app/templates/admin_history.html +++ b/app/templates/admin_history.html @@ -96,7 +96,7 @@ 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/templates/user_profile.html b/app/templates/user_profile.html index a94efe8..dbda925 100644 --- a/app/templates/user_profile.html +++ b/app/templates/user_profile.html @@ -32,7 +32,8 @@ {% if not external_account %}
  • Change Password
  • Authentication -
  • {% endif %}> + + {% endif %}
    diff --git a/app/views.py b/app/views.py index 0cb9bf9..4cfca61 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 @@ -118,7 +117,7 @@ def login_via_authorization_header(request): if auth_header: auth_header = auth_header.replace('Basic ', '', 1) try: - auth_header = base64.b64decode(auth_header) + auth_header = str(base64.b64decode(auth_header), 'utf-8') username,password = auth_header.split(":") except TypeError as e: return None @@ -312,12 +311,13 @@ def login(): return redirect(url_for('index')) if request.method == 'GET': - return render_template('login.html', - github_enabled=GITHUB_ENABLE, - google_enabled=GOOGLE_ENABLE, - saml_enabled=SAML_ENABLED, - ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, - basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED) + return render_template('login.html', github_enabled=GITHUB_ENABLE, + google_enabled=GOOGLE_ENABLE, + saml_enabled=SAML_ENABLED, + ldap_enabled=LDAP_ENABLED, + login_title=LOGIN_TITLE, + basic_enabled=BASIC_ENABLED, + signup_enabled=SIGNUP_ENABLED) # process login username = request.form['username'] @@ -331,6 +331,9 @@ def login(): email = request.form.get('email') rpassword = request.form.get('rpassword') + if auth_method != 'LOCAL': + session['external_auth'] = True + if None in [firstname, lastname, email]: #login case remember_me = False @@ -342,37 +345,46 @@ def login(): try: auth = user.is_validate(method=auth_method) if auth == False: - return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLED, - login_title=LOGIN_TITLE, - basic_enabled=BASIC_ENABLED, - signup_enabled=SIGNUP_ENABLED, - github_enabled=GITHUB_ENABLE, - saml_enabled=SAML_ENABLED) + return render_template('login.html', error='Invalid credentials', + github_enabled=GITHUB_ENABLE, + google_enabled=GOOGLE_ENABLE, + saml_enabled=SAML_ENABLED, + ldap_enabled=LDAP_ENABLED, + login_title=LOGIN_TITLE, + basic_enabled=BASIC_ENABLED, + signup_enabled=SIGNUP_ENABLED) except Exception as e: - return render_template('login.html', error=e, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, - basic_enabled=BASIC_ENABLED, - signup_enabled=SIGNUP_ENABLED, - github_enabled=GITHUB_ENABLE, - saml_enabled=SAML_ENABLED) + return render_template('login.html', error=e, + github_enabled=GITHUB_ENABLE, + google_enabled=GOOGLE_ENABLE, + saml_enabled=SAML_ENABLED, + ldap_enabled=LDAP_ENABLED, + login_title=LOGIN_TITLE, + basic_enabled=BASIC_ENABLED, + signup_enabled=SIGNUP_ENABLED) # check if user enabled OPT authentication if user.otp_secret: if otp_token: good_token = user.verify_totp(otp_token) if not good_token: - return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLED, - login_title=LOGIN_TITLE, - basic_enabled=BASIC_ENABLED, - signup_enabled=SIGNUP_ENABLED, - github_enabled=GITHUB_ENABLE, - saml_enabled=SAML_ENABLED) + return render_template('login.html', error='Invalid credentials', + github_enabled=GITHUB_ENABLE, + google_enabled=GOOGLE_ENABLE, + saml_enabled=SAML_ENABLED, + ldap_enabled=LDAP_ENABLED, + login_title=LOGIN_TITLE, + basic_enabled=BASIC_ENABLED, + signup_enabled=SIGNUP_ENABLED) else: - return render_template('login.html', error='Token required', ldap_enabled=LDAP_ENABLED, - login_title=LOGIN_TITLE, - basic_enabled=BASIC_ENABLED, - signup_enabled=SIGNUP_ENABLED, - github_enabled = GITHUB_ENABLE, - saml_enabled = SAML_ENABLED) + return render_template('login.html', error='Token required', + github_enabled=GITHUB_ENABLE, + google_enabled=GOOGLE_ENABLE, + saml_enabled=SAML_ENABLED, + ldap_enabled=LDAP_ENABLED, + login_title=LOGIN_TITLE, + basic_enabled=BASIC_ENABLED, + signup_enabled=SIGNUP_ENABLED) login_user(user, remember = remember_me) return redirect(request.args.get('next') or url_for('index')) @@ -389,9 +401,14 @@ def login(): try: result = user.create_local_user() if result == True: - return render_template('login.html', username=username, password=password, ldap_enabled=LDAP_ENABLED, - login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED, - github_enabled=GITHUB_ENABLE,saml_enabled=SAML_ENABLED) + return render_template('login.html', username=username, password=password, + github_enabled=GITHUB_ENABLE, + google_enabled=GOOGLE_ENABLE, + saml_enabled=SAML_ENABLED, + ldap_enabled=LDAP_ENABLED, + login_title=LOGIN_TITLE, + basic_enabled=BASIC_ENABLED, + signup_enabled=SIGNUP_ENABLED) else: return render_template('register.html', error=result['msg']) except Exception as e: @@ -523,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): @@ -551,7 +567,7 @@ def domain(domain_name): if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name): editable_records = app.config['RECORDS_ALLOW_EDIT'] else: - editable_records = app.config['REVERSE_ALLOW_EDIT'] + editable_records = app.config['REVERSE_RECORDS_ALLOW_EDIT'] return render_template('domain.html', domain=domain, records=records, editable_records=editable_records) else: for jr in jrecords: @@ -562,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']) @@ -575,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: @@ -698,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() @@ -752,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 @@ -898,17 +937,18 @@ def create_template_from_zone(): if NEW_SCHEMA: for jr in jrecords: - name = '@' if jr['name'] == domain_name else jr['name'] if jr['type'] in app.config['RECORDS_ALLOW_EDIT']: + name = '@' if jr['name'] == domain_name else re.sub('\.{}$'.format(domain_name), '', jr['name']) for subrecord in jr['records']: - record = DomainTemplateRecord(name=name, type=jr['type'], status=True if subrecord['disabled'] else False, ttl=jr['ttl'], data=subrecord['content']) records.append(record) else: for jr in jrecords: if jr['type'] in app.config['RECORDS_ALLOW_EDIT']: + name = '@' if jr['name'] == domain_name else re.sub('\.{}$'.format(domain_name), '', jr['name']) record = DomainTemplateRecord(name=name, type=jr['type'], status=True if jr['disabled'] else False, ttl=jr['ttl'], data=jr['content']) records.append(record) + result_records = t.replace_records(records) if result_records['status'] == 'ok': @@ -1150,7 +1190,7 @@ def admin_settings_edit(setting): @login_required def user_profile(): external_account = False - if session.has_key('external_auth'): + if 'external_auth' in session: external_account = session['external_auth'] if request.method == 'GET' or external_account: return render_template('user_profile.html', external_account=external_account) diff --git a/config_template.py b/config_template.py index 2a0766a..cbc4d2a 100644 --- a/config_template.py +++ b/config_template.py @@ -123,7 +123,7 @@ PDNS_VERSION = '4.1.1' # RECORDS ALLOWED TO EDIT RECORDS_ALLOW_EDIT = ['SOA', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'PTR', 'SPF', 'SRV', 'TXT', 'LOC', 'NS', 'PTR'] FORWARD_RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'PTR', 'SPF', 'SRV', 'TXT', 'LOC' 'NS'] -REVERSE_RECORDS_ALLOW_EDIT = ['TXT', 'LOC', 'NS', 'PTR'] +REVERSE_RECORDS_ALLOW_EDIT = ['SOA', 'TXT', 'LOC', 'NS', 'PTR'] # EXPERIMENTAL FEATURES PRETTY_IPV6_PTR = False diff --git a/configs/development.py b/configs/development.py index ea78d9f..c797e86 100644 --- a/configs/development.py +++ b/configs/development.py @@ -111,10 +111,9 @@ PDNS_API_KEY = os.environ.get('PDNS_API_KEY') PDNS_VERSION = '4.1.1' # RECORDS ALLOWED TO EDIT -RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'PTR', 'SPF', 'SRV', 'TXT', 'NS'] - -# RECORDS ALLOWED TO EDIT FOR REVERSE DOMAINS -REVERSE_ALLOW_EDIT = ['PTR', 'NS'] +RECORDS_ALLOW_EDIT = ['SOA', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'PTR', 'SPF', 'SRV', 'TXT', 'LOC', 'NS', 'PTR'] +FORWARD_RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'PTR', 'SPF', 'SRV', 'TXT', 'LOC' 'NS'] +REVERSE_RECORDS_ALLOW_EDIT = ['SOA', 'TXT', 'LOC', 'NS', 'PTR'] # EXPERIMENTAL FEATURES PRETTY_IPV6_PTR = False