diff --git a/.travis.yml b/.travis.yml index 1171f8f..422faa2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,23 @@ language: python python: - "3.5.2" before_install: - - 'travis_retry sudo apt-get update' - - 'travis_retry sudo apt-get install python3-dev libxml2-dev libxmlsec1-dev' + - sudo apt-key adv --fetch-keys http://dl.yarnpkg.com/debian/pubkey.gpg + - echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list + - travis_retry sudo apt-get update + - travis_retry sudo apt-get install python3-dev libxml2-dev libxmlsec1-dev yarn + - mysql -e 'CREATE DATABASE pda'; + - mysql -e "GRANT ALL PRIVILEGES ON pda.* to pda@'%' IDENTIFIED BY 'changeme'"; install: - pip install -r requirements.txt before_script: - - mv config_template.py config.py + - mv config_template.py config.py + - export FLASK_APP=app/__init__.py + - flask db upgrade + - yarn install --pure-lockfile + - flask assets build script: - - sh run_travis.sh \ No newline at end of file + - sh run_travis.sh +cache: + yarn: true +services: + - mysql \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 1f5687d..b2425ce 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,7 +3,8 @@ from flask import Flask, request, session, redirect, url_for from flask_login import LoginManager from flask_sqlalchemy import SQLAlchemy as SA from flask_migrate import Migrate - +from flask_oauthlib.client import OAuth +from sqlalchemy.exc import OperationalError # subclass SQLAlchemy to enable pool_pre_ping class SQLAlchemy(SA): @@ -26,89 +27,14 @@ logging = logger('powerdns-admin', app.config['LOG_LEVEL'], app.config['LOG_FILE login_manager = LoginManager() login_manager.init_app(app) -db = SQLAlchemy(app) -migrate = Migrate(app, db) # used for flask-migrate - -def enable_github_oauth(GITHUB_ENABLE): - if not GITHUB_ENABLE: - return None, None - from flask_oauthlib.client import OAuth - oauth = OAuth(app) - github = oauth.remote_app( - 'github', - consumer_key=app.config['GITHUB_OAUTH_KEY'], - consumer_secret=app.config['GITHUB_OAUTH_SECRET'], - request_token_params={'scope': app.config['GITHUB_OAUTH_SCOPE']}, - base_url=app.config['GITHUB_OAUTH_URL'], - request_token_url=None, - access_token_method='POST', - access_token_url=app.config['GITHUB_OAUTH_TOKEN'], - authorize_url=app.config['GITHUB_OAUTH_AUTHORIZE'] - ) - - @app.route('/user/authorized') - def authorized(): - session['github_oauthredir'] = url_for('.authorized', _external=True) - resp = github.authorized_response() - if resp is None: - return 'Access denied: reason=%s error=%s' % ( - request.args['error'], - request.args['error_description'] - ) - session['github_token'] = (resp['access_token'], '') - return redirect(url_for('.login')) - - @github.tokengetter - def get_github_oauth_token(): - return session.get('github_token') - - return oauth, github - - -oauth, github = enable_github_oauth(app.config.get('GITHUB_OAUTH_ENABLE')) - - -def enable_google_oauth(GOOGLE_ENABLE): - if not GOOGLE_ENABLE: - return None - from flask_oauthlib.client import OAuth - oauth = OAuth(app) - - google = oauth.remote_app( - 'google', - consumer_key=app.config['GOOGLE_OAUTH_CLIENT_ID'], - consumer_secret=app.config['GOOGLE_OAUTH_CLIENT_SECRET'], - request_token_params=app.config['GOOGLE_TOKEN_PARAMS'], - base_url=app.config['GOOGLE_BASE_URL'], - request_token_url=None, - access_token_method='POST', - access_token_url=app.config['GOOGLE_TOKEN_URL'], - authorize_url=app.config['GOOGLE_AUTHORIZE_URL'], - ) - - @app.route('/user/authorized') - def authorized(): - resp = google.authorized_response() - if resp is None: - return 'Access denied: reason=%s error=%s' % ( - request.args['error_reason'], - request.args['error_description'] - ) - session['google_token'] = (resp['access_token'], '') - return redirect(url_for('.login')) - - @google.tokengetter - def get_google_oauth_token(): - return session.get('google_token') - - return google - - -google = enable_google_oauth(app.config.get('GOOGLE_OAUTH_ENABLE')) - -from app import views, models +db = SQLAlchemy(app) # database +migrate = Migrate(app, db) # flask-migrate +oauth = OAuth(app) # oauth if app.config.get('SAML_ENABLED') and app.config.get('SAML_ENCRYPT'): from app.lib import certutil if not certutil.check_certificate(): certutil.create_self_signed_cert() + +from app import models +from app import views diff --git a/app/assets.py b/app/assets.py index e6f93b4..06cfa37 100644 --- a/app/assets.py +++ b/app/assets.py @@ -28,6 +28,11 @@ js_login = Bundle( 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', 'node_modules/font-awesome/css/font-awesome.css', @@ -62,6 +67,7 @@ js_main = Bundle( assets = Environment() assets.register('js_login', js_login) +assets.register('js_validation', js_validation) assets.register('css_login', css_login) assets.register('js_main', js_main) assets.register('css_main', css_main) diff --git a/app/decorators.py b/app/decorators.py index 2ba7ce4..673eea6 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -2,7 +2,7 @@ from functools import wraps from flask import g, request, redirect, url_for from app import app -from app.models import Role +from app.models import Role, Setting def admin_role_required(f): @@ -31,7 +31,7 @@ def can_access_domain(f): def can_configure_dnssec(f): @wraps(f) def decorated_function(*args, **kwargs): - if g.user.role.name != 'Administrator' and app.config['DNSSEC_ADMINS_ONLY']: + if g.user.role.name != 'Administrator' and Setting().get('dnssec_admins_only'): return redirect(url_for('error', code=401)) return f(*args, **kwargs) diff --git a/app/models.py b/app/models.py index 974518e..5eec5ab 100644 --- a/app/models.py +++ b/app/models.py @@ -9,9 +9,12 @@ import traceback import pyotp import re import dns.reversename +import dns.inet +import dns.name import sys import logging as logger +from ast import literal_eval from datetime import datetime from urllib.parse import urljoin from distutils.util import strtobool @@ -23,38 +26,6 @@ from app.lib import utils logging = logger.getLogger(__name__) -if 'LDAP_TYPE' in app.config.keys(): - LDAP_URI = app.config['LDAP_URI'] - LDAP_SEARCH_BASE = app.config['LDAP_SEARCH_BASE'] - LDAP_TYPE = app.config['LDAP_TYPE'] - LDAP_FILTER = app.config['LDAP_FILTER'] - LDAP_USERNAMEFIELD = app.config['LDAP_USERNAMEFIELD'] - - LDAP_GROUP_SECURITY = app.config.get('LDAP_GROUP_SECURITY') - if LDAP_GROUP_SECURITY == True: - LDAP_ADMIN_GROUP = app.config['LDAP_ADMIN_GROUP'] - LDAP_USER_GROUP = app.config['LDAP_USER_GROUP'] -else: - LDAP_TYPE = False - -if 'PRETTY_IPV6_PTR' in app.config.keys(): - import dns.inet - import dns.name - PRETTY_IPV6_PTR = app.config['PRETTY_IPV6_PTR'] -else: - PRETTY_IPV6_PTR = False - -PDNS_STATS_URL = app.config['PDNS_STATS_URL'] -PDNS_API_KEY = app.config['PDNS_API_KEY'] -PDNS_VERSION = app.config['PDNS_VERSION'] -API_EXTENDED_URL = utils.pdns_api_extended_uri(PDNS_VERSION) - -# Flag for pdns v4.x.x -# TODO: Find another way to do this -if StrictVersion(PDNS_VERSION) >= StrictVersion('4.0.0'): - NEW_SCHEMA = True -else: - NEW_SCHEMA = False class Anonymous(AnonymousUserMixin): def __init__(self): @@ -147,7 +118,7 @@ class User(db.Model): def ldap_init_conn(self): ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) - conn = ldap.initialize(LDAP_URI) + conn = ldap.initialize(Setting().get('ldap_uri')) conn.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF) conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3) conn.set_option(ldap.OPT_X_TLS,ldap.OPT_X_TLS_DEMAND) @@ -162,7 +133,7 @@ class User(db.Model): try: conn = self.ldap_init_conn() - conn.simple_bind_s(app.config['LDAP_ADMIN_USERNAME'], app.config['LDAP_ADMIN_PASSWORD']) + conn.simple_bind_s(Setting().get('ldap_admin_username'), Setting().get('ldap_admin_password')) ldap_result_id = conn.search(baseDN, searchScope, searchFilter, retrieveAttributes) result_set = [] @@ -177,6 +148,8 @@ class User(db.Model): except ldap.LDAPError as e: logging.error(e) + logging.debug('baseDN: {0}'.format(baseDN)) + logging.debug(traceback.format_exc()) raise def ldap_auth(self, ldap_username, password): @@ -207,34 +180,53 @@ class User(db.Model): if method == 'LDAP': isadmin = False - if not LDAP_TYPE: - logging.error('LDAP authentication is disabled') - return False + LDAP_TYPE = Setting().get('ldap_type') + LDAP_BASE_DN = Setting().get('ldap_base_dn') + LDAP_FILTER_BASIC = Setting().get('ldap_filter_basic') + LDAP_FILTER_USERNAME = Setting().get('ldap_filter_username') + LDAP_ADMIN_GROUP = Setting().get('ldap_admin_group') + LDAP_USER_GROUP = Setting().get('ldap_user_group') + LDAP_GROUP_SECURITY_ENABLED = Setting().get('ldap_sg_enabled') - if LDAP_TYPE == 'ldap': - searchFilter = "(&({0}={1}){2})".format(LDAP_USERNAMEFIELD, self.username, LDAP_FILTER) - logging.debug('Ldap searchFilter "{0}"'.format(searchFilter)) - elif LDAP_TYPE == 'ad': - searchFilter = "(&(objectcategory=person)({0}={1}){2})".format(LDAP_USERNAMEFIELD, self.username, LDAP_FILTER) + searchFilter = "(&({0}={1}){2})".format(LDAP_FILTER_USERNAME, self.username, LDAP_FILTER_BASIC) + logging.debug('Ldap searchFilter {0}'.format(searchFilter)) + + ldap_result = self.ldap_search(searchFilter, LDAP_BASE_DN) + logging.debug('Ldap search result: {0}'.format(ldap_result)) - ldap_result = self.ldap_search(searchFilter, LDAP_SEARCH_BASE) if not ldap_result: logging.warning('LDAP User "{0}" does not exist. Authentication request from {1}'.format(self.username, src_ip)) return False else: try: ldap_username = ldap.filter.escape_filter_chars(ldap_result[0][0][0]) - # check if LDAP_SECURITY_GROUP is enabled + # check if LDAP_GROUP_SECURITY_ENABLED is True # user can be assigned to ADMIN or USER role. - if LDAP_GROUP_SECURITY: + if LDAP_GROUP_SECURITY_ENABLED: try: - if (self.ldap_search(searchFilter, LDAP_ADMIN_GROUP)): - isadmin = True - logging.info('User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'.format(self.username,LDAP_ADMIN_GROUP)) - elif (self.ldap_search(searchFilter, LDAP_USER_GROUP)): - logging.info('User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'.format(self.username,LDAP_USER_GROUP)) + if LDAP_TYPE == 'ldap': + if (self.ldap_search(searchFilter, LDAP_ADMIN_GROUP)): + isadmin = True + logging.info('User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP)) + elif (self.ldap_search(searchFilter, LDAP_USER_GROUP)): + logging.info('User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'.format(self.username, LDAP_USER_GROUP)) + else: + logging.error('User {0} is not part of the "{1}" or "{2}" groups that allow access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP, LDAP_USER_GROUP)) + return False + elif LDAP_TYPE == 'ad': + user_ldap_groups = [g.decode("utf-8") for g in ldap_result[0][0][1]['memberOf']] + logging.debug('user_ldap_groups: {0}'.format(user_ldap_groups)) + + if (LDAP_ADMIN_GROUP in user_ldap_groups): + isadmin = True + logging.info('User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP)) + elif (LDAP_USER_GROUP in user_ldap_groups): + logging.info('User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'.format(self.username, LDAP_USER_GROUP)) + else: + logging.error('User {0} is not part of the "{1}" or "{2}" groups that allow access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP, LDAP_USER_GROUP)) + return False else: - logging.error('User {0} is not part of the "{1}" or "{2}" groups that allow access to PowerDNS-Admin'.format(self.username,LDAP_ADMIN_GROUP,LDAP_USER_GROUP)) + logging.error('Invalid LDAP type') return False except Exception as e: logging.error('LDAP group lookup for user "{0}" has failed. Authentication request from {1}'.format(self.username, src_ip)) @@ -256,13 +248,16 @@ class User(db.Model): self.firstname = self.username self.lastname = '' try: - # try to get user's firstname & lastname from LDAP - # this might be changed in the future - self.firstname = ldap_result[0][0][1]['givenName'][0].decode("utf-8") - self.lastname = ldap_result[0][0][1]['sn'][0].decode("utf-8") - self.email = ldap_result[0][0][1]['mail'][0].decode("utf-8") + # try to get user's firstname, lastname and email address from LDAP attributes + if LDAP_TYPE == 'ldap': + self.firstname = ldap_result[0][0][1]['givenName'][0].decode("utf-8") + self.lastname = ldap_result[0][0][1]['sn'][0].decode("utf-8") + self.email = ldap_result[0][0][1]['mail'][0].decode("utf-8") + elif LDAP_TYPE == 'ad': + self.firstname = ldap_result[0][0][1]['name'][0].decode("utf-8") + self.email = ldap_result[0][0][1]['userPrincipalName'][0].decode("utf-8") except Exception as e: - logging.info("Reading ldap data threw an exception {0}".format(e)) + logging.warning("Reading ldap data threw an exception {0}".format(e)) logging.debug(traceback.format_exc()) # first register user will be in Administrator role @@ -271,7 +266,7 @@ class User(db.Model): self.role_id = Role.query.filter_by(name='Administrator').first().id # user will be in Administrator role if part of LDAP Admin group - if LDAP_GROUP_SECURITY: + if LDAP_GROUP_SECURITY_ENABLED: if isadmin == True: self.role_id = Role.query.filter_by(name='Administrator').first().id @@ -279,9 +274,9 @@ class User(db.Model): logging.info('Created user "{0}" in the DB'.format(self.username)) # user already exists in database, set their admin status based on group membership (if enabled) - if LDAP_GROUP_SECURITY: + if LDAP_GROUP_SECURITY_ENABLED: self.set_admin(isadmin) - self.update_profile() + return True else: logging.error('Unsupported authentication method') @@ -319,9 +314,9 @@ class User(db.Model): if User.query.count() == 0: self.role_id = Role.query.filter_by(name='Administrator').first().id - self.password = self.get_hashed_password(self.plain_text_password) + self.password = self.get_hashed_password(self.plain_text_password) if self.plain_text_password else '*' - if self.password: + if self.password and self.password != '*': self.password = self.password.decode("utf-8") db.session.add(self) @@ -741,6 +736,16 @@ class Domain(db.Model): self.last_check = last_check self.dnssec = dnssec self.account_id = account_id + # PDNS configs + self.PDNS_STATS_URL = Setting().get('pdns_api_url') + self.PDNS_API_KEY = Setting().get('pdns_api_key') + self.PDNS_VERSION = Setting().get('pdns_version') + self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION) + + if StrictVersion(self.PDNS_VERSION) >= StrictVersion('4.0.0'): + self.NEW_SCHEMA = True + else: + self.NEW_SCHEMA = False def __repr__(self): return ''.format(self.name) @@ -759,8 +764,8 @@ class Domain(db.Model): 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) + headers['X-API-Key'] = self.PDNS_API_KEY + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain_name)), headers=headers) return jdata def get_domains(self): @@ -768,8 +773,8 @@ class Domain(db.Model): 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'), headers=headers) + headers['X-API-Key'] = self.PDNS_API_KEY + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers) return jdata def get_id_by_name(self, name): @@ -791,9 +796,9 @@ class Domain(db.Model): dict_db_domain = dict((x.name,x) for x in db_domain) headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY try: - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers) + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers) list_jdomain = [d['name'].rstrip('.') for d in jdata] try: # domains should remove from db since it doesn't exist in powerdns anymore @@ -874,9 +879,9 @@ class Domain(db.Model): Add a domain to power dns """ headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY - if NEW_SCHEMA: + if self.NEW_SCHEMA: domain_name = domain_name + '.' domain_ns = [ns + '.' for ns in domain_ns] @@ -896,7 +901,7 @@ class Domain(db.Model): } try: - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers, method='POST', data=post_data) + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers, method='POST', data=post_data) if 'error' in jdata.keys(): logging.error(jdata['error']) return {'status': 'error', 'msg': jdata['error']} @@ -914,7 +919,7 @@ class Domain(db.Model): if not domain: return {'status': 'error', 'msg': 'Domain doesnt exist.'} headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]: soa_edit_api = 'DEFAULT' @@ -929,7 +934,7 @@ class Domain(db.Model): try: jdata = utils.fetch_json( - urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain.name)), headers=headers, + urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain.name)), headers=headers, method='PUT', data=post_data) if 'error' in jdata.keys(): logging.error(jdata['error']) @@ -951,7 +956,7 @@ class Domain(db.Model): domain_obj = Domain.query.filter(Domain.name == domain_name).first() domain_auto_ptr = DomainSetting.query.filter(DomainSetting.domain == domain_obj).filter(DomainSetting.setting == 'auto_ptr').first() domain_auto_ptr = strtobool(domain_auto_ptr.value) if domain_auto_ptr else False - system_auto_ptr = strtobool(Setting().get('auto_ptr')) + system_auto_ptr = Setting().get('auto_ptr') self.name = domain_name domain_id = self.get_id_by_name(domain_reverse_name) if None == domain_id and \ @@ -1002,9 +1007,9 @@ class Domain(db.Model): Delete a single domain name from powerdns """ headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY try: - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain_name)), headers=headers, method='DELETE') + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain_name)), headers=headers, method='DELETE') logging.info('Delete domain {0} successfully'.format(domain_name)) return {'status': 'ok', 'msg': 'Delete domain successfully'} except Exception as e: @@ -1059,9 +1064,9 @@ class Domain(db.Model): domain = Domain.query.filter(Domain.name == domain_name).first() if domain: headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY try: - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}/axfr-retrieve'.format(domain.name)), headers=headers, method='PUT') + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}/axfr-retrieve'.format(domain.name)), headers=headers, method='PUT') return {'status': 'ok', 'msg': 'Update from Master successfully'} except: return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'} @@ -1075,9 +1080,9 @@ class Domain(db.Model): domain = Domain.query.filter(Domain.name == domain_name).first() if domain: headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY try: - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}/cryptokeys'.format(domain.name)), headers=headers, method='GET') + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}/cryptokeys'.format(domain.name)), headers=headers, method='GET') if 'error' in jdata: return {'status': 'error', 'msg': 'DNSSEC is not enabled for this domain'} else: @@ -1094,13 +1099,13 @@ class Domain(db.Model): domain = Domain.query.filter(Domain.name == domain_name).first() if domain: headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY try: # Enable API-RECTIFY for domain, BEFORE activating DNSSEC post_data = { "api_rectify": True } - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain.name)), headers=headers, method='PUT', data=post_data) + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain.name)), headers=headers, method='PUT', data=post_data) if 'error' in jdata: return {'status': 'error', 'msg': 'API-RECTIFY could not be enabled for this domain', 'jdata' : jdata} @@ -1109,7 +1114,7 @@ class Domain(db.Model): "keytype": "ksk", "active": True } - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}/cryptokeys'.format(domain.name)), headers=headers, method='POST',data=post_data) + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}/cryptokeys'.format(domain.name)), headers=headers, method='POST',data=post_data) if 'error' in jdata: return {'status': 'error', 'msg': 'Cannot enable DNSSEC for this domain. Error: {0}'.format(jdata['error']), 'jdata' : jdata} @@ -1129,10 +1134,10 @@ class Domain(db.Model): domain = Domain.query.filter(Domain.name == domain_name).first() if domain: headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY try: # Deactivate DNSSEC - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}/cryptokeys/{1}'.format(domain.name, key_id)), headers=headers, method='DELETE') + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}/cryptokeys/{1}'.format(domain.name, key_id)), headers=headers, method='DELETE') if jdata != True: return {'status': 'error', 'msg': 'Cannot disable DNSSEC for this domain. Error: {0}'.format(jdata['error']), 'jdata' : jdata} @@ -1140,7 +1145,7 @@ class Domain(db.Model): post_data = { "api_rectify": False } - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain.name)), headers=headers, method='PUT', data=post_data) + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain.name)), headers=headers, method='PUT', data=post_data) if 'error' in jdata: return {'status': 'error', 'msg': 'API-RECTIFY could not be disabled for this domain', 'jdata' : jdata} @@ -1169,7 +1174,7 @@ class Domain(db.Model): return {'status': False, 'msg': 'Domain does not exist'} headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY account_name = Account().get_name_by_id(account_id) @@ -1179,7 +1184,7 @@ class Domain(db.Model): try: jdata = utils.fetch_json( - urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain_name)), headers=headers, + urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain_name)), headers=headers, method='PUT', data=post_data) if 'error' in jdata.keys(): @@ -1248,24 +1253,35 @@ class Record(object): self.status = status self.ttl = ttl self.data = data + # PDNS configs + self.PDNS_STATS_URL = Setting().get('pdns_api_url') + self.PDNS_API_KEY = Setting().get('pdns_api_key') + self.PDNS_VERSION = Setting().get('pdns_version') + self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION) + self.PRETTY_IPV6_PTR = Setting().get('pretty_ipv6_ptr') + + if StrictVersion(self.PDNS_VERSION) >= StrictVersion('4.0.0'): + self.NEW_SCHEMA = True + else: + self.NEW_SCHEMA = False def get_record_data(self, domain): """ Query domain's DNS records via API """ headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY try: - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers) + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers) except: logging.error("Cannot fetch domain's record data from remote powerdns api") return False - if NEW_SCHEMA: + if self.NEW_SCHEMA: rrsets = jdata['rrsets'] for rrset in rrsets: r_name = rrset['name'].rstrip('.') - if PRETTY_IPV6_PTR: # only if activated + if self.PRETTY_IPV6_PTR: # only if activated if rrset['type'] == 'PTR': # only ptr if 'ip6.arpa' in r_name: # only if v6-ptr r_name = dns.reversename.to_address(dns.name.from_text(r_name)) @@ -1292,9 +1308,9 @@ class Record(object): # continue if the record is ready to be added headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY - if NEW_SCHEMA: + if self.NEW_SCHEMA: data = {"rrsets": [ { "name": self.name.rstrip('.') + '.', @@ -1330,7 +1346,7 @@ class Record(object): } try: - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=data) + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=data) logging.debug(jdata) return {'status': 'ok', 'msg': 'Record was added successfully'} except Exception as e: @@ -1356,7 +1372,7 @@ class Record(object): list_deleted_records = [x for x in list_current_records if x not in list_new_records] # convert back to list of hash - deleted_records = [x for x in current_records if [x['name'],x['type']] in list_deleted_records and (x['type'] in app.config['RECORDS_ALLOW_EDIT'] and x['type'] != 'SOA')] + deleted_records = [x for x in current_records if [x['name'],x['type']] in list_deleted_records and (x['type'] in Setting().get_records_allow_to_edit() and x['type'] != 'SOA')] # return a tuple return deleted_records, new_records @@ -1370,8 +1386,8 @@ class Record(object): for r in post_records: r_name = domain if r['record_name'] in ['@', ''] else r['record_name'] + '.' + domain r_type = r['record_type'] - if PRETTY_IPV6_PTR: # only if activated - if NEW_SCHEMA: # only if new schema + if self.PRETTY_IPV6_PTR: # only if activated + if self.NEW_SCHEMA: # only if new schema if r_type == 'PTR': # only ptr if ':' in r['record_name']: # dirty ipv6 check r_name = r['record_name'] @@ -1389,10 +1405,10 @@ class Record(object): records = [] for r in deleted_records: - r_name = r['name'].rstrip('.') + '.' if NEW_SCHEMA else r['name'] + r_name = r['name'].rstrip('.') + '.' if self.NEW_SCHEMA else r['name'] r_type = r['type'] - if PRETTY_IPV6_PTR: # only if activated - if NEW_SCHEMA: # only if new schema + if self.PRETTY_IPV6_PTR: # only if activated + if self.NEW_SCHEMA: # only if new schema if r_type == 'PTR': # only ptr if ':' in r['name']: # dirty ipv6 check r_name = dns.reversename.from_address(r['name']).to_text() @@ -1410,10 +1426,10 @@ class Record(object): records = [] for r in new_records: - if NEW_SCHEMA: + if self.NEW_SCHEMA: r_name = r['name'].rstrip('.') + '.' r_type = r['type'] - if PRETTY_IPV6_PTR: # only if activated + if self.PRETTY_IPV6_PTR: # only if activated if r_type == 'PTR': # only ptr if ':' in r['name']: # dirty ipv6 check r_name = r['name'] @@ -1453,12 +1469,12 @@ class Record(object): final_records = [] records = sorted(records, key = lambda item: (item["name"], item["type"], item["changetype"])) for key, group in itertools.groupby(records, lambda item: (item["name"], item["type"], item["changetype"])): - if NEW_SCHEMA: + if self.NEW_SCHEMA: r_name = key[0] r_type = key[1] r_changetype = key[2] - if PRETTY_IPV6_PTR: # only if activated + if self.PRETTY_IPV6_PTR: # only if activated if r_type == 'PTR': # only ptr if ':' in r_name: # dirty ipv6 check r_name = dns.reversename.from_address(r_name).to_text() @@ -1504,14 +1520,14 @@ class Record(object): }) postdata_for_new = {"rrsets": final_records} - logging.info(postdata_for_new) - logging.info(postdata_for_delete) - logging.info(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain))) + logging.debug(postdata_for_new) + logging.debug(postdata_for_delete) + logging.info(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain))) try: headers = {} - headers['X-API-Key'] = PDNS_API_KEY - jdata1 = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=postdata_for_delete) - jdata2 = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=postdata_for_new) + headers['X-API-Key'] = self.PDNS_API_KEY + jdata1 = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=postdata_for_delete) + jdata2 = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=postdata_for_new) if 'error' in jdata2.keys(): logging.error('Cannot apply record changes.') @@ -1523,7 +1539,8 @@ class Record(object): logging.info('Record was applied successfully.') return {'status': 'ok', 'msg': 'Record was applied successfully'} except Exception as e: - logging.error("Cannot apply record changes to domain {0}. DETAIL: {1}".format(e, domain)) + logging.error("Cannot apply record changes to domain {0}. Error: {1}".format(domain, e)) + logging.debug(traceback.format_exc()) return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'} def auto_ptr(self, domain, new_records, deleted_records): @@ -1534,7 +1551,7 @@ class Record(object): domain_auto_ptr = DomainSetting.query.filter(DomainSetting.domain == domain_obj).filter(DomainSetting.setting == 'auto_ptr').first() domain_auto_ptr = strtobool(domain_auto_ptr.value) if domain_auto_ptr else False - system_auto_ptr = strtobool(Setting().get('auto_ptr')) + system_auto_ptr = Setting().get('auto_ptr') if system_auto_ptr or domain_auto_ptr: try: @@ -1572,7 +1589,7 @@ class Record(object): Delete a record from domain """ headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY data = {"rrsets": [ { "name": self.name.rstrip('.') + '.', @@ -1584,7 +1601,7 @@ class Record(object): ] } try: - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=data) + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=data) logging.debug(jdata) return {'status': 'ok', 'msg': 'Record was removed successfully'} except: @@ -1595,13 +1612,13 @@ class Record(object): """ Check if record is allowed to edit """ - return self.type in app.config['RECORDS_ALLOW_EDIT'] + return self.type in Setting().get_records_allow_to_edit() def is_allowed_delete(self): """ Check if record is allowed to removed """ - return (self.type in app.config['RECORDS_ALLOW_EDIT'] and self.type != 'SOA') + return (self.type in Setting().get_records_allow_to_edit() and self.type != 'SOA') def exists(self, domain): """ @@ -1626,9 +1643,9 @@ class Record(object): Update single record """ headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY - if NEW_SCHEMA: + if self.NEW_SCHEMA: data = {"rrsets": [ { "name": self.name + '.', @@ -1664,7 +1681,7 @@ class Record(object): ] } try: - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=data) + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=data) logging.debug("dyndns data: {0}".format(data)) return {'status': 'ok', 'msg': 'Record was updated successfully'} except Exception as e: @@ -1673,8 +1690,8 @@ class Record(object): 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') + headers['X-API-Key'] = self.PDNS_API_KEY + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='GET') serial = jdata['serial'] domain = Domain.query.filter(Domain.name==domain).first() @@ -1695,16 +1712,21 @@ class Server(object): def __init__(self, server_id=None, server_config=None): self.server_id = server_id self.server_config = server_config + # PDNS configs + self.PDNS_STATS_URL = Setting().get('pdns_api_url') + self.PDNS_API_KEY = Setting().get('pdns_api_key') + self.PDNS_VERSION = Setting().get('pdns_version') + self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION) def get_config(self): """ Get server config """ headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY try: - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/{0}/config'.format(self.server_id)), headers=headers, method='GET') + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/{0}/config'.format(self.server_id)), headers=headers, method='GET') return jdata except: logging.error("Can not get server configuration.") @@ -1716,10 +1738,10 @@ class Server(object): Get server statistics """ headers = {} - headers['X-API-Key'] = PDNS_API_KEY + headers['X-API-Key'] = self.PDNS_API_KEY try: - jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/{0}/statistics'.format(self.server_id)), headers=headers, method='GET') + jdata = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/{0}/statistics'.format(self.server_id)), headers=headers, method='GET') return jdata except: logging.error("Can not get server statistics.") @@ -1773,19 +1795,54 @@ class History(db.Model): class Setting(db.Model): id = db.Column(db.Integer, primary_key = True) name = db.Column(db.String(64)) - value = db.Column(db.String(256)) + value = db.Column(db.Text()) + view = db.Column(db.String(64)) - # default settings (serves as list of known settings too): - # Note: booleans must be strings because of the way they are stored and used defaults = { - 'maintenance': 'False', - 'fullscreen_layout': 'True', - 'record_helper': 'True', - 'login_ldap_first': 'True', + 'maintenance': False, + 'fullscreen_layout': True, + 'record_helper': True, + 'login_ldap_first': True, 'default_record_table_size': 15, 'default_domain_table_size': 10, - 'auto_ptr': 'False', - 'allow_quick_edit': 'True' + 'auto_ptr': False, + 'allow_quick_edit': True, + 'pretty_ipv6_ptr': False, + 'dnssec_admins_only': False, + 'bg_domain_updates': False, + 'site_name': 'PowerDNS-Admin', + 'pdns_api_url': '', + 'pdns_api_key': '', + 'pdns_version': '4.1.1', + 'local_db_enabled': True, + 'signup_enabled': True, + 'ldap_enabled': False, + 'ldap_type': 'ldap', + 'ldap_uri': '', + 'ldap_base_dn': '', + 'ldap_admin_username': '', + 'ldap_admin_password': '', + 'ldap_filter_basic': '', + 'ldap_filter_username': '', + 'ldap_sg_enabled': False, + 'ldap_admin_group': False, + 'ldap_user_group': False, + 'github_oauth_enabled': False, + 'github_oauth_key': '', + 'github_oauth_secret': '', + 'github_oauth_scope': 'email', + 'github_oauth_api_url': 'https://api.github.com/user', + 'github_oauth_token_url': 'https://github.com/login/oauth/access_token', + 'github_oauth_authorize_url': 'https://github.com/login/oauth/authorize', + 'google_oauth_enabled': False, + 'google_oauth_client_id':'', + 'google_oauth_client_secret':'', + 'google_token_url': 'https://accounts.google.com/o/oauth2/token', + 'google_token_params': {'scope': 'email profile'}, + 'google_authorize_url':'https://accounts.google.com/o/oauth2/auth', + 'google_base_url':'https://www.googleapis.com/oauth2/v1/', + 'forward_records_allow_edit': {'A': True, 'AAAA': True, 'AFSDB': False, 'ALIAS': False, 'CAA': True, 'CERT': False, 'CDNSKEY': False, 'CDS': False, 'CNAME': True, 'DNSKEY': False, 'DNAME': False, 'DS': False, 'HINFO': False, 'KEY': False, 'LOC': True, 'MX': True, 'NAPTR': False, 'NS': True, 'NSEC': False, 'NSEC3': False, 'NSEC3PARAM': False, 'OPENPGPKEY': False, 'PTR': True, 'RP': False, 'RRSIG': False, 'SOA': False, 'SPF': True, 'SSHFP': False, 'SRV': True, 'TKEY': False, 'TSIG': False, 'TLSA': False, 'SMIMEA': False, 'TXT': True, 'URI': False}, + 'reverse_records_allow_edit': {'A': False, 'AAAA': False, 'AFSDB': False, 'ALIAS': False, 'CAA': False, 'CERT': False, 'CDNSKEY': False, 'CDS': False, 'CNAME': False, 'DNSKEY': False, 'DNAME': False, 'DS': False, 'HINFO': False, 'KEY': False, 'LOC': True, 'MX': False, 'NAPTR': False, 'NS': True, 'NSEC': False, 'NSEC3': False, 'NSEC3PARAM': False, 'OPENPGPKEY': False, 'PTR': True, 'RP': False, 'RRSIG': False, 'SOA': False, 'SPF': False, 'SSHFP': False, 'SRV': False, 'TKEY': False, 'TSIG': False, 'TLSA': False, 'SMIMEA': False, 'TXT': True, 'URI': False}, } def __init__(self, id=None, name=None, value=None): @@ -1804,7 +1861,7 @@ class Setting(db.Model): if maintenance is None: value = self.defaults['maintenance'] - maintenance = Setting(name='maintenance', value=value) + maintenance = Setting(name='maintenance', value=str(value)) db.session.add(maintenance) mode = str(mode) @@ -1825,7 +1882,7 @@ class Setting(db.Model): if current_setting is None: value = self.defaults[setting] - current_setting = Setting(name=setting, value=value) + current_setting = Setting(name=setting, value=str(value)) db.session.add(current_setting) try: @@ -1864,12 +1921,31 @@ class Setting(db.Model): if setting in self.defaults: result = self.query.filter(Setting.name == setting).first() if result is not None: - return result.value + return strtobool(result.value) if result.value in ['True', 'False'] else result.value else: return self.defaults[setting] else: logging.error('Unknown setting queried: {0}'.format(setting)) + def get_records_allow_to_edit(self): + return list(set(self.get_forward_records_allow_to_edit() + self.get_reverse_records_allow_to_edit())) + + def get_forward_records_allow_to_edit(self): + records = literal_eval(self.get('forward_records_allow_edit')) + return [r for r in records if records[r]] + + def get_reverse_records_allow_to_edit(self): + records = literal_eval(self.get('reverse_records_allow_edit')) + return [r for r in records if records[r]] + + def get_view(self, view): + r = {} + settings = Setting.query.filter(Setting.view == view).all() + for setting in settings: + d = setting.__dict__ + r[d['name']] = d['value'] + return r + class DomainTemplate(db.Model): __tablename__ = "domain_template" diff --git a/app/oauth.py b/app/oauth.py new file mode 100644 index 0000000..17f30a0 --- /dev/null +++ b/app/oauth.py @@ -0,0 +1,78 @@ +from ast import literal_eval +from flask import request, session, redirect, url_for +from flask_oauthlib.client import OAuth + +from app import app, oauth +from app.models import Setting + +# TODO: +# - Replace Flask-OAuthlib by authlib +# - Fix github/google enabling (Currently need to reload the flask app) + +def github_oauth(): + if not Setting().get('github_oauth_enabled'): + return None + + github = oauth.remote_app( + 'github', + consumer_key = Setting().get('github_oauth_key'), + consumer_secret = Setting().get('github_oauth_secret'), + request_token_params = {'scope': Setting().get('github_oauth_scope')}, + base_url = Setting().get('github_oauth_api_url'), + request_token_url = None, + access_token_method = 'POST', + access_token_url = Setting().get('github_oauth_token_url'), + authorize_url = Setting().get('github_oauth_authorize_url') + ) + + @app.route('/github/authorized') + def github_authorized(): + session['github_oauthredir'] = url_for('.github_authorized', _external=True) + resp = github.authorized_response() + if resp is None: + return 'Access denied: reason=%s error=%s' % ( + request.args['error'], + request.args['error_description'] + ) + session['github_token'] = (resp['access_token'], '') + return redirect(url_for('.login')) + + @github.tokengetter + def get_github_oauth_token(): + return session.get('github_token') + + return github + + +def google_oauth(): + if not Setting().get('google_oauth_enabled'): + return None + + google = oauth.remote_app( + 'google', + consumer_key=Setting().get('google_oauth_client_id'), + consumer_secret=Setting().get('google_oauth_client_secret'), + request_token_params=literal_eval(Setting().get('google_token_params')), + base_url=Setting().get('google_base_url'), + request_token_url=None, + access_token_method='POST', + access_token_url=Setting().get('google_token_url'), + authorize_url=Setting().get('google_authorize_url'), + ) + + @app.route('/google/authorized') + def google_authorized(): + resp = google.authorized_response() + if resp is None: + return 'Access denied: reason=%s error=%s' % ( + request.args['error_reason'], + request.args['error_description'] + ) + session['google_token'] = (resp['access_token'], '') + return redirect(url_for('.login')) + + @google.tokengetter + def get_google_oauth_token(): + return session.get('google_token') + + return google diff --git a/app/templates/admin.html b/app/templates/admin.html index c229bc0..3d7a917 100644 --- a/app/templates/admin.html +++ b/app/templates/admin.html @@ -1,5 +1,6 @@ {% extends "base.html" %} -{% block title %}DNS Control Panel - Admin Console{% endblock %} +{% set active_page = "admin_console" %} +{% block title %}Admin Console - {{ SITE_NAME }}{% endblock %} {% block dashboard_stat %} diff --git a/app/templates/admin_editaccount.html b/app/templates/admin_editaccount.html index 1f31650..7a46a89 100644 --- a/app/templates/admin_editaccount.html +++ b/app/templates/admin_editaccount.html @@ -1,5 +1,6 @@ {% extends "base.html" %} -{% block title %}DNS Control Panel - Edit Account{% endblock %} +{% set active_page = "admin_accounts" %} +{% block title %}Edit Account - {{ SITE_NAME }}{% endblock %} {% block dashboard_stat %} diff --git a/app/templates/admin_edituser.html b/app/templates/admin_edituser.html index 83e861d..d374d22 100644 --- a/app/templates/admin_edituser.html +++ b/app/templates/admin_edituser.html @@ -1,5 +1,6 @@ {% extends "base.html" %} -{% block title %}DNS Control Panel - Edit User{% endblock %} +{% set active_page = "admin_users" %} +{% block title %}Edit Use - {{ SITE_NAME }}{% endblock %} {% block dashboard_stat %} diff --git a/app/templates/admin_history.html b/app/templates/admin_history.html index c146d52..936cc7a 100644 --- a/app/templates/admin_history.html +++ b/app/templates/admin_history.html @@ -1,5 +1,7 @@ -{% extends "base.html" %} {% block title %} -DNS Control Panel - History +{% extends "base.html" %} +{% set active_page = "admin_history" %} +{% block title %} +History - {{ SITE_NAME }} {% endblock %} {% block dashboard_stat %}
diff --git a/app/templates/admin_manageaccount.html b/app/templates/admin_manageaccount.html index 7af5125..579d463 100644 --- a/app/templates/admin_manageaccount.html +++ b/app/templates/admin_manageaccount.html @@ -1,5 +1,7 @@ -{% extends "base.html" %} {% block title %} -DNS Control Panel - Account Management +{% extends "base.html" %} +{% set active_page = "admin_accounts" %} +{% block title %} +Account Management - {{ SITE_NAME }} {% endblock %} {% block dashboard_stat %}

diff --git a/app/templates/admin_manageuser.html b/app/templates/admin_manageuser.html index 24d70e9..653738e 100644 --- a/app/templates/admin_manageuser.html +++ b/app/templates/admin_manageuser.html @@ -1,5 +1,7 @@ -{% extends "base.html" %} {% block title %} -DNS Control Panel - User Management +{% extends "base.html" %} +{% set active_page = "admin_users" %} +{% block title %} +User Management - {{ SITE_NAME }} {% endblock %} {% block dashboard_stat %}

diff --git a/app/templates/admin_setting_authentication.html b/app/templates/admin_setting_authentication.html new file mode 100644 index 0000000..e929065 --- /dev/null +++ b/app/templates/admin_setting_authentication.html @@ -0,0 +1,495 @@ +{% extends "base.html" %} +{% set active_page = "admin_settings" %} +{% block title %} +Authentication Settings - {{ SITE_NAME }} +{% endblock %} {% block dashboard_stat %} + +
+

+ Settings PowerDNS-Admin settings +

+ +
+{% endblock %} +{% block content %} +
+
+
+
+
+

Authentication Settings

+
+
+ {% if result %} +
+ + {{ result['msg'] }} +
+ {% endif %} + + +
+
+
+
+
+{% endblock %} +{% block extrascripts %} + +{% assets "js_validation" -%} + +{%- endassets %} + + +{% endblock %} diff --git a/app/templates/admin_settings.html b/app/templates/admin_setting_basic.html similarity index 70% rename from app/templates/admin_settings.html rename to app/templates/admin_setting_basic.html index 3eafc46..b289f75 100644 --- a/app/templates/admin_settings.html +++ b/app/templates/admin_setting_basic.html @@ -1,5 +1,7 @@ -{% extends "base.html" %} {% block title %} -DNS Control Panel - Settings +{% extends "base.html" %} +{% set active_page = "admin_settings" %} +{% block title %} +Basic Settings - {{ SITE_NAME }} {% endblock %} {% block dashboard_stat %}
@@ -7,9 +9,9 @@ Settings PowerDNS-Admin settings

{% endblock %} {% block content %} @@ -18,7 +20,7 @@
-

Settings Management

+

Basic Settings

@@ -30,21 +32,21 @@ - {% for setting_name, setting_value in settings.items() %} + {% for setting in settings %} - - {% if setting_value == "True" or setting_value == "False" %} - + + {% if setting.value == "True" or setting.value == "False" %} + {% else %} - + {% endif %}
{{ setting_name }}{{ setting_value }}{{ setting.name }}{{ setting.value }} - {% if setting_value == "True" or setting_value == "False" %} - {% else %} - {% endif %} @@ -76,14 +78,14 @@ }); $(document.body).on('click', '.setting-toggle-button', function() { var setting = $(this).prop('id'); - applyChanges('', $SCRIPT_ROOT + '/admin/setting/' + setting + '/toggle', false, true) + applyChanges('', $SCRIPT_ROOT + '/admin/setting/basic/' + setting + '/toggle', false, true) }); $(document.body).on('click', '.setting-save-button', function() { var setting = $(this).prop('id'); var value = $(this).parents('tr').find('#value')[0].value; var postdata = {'value': value}; - applyChanges(postdata, $SCRIPT_ROOT + '/admin/setting/' + setting + '/edit', false, true) + applyChanges(postdata, $SCRIPT_ROOT + '/admin/setting/basic/' + setting + '/edit', false, true) }); {% endblock %} diff --git a/app/templates/admin_setting_pdns.html b/app/templates/admin_setting_pdns.html new file mode 100644 index 0000000..528b73c --- /dev/null +++ b/app/templates/admin_setting_pdns.html @@ -0,0 +1,85 @@ +{% extends "base.html" %} +{% set active_page = "admin_settings" %} +{% block title %} +PDNS Settings - {{ SITE_NAME }} +{% endblock %} {% block dashboard_stat %} + +
+

+ Settings PowerDNS-Admin settings +

+ +
+{% endblock %} +{% block content %} +
+
+
+
+
+

PDNS Settings

+
+ + +
+
+ {% if not SETTING.get('pdns_api_url') or not SETTING.get('pdns_api_key') or not SETTING.get('pdns_version') %} +
+ +

Error!

+ Please complete your PowerDNS API configuration before continuing +
+ {% endif %} +
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+
+
+

Help

+
+
+
+

You must configure the API connection information before PowerDNS-Admiin can query your PowerDNS data. Following fields are required:

+
PDNS API URL
+
Your PowerDNS API URL (eg. http://127.0.0.1:8081/).
+
PDNS API KEY
+
Your PowerDNS API key.
+
PDNS VERSION
+
Your PowerDNS version number (eg. 4.1.1).
+
+

Find more details at https://doc.powerdns.com/md/httpapi/README/

+
+
+
+
+
+{% endblock %} +{% block extrascripts %} + {% assets "js_validation" -%} + + {%- endassets %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/admin_setting_records.html b/app/templates/admin_setting_records.html new file mode 100644 index 0000000..fd1f316 --- /dev/null +++ b/app/templates/admin_setting_records.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} +{% set active_page = "admin_settings" %} +{% block title %} +DNS Records Settings - {{ SITE_NAME }} +{% endblock %} {% block dashboard_stat %} + +
+

+ Settings PowerDNS-Admin settings +

+ +
+{% endblock %} +{% block content %} +
+
+
+
+
+

DNS record Settings

+
+ + +
+ +
+ + + + + + + + {% for record in f_records %} + + + + + + + {% endfor %} +
#RecordForward ZoneReverse Zone
{{ loop.index }}{{ record }} + + + +
+
+ +
+
+
+
+
+
+

Help

+
+
+

Select record types you allow user to edit in the forward zone and reverse zone. Take a look at PowerDNS docs for full list of supported record types.

+
+
+
+
+
+{% endblock %} +{% block extrascripts %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 432274b..98845ea 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -4,7 +4,7 @@ {% block head %} - {% block title %}DNS Control Panel{% endblock %} + {% block title %}{{ SITE_NAME }}{% endblock %} @@ -21,7 +21,7 @@ {% endblock %} - +
{% block pageheader %}
@@ -105,17 +105,45 @@ {% endif %} @@ -146,7 +174,7 @@
{% block scripts %} {% assets "js_main" -%} diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 6ce2180..b1d3400 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -1,5 +1,6 @@ {% extends "base.html" %} -{% block title %}DNS Control Panel - HOME{% endblock %} +{% set active_page = "dashboard" %} +{% block title %}Dashboard - {{ SITE_NAME }}{% endblock %} {% block dashboard_stat %} @@ -155,7 +156,7 @@ {% endblock %} {% block extrascripts %} {%- endassets %} +{% assets "js_validation" -%} + +{%- endassets %} + -{%- endassets %} - - - + + + + + + Register - {{ SITE_NAME }} + + + {% assets "css_login" -%} + + {%- endassets %} + + + + + + +
+ +
+ {% if error %} +
+ + {{ error }} +
+ {% endif %} + +
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+ + + +
+
+ + +
+
+ + + +
+
+
+ +
+
+ +
+ +
+
+
+ + +
+ + +{% assets "js_login" -%} + +{%- endassets %} +{% assets "js_validation" -%} + +{%- endassets %} + + + diff --git a/app/templates/template.html b/app/templates/template.html index 19c8efc..4f1e1fe 100644 --- a/app/templates/template.html +++ b/app/templates/template.html @@ -1,5 +1,6 @@ {% extends "base.html" %} -{% block title %}DNS Control Panel - Templates{% endblock %} +{% set active_page = "admin_domain_template" %} +{% block title %}Templates - {{ SITE_NAME }}{% endblock %} {% block dashboard_stat %} diff --git a/app/templates/template_add.html b/app/templates/template_add.html index 772c204..35dfda5 100644 --- a/app/templates/template_add.html +++ b/app/templates/template_add.html @@ -1,5 +1,6 @@ {% extends "base.html" %} -{% block title %}DNS Control Panel - Create Template{% endblock %} +{% set active_page = "admin_domain_template" %} +{% block title %}Create Template - {{ SITE_NAME }}{% endblock %} {% block dashboard_stat %} diff --git a/app/templates/template_edit.html b/app/templates/template_edit.html index 3445eb0..69a6c7c 100644 --- a/app/templates/template_edit.html +++ b/app/templates/template_edit.html @@ -1,5 +1,6 @@ {% extends "base.html" %} -{% block title %}DNS Control Panel - Edit Template{% endblock %} +{% set active_page = "admin_domain_template" %} +{% block title %}Edit Template - {{ SITE_NAME }}{% endblock %} {% block dashboard_stat %}
@@ -102,14 +103,14 @@ "ordering" : true, "info" : true, "autoWidth" : false, - {% if default_record_table_size_setting in ['5','15','20'] %} + {% if SETTING.get('default_record_table_size') in ['5','15','20'] %} "lengthMenu": [ [5, 15, 20, -1], [5, 15, 20, "All"]], {% else %} - "lengthMenu": [ [5, 15, 20, {{ default_record_table_size_setting }}, -1], - [5, 15, 20, {{ default_record_table_size_setting }}, "All"]], + "lengthMenu": [ [5, 15, 20, {{ SETTING.get('default_record_table_size') }}, -1], + [5, 15, 20, {{ SETTING.get('default_record_table_size') }}, "All"]], {% endif %} - "pageLength": {{ default_record_table_size_setting }}, + "pageLength": {{ SETTING.get('default_record_table_size') }}, "language": { "lengthMenu": " _MENU_ records" }, @@ -223,7 +224,7 @@ nNew = false; }); - {% if record_helper_setting %} + {% if SETTING.get('record_helper') %} //handle wacky record types $(document.body).on("focus", "#current_edit_record_data", function (e) { var record_type = $(this).parents("tr").find('#record_type').val(); diff --git a/app/templates/user_profile.html b/app/templates/user_profile.html index 37ec41d..6b9e3ba 100644 --- a/app/templates/user_profile.html +++ b/app/templates/user_profile.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% block title %}DNS Control Panel - My Profile{% endblock %} +{% block title %}My Profile - {{ SITE_NAME }}{% endblock %} {% block dashboard_stat %}
@@ -19,7 +19,7 @@
-

Edit my profile{% if external_account %} [Disabled - Authenticated externally]{% endif %}

+

Edit my profile{% if session['authentication_type'] != 'LOCAL' %} [Disabled - Authenticated externally]{% endif %}

@@ -29,10 +29,11 @@ Info
  • Change Avatar
  • - {% if not external_account %}
  • Change - Password
  • -
  • Authentication -
  • + {% if session['authentication_type'] == 'LOCAL' %} +
  • Change Password
  • + {% endif %} + {% if session['authentication_type'] in ['LOCAL', 'LDAP'] %} +
  • Authentication
  • {% endif %}
    @@ -41,18 +42,18 @@
    + placeholder="{{ current_user.firstname }}" {% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
    + placeholder="{{ current_user.lastname }}" {% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
    -
    {% if not external_account %} + placeholder="{{ current_user.email }}" {% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}> +
    {% if session['authentication_type'] == 'LOCAL' %}
    {% endif %} @@ -70,50 +71,50 @@ else %} {% endif %} -
    {% if not external_account %} +
    {% if session['authentication_type'] == 'LOCAL' %}
    {% endif %} -
    {% if not external_account %} + {% if session['authentication_type'] == 'LOCAL' %}
    NOTE!  Only supports .PNG, .JPG, .JPEG. The best size to use is 200x200.
    {% endif %} - {% if not external_account %} + {% if session['authentication_type'] == 'LOCAL' %}
    {% endif %} - {% if not external_account %}
    - {% if not current_user.password %} Your account password is - managed via LDAP which isn't supported to change here. {% else - %} + {% if session['authentication_type'] == 'LOCAL' %} +
    + {% if not current_user.password %} + Your account password is managed via LDAP which isn't supported to change here. + {% else %}
    + type="password" class="form-control" name="password" id="newpassword"/>
    + type="password" class="form-control" name="rpassword" id="rpassword"/>
    - +
    {% endif %}
    + {% endif %} +
    - + {% if current_user.otp_secret %}
    @@ -125,7 +126,8 @@ {% endif %}
    -
    {% endif %} +
    +
    diff --git a/app/views.py b/app/views.py index 3cf5760..e5e9bd6 100644 --- a/app/views.py +++ b/app/views.py @@ -8,6 +8,7 @@ from distutils.util import strtobool from distutils.version import StrictVersion from functools import wraps from io import BytesIO +from ast import literal_eval import jinja2 import qrcode as qrc @@ -18,79 +19,58 @@ from werkzeug import secure_filename from werkzeug.security import gen_salt from .models import User, Account, Domain, Record, Role, Server, History, Anonymous, Setting, DomainSetting, DomainTemplate, DomainTemplateRecord -from app import app, login_manager, github, google +from app import app, login_manager from app.lib import utils +from app.oauth import github_oauth, google_oauth from app.decorators import admin_role_required, can_access_domain, can_configure_dnssec if app.config['SAML_ENABLED']: from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.utils import OneLogin_Saml2_Utils +google = None +github = None logging = logger.getLogger(__name__) + # FILTERS app.jinja_env.filters['display_record_name'] = utils.display_record_name app.jinja_env.filters['display_master_name'] = utils.display_master_name app.jinja_env.filters['display_second_to_time'] = utils.display_time app.jinja_env.filters['email_to_gravatar_url'] = utils.email_to_gravatar_url -# Flag for pdns v4.x.x -# TODO: Find another way to do this -PDNS_VERSION = app.config['PDNS_VERSION'] -if StrictVersion(PDNS_VERSION) >= StrictVersion('4.0.0'): - NEW_SCHEMA = True -else: - NEW_SCHEMA = False - @app.context_processor -def inject_fullscreen_layout_setting(): - setting_value = Setting().get('fullscreen_layout') - return dict(fullscreen_layout_setting=strtobool(setting_value)) - +def inject_sitename(): + setting = Setting().get('site_name') + return dict(SITE_NAME=setting) @app.context_processor -def inject_record_helper_setting(): - setting_value = Setting().get('record_helper') - return dict(record_helper_setting=strtobool(setting_value)) +def inject_setting(): + setting = Setting() + return dict(SETTING=setting) -@app.context_processor -def inject_login_ldap_first_setting(): - setting_value = Setting().get('login_ldap_first') - return dict(login_ldap_first_setting=strtobool(setting_value)) - - -@app.context_processor -def inject_default_record_table_size_setting(): - setting_value = Setting().get('default_record_table_size') - return dict(default_record_table_size_setting=setting_value) - - -@app.context_processor -def inject_default_domain_table_size_setting(): - setting_value = Setting().get('default_domain_table_size') - return dict(default_domain_table_size_setting=setting_value) - - -@app.context_processor -def inject_auto_ptr_setting(): - setting_value = Setting().get('auto_ptr') - return dict(auto_ptr_setting=strtobool(setting_value)) +@app.before_first_request +def register_modules(): + global google + global github + google = google_oauth() + github = github_oauth() # START USER AUTHENTICATION HANDLER @app.before_request def before_request(): - # check site maintenance mode first - maintenance = Setting().get('maintenance') - if strtobool(maintenance): - return render_template('maintenance.html') - # check if user is anonymous g.user = current_user login_manager.anonymous_user = Anonymous + # check site maintenance mode + maintenance = Setting().get('maintenance') + if maintenance and current_user.is_authenticated and current_user.role.name != 'Administrator': + return render_template('maintenance.html') + @login_manager.user_loader def load_user(id): @@ -164,8 +144,7 @@ def error(code, msg=None): @app.route('/register', methods=['GET']) def register(): - SIGNUP_ENABLED = app.config['SIGNUP_ENABLED'] - if SIGNUP_ENABLED: + if Setting().get('signup_enabled'): return render_template('register.html') else: return render_template('errors/404.html'), 404 @@ -173,16 +152,21 @@ def register(): @app.route('/google/login') def google_login(): - if not app.config.get('GOOGLE_OAUTH_ENABLE'): + if not Setting().get('google_oauth_enabled') or google is None: + logging.error('Google OAuth is disabled or you have not yet reloaded the pda application after enabling.') return abort(400) - return google.authorize(callback=url_for('authorized', _external=True)) + else: + return google.authorize(callback=url_for('google_authorized', _external=True)) @app.route('/github/login') def github_login(): - if not app.config.get('GITHUB_OAUTH_ENABLE'): + if not Setting().get('github_oauth_enabled') or github is None: + logging.error('Github OAuth is disabled or you have not yet reloaded the pda application after enabling.') return abort(400) - return github.authorize(callback=url_for('authorized', _external=True)) + else: + return github.authorize(callback=url_for('github_authorized', _external=True)) + @app.route('/saml/login') def saml_login(): @@ -193,6 +177,7 @@ def saml_login(): redirect_url=OneLogin_Saml2_Utils.get_self_url(req) + url_for('saml_authorized') return redirect(auth.login(return_to=redirect_url)) + @app.route('/saml/metadata') def saml_metadata(): if not app.config.get('SAML_ENABLED'): @@ -210,6 +195,7 @@ def saml_metadata(): resp = make_response(errors.join(', '), 500) return resp + @app.route('/saml/authorized', methods=['GET', 'POST']) def saml_authorized(): errors = [] @@ -288,21 +274,17 @@ def saml_authorized(): history.add() user.plain_text_password = None user.update_profile() - session['external_auth'] = True + session['authentication_type'] = 'SAML' login_user(user, remember=False) return redirect(url_for('index')) else: return render_template('errors/SAML.html', errors=errors) + @app.route('/login', methods=['GET', 'POST']) @login_manager.unauthorized_handler def login(): LOGIN_TITLE = app.config['LOGIN_TITLE'] if 'LOGIN_TITLE' in app.config.keys() else '' - BASIC_ENABLED = app.config['BASIC_ENABLED'] - SIGNUP_ENABLED = app.config['SIGNUP_ENABLED'] - LDAP_ENABLED = app.config.get('LDAP_ENABLED') - GITHUB_ENABLE = app.config.get('GITHUB_OAUTH_ENABLE') - GOOGLE_ENABLE = app.config.get('GOOGLE_OAUTH_ENABLE') SAML_ENABLED = app.config.get('SAML_ENABLED') if g.user is not None and current_user.is_authenticated: @@ -315,7 +297,6 @@ def login(): email = user_data['email'] user = User.query.filter_by(username=email).first() if not user: - # create user user = User(username=email, firstname=first_name, lastname=surname, @@ -329,18 +310,23 @@ def login(): session['user_id'] = user.id login_user(user, remember = False) - session['external_auth'] = True + session['authentication_type'] = 'OAuth' return redirect(url_for('index')) if 'github_token' in session: - me = github.get('user') - user_info = me.data - user = User.query.filter_by(username=user_info['name']).first() + me = github.get('user').data + + github_username = me['login'] + github_name = me['name'] + github_email = me['email'] + + user = User.query.filter_by(username=github_username).first() if not user: - # create user - user = User(username=user_info['name'], + user = User(username=github_username, plain_text_password=None, - email=user_info['email']) + firstname=github_name, + lastname='', + email=github_email) result = user.create_local_user() if not result['status']: @@ -348,18 +334,12 @@ def login(): return redirect(url_for('login')) session['user_id'] = user.id - session['external_auth'] = True + session['authentication_type'] = 'OAuth' login_user(user, remember = False) 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', saml_enabled=SAML_ENABLED) # process login username = request.form['username'] @@ -373,8 +353,7 @@ def login(): email = request.form.get('email') rpassword = request.form.get('rpassword') - if auth_method != 'LOCAL': - session['external_auth'] = True + session['authentication_type'] = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL' if None in [firstname, lastname, email]: #login case @@ -387,46 +366,18 @@ def login(): try: auth = user.is_validate(method=auth_method, src_ip=request.remote_addr) if auth == False: - 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) + return render_template('login.html', saml_enabled=SAML_ENABLED, error='Invalid credentials') except Exception as e: - 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) + return render_template('login.html', saml_enabled=SAML_ENABLED, error=e) # check if user enabled OPT authentication if user.otp_secret: if otp_token and otp_token.isdigit(): good_token = user.verify_totp(otp_token) if not good_token: - 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) + return render_template('login.html', saml_enabled=SAML_ENABLED, error='Invalid credentials') else: - 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) + return render_template('login.html', saml_enabled=SAML_ENABLED, error='Token required') login_user(user, remember = remember_me) return redirect(request.args.get('next') or url_for('index')) @@ -437,35 +388,29 @@ def login(): # registration case user = User(username=username, plain_text_password=password, firstname=firstname, lastname=lastname, email=email) - # TODO: Move this into the JavaScript - # validate password and password confirmation if password != rpassword: error = "Password confirmation does not match" return render_template('register.html', error=error) try: result = user.create_local_user() - if result == True: - 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) + if result and result['status']: + return render_template('login.html', saml_enabled=SAML_ENABLED, username=username, password=password) else: return render_template('register.html', error=result['msg']) except Exception as e: return render_template('register.html', error=e) + def clear_session(): session.pop('user_id', None) session.pop('github_token', None) session.pop('google_token', None) + session.pop('authentication_type', None) session.clear() logout_user() + @app.route('/logout') def logout(): if app.config.get('SAML_ENABLED') and 'samlSessionIndex' in session and app.config.get('SAML_LOGOUT'): @@ -474,13 +419,14 @@ def logout(): if app.config.get('SAML_LOGOUT_URL'): return redirect(auth.logout(name_id_format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", return_to = app.config.get('SAML_LOGOUT_URL'), - session_index = session['samlSessionIndex'], name_id=session['samlNameId'])) + session_index = session['samlSessionIndex'], name_id=session['samlNameId'])) return redirect(auth.logout(name_id_format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", - session_index = session['samlSessionIndex'], + session_index = session['samlSessionIndex'], name_id=session['samlNameId'])) clear_session() return redirect(url_for('login')) + @app.route('/saml/sls') def saml_logout(): req = utils.prepare_flask_request(request) @@ -498,10 +444,15 @@ def saml_logout(): else: return render_template('errors/SAML.html', errors=errors) + @app.route('/dashboard', methods=['GET', 'POST']) @login_required def dashboard(): - if not app.config.get('BG_DOMAIN_UPDATES'): + if not Setting().get('pdns_api_url') or not Setting().get('pdns_api_key') or not Setting().get('pdns_version'): + return redirect(url_for('admin_setting_pdns')) + + BG_DOMAIN_UPDATE = Setting().get('bg_domain_updates') + if not BG_DOMAIN_UPDATE: logging.debug('Update domains in foreground') d = Domain().update() else: @@ -519,7 +470,7 @@ def dashboard(): else: uptime = 0 - return render_template('dashboard.html', domain_count=domain_count, users=users, history_number=history_number, uptime=uptime, histories=history, dnssec_adm_only=app.config['DNSSEC_ADMINS_ONLY'], pdns_version=app.config['PDNS_VERSION'], show_bg_domain_button=app.config['BG_DOMAIN_UPDATES']) + return render_template('dashboard.html', domain_count=domain_count, users=users, history_number=history_number, uptime=uptime, histories=history, show_bg_domain_button=BG_DOMAIN_UPDATE) @app.route('/dashboard-domains', methods=['GET']) @@ -621,31 +572,33 @@ def domain(domain_name): # can not get any record, API server might be down return redirect(url_for('error', code=500)) - quick_edit = strtobool(Setting().get('allow_quick_edit')) - + quick_edit = Setting().get('allow_quick_edit') + records_allow_to_edit = Setting().get_records_allow_to_edit() + forward_records_allow_to_edit = Setting().get_forward_records_allow_to_edit() + reverse_records_allow_to_edit = Setting().get_reverse_records_allow_to_edit() records = [] - #TODO: This should be done in the "model" instead of "view" - if NEW_SCHEMA: + + if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'): for jr in jrecords: - if jr['type'] in app.config['RECORDS_ALLOW_EDIT']: + if jr['type'] in Setting().get_records_allow_to_edit(): for subrecord in jr['records']: record = Record(name=jr['name'], type=jr['type'], status='Disabled' if subrecord['disabled'] else 'Active', ttl=jr['ttl'], data=subrecord['content']) records.append(record) if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name): - editable_records = app.config['RECORDS_ALLOW_EDIT'] + editable_records = forward_records_allow_to_edit else: - editable_records = app.config['REVERSE_RECORDS_ALLOW_EDIT'] + editable_records = reverse_records_allow_to_edit return render_template('domain.html', domain=domain, records=records, editable_records=editable_records, quick_edit=quick_edit) else: for jr in jrecords: - if jr['type'] in app.config['RECORDS_ALLOW_EDIT']: + if jr['type'] in Setting().get_records_allow_to_edit(): record = Record(name=jr['name'], type=jr['type'], status='Disabled' if jr['disabled'] else 'Active', ttl=jr['ttl'], data=jr['content']) records.append(record) if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name): - editable_records = app.config['FORWARD_RECORDS_ALLOW_EDIT'] + editable_records = forward_records_allow_to_edit else: - editable_records = app.config['REVERSE_RECORDS_ALLOW_EDIT'] - return render_template('domain.html', domain=domain, records=records, editable_records=editable_records, quick_edit=quick_edit, pdns_version=app.config['PDNS_VERSION']) + editable_records = reverse_records_allow_to_edit + return render_template('domain.html', domain=domain, records=records, editable_records=editable_records, quick_edit=quick_edit) @app.route('/admin/domain/add', methods=['GET', 'POST']) @@ -1036,16 +989,16 @@ def create_template_from_zone(): if zone_info: jrecords = zone_info['records'] - if NEW_SCHEMA: + if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'): for jr in jrecords: - if jr['type'] in app.config['RECORDS_ALLOW_EDIT']: + if jr['type'] in Setting().get_records_allow_to_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']: + if jr['type'] in Setting().get_records_allow_to_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) @@ -1071,14 +1024,15 @@ def create_template_from_zone(): def edit_template(template): try: t = DomainTemplate.query.filter(DomainTemplate.name == template).first() + records_allow_to_edit = Setting().get_records_allow_to_edit() if t is not None: records = [] for jr in t.records: - if jr.type in app.config['RECORDS_ALLOW_EDIT']: + if jr.type in records_allow_to_edit: record = DomainTemplateRecord(name=jr.name, type=jr.type, status='Disabled' if jr.status else 'Active', ttl=jr.ttl, data=jr.data) records.append(record) - return render_template('template_edit.html', template=t.name, records=records, editable_records=app.config['RECORDS_ALLOW_EDIT']) + return render_template('template_edit.html', template=t.name, records=records, editable_records=records_allow_to_edit) except: logging.error(traceback.print_exc()) return redirect(url_for('error', code=500)) @@ -1140,6 +1094,9 @@ def delete_template(template): @login_required @admin_role_required def admin(): + if not Setting().get('pdns_api_url') or not Setting().get('pdns_api_key') or not Setting().get('pdns_version'): + return redirect(url_for('admin_setting_pdns')) + domains = Domain.query.all() users = User.query.all() @@ -1374,39 +1331,19 @@ def admin_history(): return render_template('admin_history.html', histories=histories) -@app.route('/admin/settings', methods=['GET']) +@app.route('/admin/setting/basic', methods=['GET']) @login_required @admin_role_required -def admin_settings(): +def admin_setting_basic(): if request.method == 'GET': - # start with a copy of the setting defaults (ignore maintenance setting) - settings = Setting.defaults.copy() - settings.pop('maintenance', None) - - # update settings info with any customizations - for s in settings: - value = Setting().get(s) - if value is not None: - settings[s] = value - - return render_template('admin_settings.html', settings=settings) + settings = Setting.query.filter(Setting.view=='basic').all() + return render_template('admin_setting_basic.html', settings=settings) -@app.route('/admin/setting//toggle', methods=['POST']) +@app.route('/admin/setting/basic//edit', methods=['POST']) @login_required @admin_role_required -def admin_settings_toggle(setting): - result = Setting().toggle(setting) - if (result): - return make_response(jsonify( { 'status': 'ok', 'msg': 'Toggled setting successfully.' } ), 200) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to toggle setting.' } ), 500) - - -@app.route('/admin/setting//edit', methods=['POST']) -@login_required -@admin_role_required -def admin_settings_edit(setting): +def admin_setting_basic_edit(setting): jdata = request.json new_value = jdata['value'] result = Setting().set(setting, new_value) @@ -1417,48 +1354,168 @@ def admin_settings_edit(setting): return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to toggle setting.' } ), 500) +@app.route('/admin/setting/basic//toggle', methods=['POST']) +@login_required +@admin_role_required +def admin_setting_basic_toggle(setting): + result = Setting().toggle(setting) + if (result): + return make_response(jsonify( { 'status': 'ok', 'msg': 'Toggled setting successfully.' } ), 200) + else: + return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to toggle setting.' } ), 500) + + +@app.route('/admin/setting/pdns', methods=['GET', 'POST']) +@login_required +@admin_role_required +def admin_setting_pdns(): + if request.method == 'GET': + pdns_api_url = Setting().get('pdns_api_url') + pdns_api_key = Setting().get('pdns_api_key') + pdns_version = Setting().get('pdns_version') + return render_template('admin_setting_pdns.html', pdns_api_url=pdns_api_url, pdns_api_key=pdns_api_key, pdns_version=pdns_version) + elif request.method == 'POST': + pdns_api_url = request.form.get('pdns_api_url') + pdns_api_key = request.form.get('pdns_api_key') + pdns_version = request.form.get('pdns_version') + + Setting().set('pdns_api_url', pdns_api_url) + Setting().set('pdns_api_key', pdns_api_key) + Setting().set('pdns_version', pdns_version) + + return render_template('admin_setting_pdns.html', pdns_api_url=pdns_api_url, pdns_api_key=pdns_api_key, pdns_version=pdns_version) + + +@app.route('/admin/setting/dns-records', methods=['GET', 'POST']) +@login_required +@admin_role_required +def admin_setting_records(): + if request.method == 'GET': + f_records = literal_eval(Setting().get('forward_records_allow_edit')) + r_records = literal_eval(Setting().get('reverse_records_allow_edit')) + return render_template('admin_setting_records.html', f_records=f_records, r_records=r_records) + elif request.method == 'POST': + fr = {} + rr = {} + records = Setting().defaults['forward_records_allow_edit'] + for r in records: + fr[r] = True if request.form.get('fr_{0}'.format(r.lower())) else False + rr[r] = True if request.form.get('rr_{0}'.format(r.lower())) else False + + Setting().set('forward_records_allow_edit', str(fr)) + Setting().set('reverse_records_allow_edit', str(rr)) + return redirect(url_for('admin_setting_records')) + + +@app.route('/admin/setting/authentication', methods=['GET', 'POST']) +@login_required +@admin_role_required +def admin_setting_authentication(): + if request.method == 'GET': + return render_template('admin_setting_authentication.html') + elif request.method == 'POST': + conf_type = request.form.get('config_tab') + result = None + + if conf_type == 'general': + local_db_enabled = True if request.form.get('local_db_enabled') else False + signup_enabled = True if request.form.get('signup_enabled', ) else False + + if not local_db_enabled and not Setting().get('ldap_enabled'): + result = {'status': False, 'msg': 'Local DB and LDAP Authentication can not be disabled at the same time.'} + else: + Setting().set('local_db_enabled', local_db_enabled) + Setting().set('signup_enabled', signup_enabled) + result = {'status': True, 'msg': 'Saved successfully'} + elif conf_type == 'ldap': + ldap_enabled = True if request.form.get('ldap_enabled') else False + + if not ldap_enabled and not Setting().get('local_db_enabled'): + result = {'status': False, 'msg': 'Local DB and LDAP Authentication can not be disabled at the same time.'} + else: + Setting().set('ldap_enabled', ldap_enabled) + Setting().set('ldap_type', request.form.get('ldap_type')) + Setting().set('ldap_uri', request.form.get('ldap_uri')) + Setting().set('ldap_base_dn', request.form.get('ldap_base_dn')) + Setting().set('ldap_admin_username', request.form.get('ldap_admin_username')) + Setting().set('ldap_admin_password', request.form.get('ldap_admin_password')) + Setting().set('ldap_filter_basic', request.form.get('ldap_filter_basic')) + Setting().set('ldap_filter_username', request.form.get('ldap_filter_username')) + Setting().set('ldap_sg_enabled', True if request.form.get('ldap_sg_enabled')=='ON' else False) + Setting().set('ldap_admin_group', request.form.get('ldap_admin_group')) + Setting().set('ldap_user_group', request.form.get('ldap_user_group')) + result = {'status': True, 'msg': 'Saved successfully'} + elif conf_type == 'google': + Setting().set('google_oauth_enabled', True if request.form.get('google_oauth_enabled') else False) + Setting().set('google_oauth_client_id', request.form.get('google_oauth_client_id')) + Setting().set('google_oauth_client_secret', request.form.get('google_oauth_client_secret')) + Setting().set('google_token_url', request.form.get('google_token_url')) + Setting().set('google_token_params', request.form.get('google_token_params')) + Setting().set('google_authorize_url', request.form.get('google_authorize_url')) + Setting().set('google_base_url', request.form.get('google_base_url')) + result = {'status': True, 'msg': 'Saved successfully. Please reload PDA to take effect.'} + elif conf_type == 'github': + Setting().set('github_oauth_enabled', True if request.form.get('github_oauth_enabled') else False) + Setting().set('github_oauth_key', request.form.get('github_oauth_key')) + Setting().set('github_oauth_secret', request.form.get('github_oauth_secret')) + Setting().set('github_oauth_scope', request.form.get('github_oauth_scope')) + Setting().set('github_oauth_api_url', request.form.get('github_oauth_api_url')) + Setting().set('github_oauth_token_url', request.form.get('github_oauth_token_url')) + Setting().set('github_oauth_authorize_url', request.form.get('github_oauth_authorize_url')) + result = {'status': True, 'msg': 'Saved successfully. Please reload PDA to take effect.'} + else: + return abort(400) + + return render_template('admin_setting_authentication.html', result=result) + + @app.route('/user/profile', methods=['GET', 'POST']) @login_required def user_profile(): - external_account = False - 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) + if request.method == 'GET': + return render_template('user_profile.html') if request.method == 'POST': - # get new profile info - firstname = request.form['firstname'] if 'firstname' in request.form else '' - lastname = request.form['lastname'] if 'lastname' in request.form else '' - email = request.form['email'] if 'email' in request.form else '' - new_password = request.form['password'] if 'password' in request.form else '' + if session['authentication_type'] == 'LOCAL': + firstname = request.form['firstname'] if 'firstname' in request.form else '' + lastname = request.form['lastname'] if 'lastname' in request.form else '' + email = request.form['email'] if 'email' in request.form else '' + new_password = request.form['password'] if 'password' in request.form else '' + else: + firstname = lastname = email = new_password = '' + logging.warning('Authenticated externally. User {0} information will not allowed to update the profile'.format(current_user.username)) - # json data if request.data: jdata = request.json data = jdata['data'] if jdata['action'] == 'enable_otp': - enable_otp = data['enable_otp'] - user = User(username=current_user.username) - user.update_profile(enable_otp=enable_otp) - return make_response(jsonify( { 'status': 'ok', 'msg': 'Change OTP Authentication successfully. Status: {0}'.format(enable_otp) } ), 200) + if session['authentication_type'] in ['LOCAL', 'LDAP']: + enable_otp = data['enable_otp'] + user = User(username=current_user.username) + user.update_profile(enable_otp=enable_otp) + return make_response(jsonify( { 'status': 'ok', 'msg': 'Change OTP Authentication successfully. Status: {0}'.format(enable_otp) } ), 200) + else: + return make_response(jsonify( { 'status': 'error', 'msg': 'User {0} is externally. You are not allowed to update the OTP'.format(current_user.username) } ), 400) # get new avatar save_file_name = None if 'file' in request.files: - file = request.files['file'] - if file: - filename = secure_filename(file.filename) - file_extension = filename.rsplit('.', 1)[1] + if session['authentication_type'] in ['LOCAL', 'LDAP']: + file = request.files['file'] + if file: + filename = secure_filename(file.filename) + file_extension = filename.rsplit('.', 1)[1] - if file_extension.lower() in ['jpg', 'jpeg', 'png']: - save_file_name = current_user.username + '.' + file_extension - file.save(os.path.join(app.config['UPLOAD_DIR'], 'avatar', save_file_name)) + if file_extension.lower() in ['jpg', 'jpeg', 'png']: + save_file_name = current_user.username + '.' + file_extension + file.save(os.path.join(app.config['UPLOAD_DIR'], 'avatar', save_file_name)) + else: + logging.error('Authenticated externally. User {0} is not allowed to update the avatar') + abort(400) - # update user profile user = User(username=current_user.username, plain_text_password=new_password, firstname=firstname, lastname=lastname, email=email, avatar=save_file_name, reload_info=False) user.update_profile() - return render_template('user_profile.html', external_account=external_account) + return render_template('user_profile.html') @app.route('/user/avatar/') diff --git a/config_template.py b/config_template.py index 51e7c87..1a1c1f8 100644 --- a/config_template.py +++ b/config_template.py @@ -6,84 +6,30 @@ WTF_CSRF_ENABLED = True SECRET_KEY = 'We are the world' BIND_ADDRESS = '127.0.0.1' PORT = 9191 -LOGIN_TITLE = "PDNS" # TIMEOUT - for large zones TIMEOUT = 10 # LOG CONFIG +# - For docker, LOG_FILE='' LOG_LEVEL = 'DEBUG' LOG_FILE = 'logfile.log' -# For Docker, leave empty string -#LOG_FILE = '' -# Upload +# UPLOAD DIRECTORY UPLOAD_DIR = os.path.join(basedir, 'upload') # DATABASE CONFIG -#You'll need MySQL-python -SQLA_DB_USER = 'powerdnsadmin' -SQLA_DB_PASSWORD = 'powerdnsadminpassword' -SQLA_DB_HOST = 'mysqlhostorip' -SQLA_DB_NAME = 'powerdnsadmin' - -#MySQL -#SQLALCHEMY_DATABASE_URI = 'mysql://'+SQLA_DB_USER+':'\ -# +SQLA_DB_PASSWORD+'@'+SQLA_DB_HOST+'/'+SQLA_DB_NAME -#SQLite -SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db') -SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository') +SQLA_DB_USER = 'pda' +SQLA_DB_PASSWORD = 'changeme' +SQLA_DB_HOST = '127.0.0.1' +SQLA_DB_NAME = 'pda' SQLALCHEMY_TRACK_MODIFICATIONS = True -# LDAP CONFIG -LDAP_ENABLED = False -LDAP_TYPE = 'ldap' -LDAP_URI = 'ldaps://your-ldap-server:636' -LDAP_ADMIN_USERNAME = 'cn=admin,dc=mydomain,dc=com' -LDAP_ADMIN_PASSWORD = 'password' -LDAP_SEARCH_BASE = 'dc=mydomain,dc=com' +# DATBASE - MySQL +SQLALCHEMY_DATABASE_URI = 'mysql://'+SQLA_DB_USER+':'+SQLA_DB_PASSWORD+'@'+SQLA_DB_HOST+'/'+SQLA_DB_NAME -# Additional options only if LDAP_TYPE=ldap -LDAP_USERNAMEFIELD = 'uid' -LDAP_FILTER = '(objectClass=inetorgperson)' - -# enable LDAP_GROUP_SECURITY to allow Admin and User roles based on LDAP groups -LDAP_GROUP_SECURITY = False # True or False -LDAP_ADMIN_GROUP = 'cn=sysops,dc=mydomain,dc=com' -LDAP_USER_GROUP = 'cn=user,dc=mydomain,dc=com' - -## AD CONFIG -#LDAP_TYPE = 'ad' -#LDAP_URI = 'ldaps://your-ad-server:636' -#LDAP_USERNAME = 'cn=dnsuser,ou=Users,dc=domain,dc=local' -#LDAP_PASSWORD = 'dnsuser' -#LDAP_SEARCH_BASE = 'dc=domain,dc=local' -## You may prefer 'userPrincipalName' instead -#LDAP_USERNAMEFIELD = 'sAMAccountName' -## AD Group that you would like to have accesss to web app -#LDAP_FILTER = 'memberof=cn=DNS_users,ou=Groups,dc=domain,dc=local' - -# Github Oauth -GITHUB_OAUTH_ENABLE = False -GITHUB_OAUTH_KEY = '' -GITHUB_OAUTH_SECRET = '' -GITHUB_OAUTH_SCOPE = 'email' -GITHUB_OAUTH_URL = 'http://127.0.0.1:9191/api/v3/' -GITHUB_OAUTH_TOKEN = 'http://127.0.0.1:9191/oauth/token' -GITHUB_OAUTH_AUTHORIZE = 'http://127.0.0.1:9191/oauth/authorize' - - -# Google OAuth -GOOGLE_OAUTH_ENABLE = False -GOOGLE_OAUTH_CLIENT_ID = ' ' -GOOGLE_OAUTH_CLIENT_SECRET = ' ' -GOOGLE_REDIRECT_URI = '/user/authorized' -GOOGLE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token' -GOOGLE_TOKEN_PARAMS = { - 'scope': 'email profile' -} -GOOGLE_AUTHORIZE_URL='https://accounts.google.com/o/oauth2/auth' -GOOGLE_BASE_URL='https://www.googleapis.com/oauth2/v1/' +# DATABSE - SQLite +# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db') # SAML Authnetication SAML_ENABLED = False @@ -157,26 +103,3 @@ SAML_LOGOUT = False #Configure to redirect to a different url then PowerDNS-Admin login after SAML logout #for example redirect to google.com after successful saml logout #SAML_LOGOUT_URL = 'https://google.com' - -#Default Auth -BASIC_ENABLED = True -SIGNUP_ENABLED = True - -# POWERDNS CONFIG -PDNS_STATS_URL = 'http://172.16.214.131:8081/' -PDNS_API_KEY = 'you never know' -PDNS_VERSION = '4.1.1' - -# RECORDS ALLOWED TO EDIT -RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'PTR', 'SPF', 'SRV', 'TXT', 'LOC', 'NS', 'PTR', 'SOA'] -FORWARD_RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'PTR', 'SPF', 'SRV', 'TXT', 'LOC' 'NS'] -REVERSE_RECORDS_ALLOW_EDIT = ['SOA', 'TXT', 'LOC', 'NS', 'PTR'] - -# ALLOW DNSSEC CHANGES FOR ADMINS ONLY -DNSSEC_ADMINS_ONLY = False - -# EXPERIMENTAL FEATURES -PRETTY_IPV6_PTR = False - -# Domain updates in background, for big installations -BG_DOMAIN_UPDATES = False diff --git a/migrations/versions/1274ed462010_change_setting_value_data_type.py b/migrations/versions/1274ed462010_change_setting_value_data_type.py new file mode 100644 index 0000000..91600ca --- /dev/null +++ b/migrations/versions/1274ed462010_change_setting_value_data_type.py @@ -0,0 +1,46 @@ +"""Change setting.value data type + +Revision ID: 1274ed462010 +Revises: 59729e468045 +Create Date: 2018-08-21 17:12:30.058782 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1274ed462010' +down_revision = '59729e468045' +branch_labels = None +depends_on = None + + +def update_data(): + setting_table = sa.sql.table('setting', + sa.sql.column('id', sa.Integer), + sa.sql.column('name', sa.String), + sa.sql.column('value', sa.String), + sa.sql.column('view', sa.String) + ) + + # add more new settings + op.bulk_insert(setting_table, + [ + {'id': 42, 'name': 'forward_records_allow_edit', 'value': "{'A': True, 'AAAA': True, 'AFSDB': False, 'ALIAS': False, 'CAA': True, 'CERT': False, 'CDNSKEY': False, 'CDS': False, 'CNAME': True, 'DNSKEY': False, 'DNAME': False, 'DS': False, 'HINFO': False, 'KEY': False, 'LOC': True, 'MX': True, 'NAPTR': False, 'NS': True, 'NSEC': False, 'NSEC3': False, 'NSEC3PARAM': False, 'OPENPGPKEY': False, 'PTR': True, 'RP': False, 'RRSIG': False, 'SOA': False, 'SPF': True, 'SSHFP': False, 'SRV': True, 'TKEY': False, 'TSIG': False, 'TLSA': False, 'SMIMEA': False, 'TXT': True, 'URI': False}", 'view': 'records'}, + {'id': 43, 'name': 'reverse_records_allow_edit', 'value': "{'A': False, 'AAAA': False, 'AFSDB': False, 'ALIAS': False, 'CAA': False, 'CERT': False, 'CDNSKEY': False, 'CDS': False, 'CNAME': False, 'DNSKEY': False, 'DNAME': False, 'DS': False, 'HINFO': False, 'KEY': False, 'LOC': True, 'MX': False, 'NAPTR': False, 'NS': True, 'NSEC': False, 'NSEC3': False, 'NSEC3PARAM': False, 'OPENPGPKEY': False, 'PTR': True, 'RP': False, 'RRSIG': False, 'SOA': False, 'SPF': False, 'SSHFP': False, 'SRV': False, 'TKEY': False, 'TSIG': False, 'TLSA': False, 'SMIMEA': False, 'TXT': True, 'URI': False}", 'view': 'records'}, + ] + ) + +def upgrade(): + # change column data type + op.alter_column('setting', 'value', existing_type=sa.String(256), type_=sa.Text()) + # update data for new schema + update_data() + + +def downgrade(): + # delete added records in previous version + op.execute("DELETE FROM setting WHERE id > 41") + # change column data type + op.alter_column('setting', 'value', existing_type=sa.Text(), type_=sa.String(256)) diff --git a/migrations/versions/59729e468045_add_view_column_to_setting_table.py b/migrations/versions/59729e468045_add_view_column_to_setting_table.py new file mode 100644 index 0000000..1d31319 --- /dev/null +++ b/migrations/versions/59729e468045_add_view_column_to_setting_table.py @@ -0,0 +1,92 @@ +"""Add view column to setting table + +Revision ID: 59729e468045 +Revises: 787bdba9e147 +Create Date: 2018-08-17 16:17:31.058782 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '59729e468045' +down_revision = '787bdba9e147' +branch_labels = None +depends_on = None + + +def update_data(): + setting_table = sa.sql.table('setting', + sa.sql.column('id', sa.Integer), + sa.sql.column('name', sa.String), + sa.sql.column('value', sa.String), + sa.sql.column('view', sa.String) + ) + + # just update previous records which have id <= 7 + op.execute( + setting_table.update().where(setting_table.c.id <= 7).values({'view': 'basic'}) + ) + + # add more new settings + op.bulk_insert(setting_table, + [ + {'id': 8, 'name': 'pretty_ipv6_ptr', 'value': 'False', 'view': 'basic'}, + {'id': 9, 'name': 'dnssec_admins_only', 'value': 'False', 'view': 'basic'}, + {'id': 10, 'name': 'bg_domain_updates', 'value': 'False', 'view': 'basic'}, + {'id': 11, 'name': 'site_name', 'value': 'PowerDNS-Admin', 'view': 'basic'}, + {'id': 12, 'name': 'pdns_api_url', 'value': '', 'view': 'pdns'}, + {'id': 13, 'name': 'pdns_api_key', 'value': '', 'view': 'pdns'}, + {'id': 14, 'name': 'pdns_version', 'value': '4.1.1', 'view': 'pdns'}, + {'id': 15, 'name': 'local_db_enabled', 'value': 'True', 'view': 'authentication'}, + {'id': 16, 'name': 'signup_enabled', 'value': 'True', 'view': 'authentication'}, + {'id': 17, 'name': 'ldap_enabled', 'value': 'False', 'view': 'authentication'}, + {'id': 18, 'name': 'ldap_type', 'value': 'ldap', 'view': 'authentication'}, + {'id': 19, 'name': 'ldap_uri', 'value': '', 'view': 'authentication'}, + {'id': 20, 'name': 'ldap_base_dn', 'value': '', 'view': 'authentication'}, + {'id': 21, 'name': 'ldap_admin_username', 'value': '', 'view': 'authentication'}, + {'id': 22, 'name': 'ldap_admin_password', 'value': '', 'view': 'authentication'}, + {'id': 23, 'name': 'ldap_filter_basic', 'value': '', 'view': 'authentication'}, + {'id': 24, 'name': 'ldap_filter_username', 'value': '', 'view': 'authentication'}, + {'id': 25, 'name': 'ldap_sg_enabled', 'value': 'False', 'view': 'authentication'}, + {'id': 26, 'name': 'ldap_admin_group', 'value': '', 'view': 'authentication'}, + {'id': 27, 'name': 'ldap_user_group', 'value': '', 'view': 'authentication'}, + {'id': 28, 'name': 'github_oauth_enabled', 'value': 'False', 'view': 'authentication'}, + {'id': 29, 'name': 'github_oauth_key', 'value': '', 'view': 'authentication'}, + {'id': 30, 'name': 'github_oauth_secret', 'value': '', 'view': 'authentication'}, + {'id': 31, 'name': 'github_oauth_scope', 'value': 'email', 'view': 'authentication'}, + {'id': 32, 'name': 'github_oauth_api_url', 'value': 'https://api.github.com/user', 'view': 'authentication'}, + {'id': 33, 'name': 'github_oauth_token_url', 'value': 'https://github.com/login/oauth/access_token', 'view': 'authentication'}, + {'id': 34, 'name': 'github_oauth_authorize_url', 'value': 'https://github.com/login/oauth/authorize', 'view': 'authentication'}, + {'id': 35, 'name': 'google_oauth_enabled', 'value': 'False', 'view': 'authentication'}, + {'id': 36, 'name': 'google_oauth_client_id', 'value': '', 'view': 'authentication'}, + {'id': 37, 'name': 'google_oauth_client_secret', 'value': '', 'view': 'authentication'}, + {'id': 38, 'name': 'google_token_url', 'value': 'https://accounts.google.com/o/oauth2/token', 'view': 'authentication'}, + {'id': 39, 'name': 'google_token_params', 'value': "{'scope': 'email profile'}", 'view': 'authentication'}, + {'id': 40, 'name': 'google_authorize_url', 'value': 'https://accounts.google.com/o/oauth2/auth', 'view': 'authentication'}, + {'id': 41, 'name': 'google_base_url', 'value': 'https://www.googleapis.com/oauth2/v1/', 'view': 'authentication'}, + ] + ) + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('setting', sa.Column('view', sa.String(length=64), nullable=True)) + # ### end Alembic commands ### + + # update data for new schema + update_data() + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + ## NOTE: + ## - Drop action does not work on sqlite3 + ## - This action touchs the `setting` table which loaded in views.py + ## during app initlization, so the downgrade function won't work + ## unless we temporary remove importing `views` from `app/__init__.py` + op.drop_column('setting', 'view') + + # delete added records in previous version + op.execute("DELETE FROM setting WHERE id > 7") + # ### end Alembic commands ### diff --git a/package.json b/package.json index 6b8d78c..939b50a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "dependencies": { "admin-lte": "2.4.3", + "bootstrap-validator": "^0.11.9", "icheck": "^1.0.2", "jquery-slimscroll": "^1.3.8", "jquery-ui-dist": "^1.12.1", diff --git a/yarn.lock b/yarn.lock index 400bd76..b7ed3b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -132,6 +132,10 @@ bootstrap-timepicker@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/bootstrap-timepicker/-/bootstrap-timepicker-0.5.2.tgz#10ed9f2a2f0b8ccaefcde0fcf6a0738b919a3835" +bootstrap-validator@^0.11.9: + version "0.11.9" + resolved "https://registry.yarnpkg.com/bootstrap-validator/-/bootstrap-validator-0.11.9.tgz#fb7058eef53623e78f5aa7967026f98f875a9404" + bootstrap@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.3.7.tgz#5a389394549f23330875a3b150656574f8a9eb71"