Merge pull request #339 from ngoduykhanh/app_config_adjustment

Reading app config from DB and authentication adjustment
This commit is contained in:
Khanh Ngo 2018-08-25 14:57:27 +07:00 committed by GitHub
commit 601859c952
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1643 additions and 712 deletions

View File

@ -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
- export FLASK_APP=app/__init__.py
- flask db upgrade
- yarn install --pure-lockfile
- flask assets build
script:
- sh run_travis.sh
cache:
yarn: true
services:
- mysql

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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,27 +180,31 @@ 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 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))
@ -236,6 +213,21 @@ class User(db.Model):
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('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))
logging.debug(traceback.format_exc())
@ -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
# 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 '<Domain {0}>'.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"

78
app/oauth.py Normal file
View File

@ -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

View File

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - Admin Console</title>{% endblock %}
{% set active_page = "admin_console" %}
{% block title %}<title>Admin Console - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->

View File

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - Edit Account</title>{% endblock %}
{% set active_page = "admin_accounts" %}
{% block title %}<title>Edit Account - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->

View File

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - Edit User</title>{% endblock %}
{% set active_page = "admin_users" %}
{% block title %}<title>Edit Use - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->

View File

@ -1,5 +1,7 @@
{% extends "base.html" %} {% block title %}
<title>DNS Control Panel - History</title>
{% extends "base.html" %}
{% set active_page = "admin_history" %}
{% block title %}
<title>History - {{ SITE_NAME }}</title>
{% endblock %} {% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">

View File

@ -1,5 +1,7 @@
{% extends "base.html" %} {% block title %}
<title>DNS Control Panel - Account Management</title>
{% extends "base.html" %}
{% set active_page = "admin_accounts" %}
{% block title %}
<title>Account Management - {{ SITE_NAME }}</title>
{% endblock %} {% block dashboard_stat %}
<section class="content-header">
<h1>

View File

@ -1,5 +1,7 @@
{% extends "base.html" %} {% block title %}
<title>DNS Control Panel - User Management</title>
{% extends "base.html" %}
{% set active_page = "admin_users" %}
{% block title %}
<title>User Management - {{ SITE_NAME }}</title>
{% endblock %} {% block dashboard_stat %}
<section class="content-header">
<h1>

View File

@ -0,0 +1,495 @@
{% extends "base.html" %}
{% set active_page = "admin_settings" %}
{% block title %}
<title>Authentication Settings - {{ SITE_NAME }}</title>
{% endblock %} {% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
Settings <small>PowerDNS-Admin settings</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li><a href="#">Setting</a></li>
<li class="active">Authentication</li>
</ol>
</section>
{% endblock %}
{% block content %}
<section class="content">
<div class="row">
<div class="col-lg-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Authentication Settings</h3>
</div>
<div class="box-body">
{% if result %}
<div class="alert {% if result['status'] %}alert-success{% else %}alert-danger{% endif%} alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{{ result['msg'] }}
</div>
{% endif %}
<!-- Custom Tabs -->
<div class="nav-tabs-custom" id="tabs">
<ul class="nav nav-tabs">
<li class="active"><a href="#tabs-general" data-toggle="tab">General</a></li>
<li class="active"><a href="#tabs-ldap" data-toggle="tab">LDAP</a></li>
<li><a href="#tabs-google" data-toggle="tab">Google OAuth</a></li>
<li><a href="#tabs-github" data-toggle="tab">Github OAuth</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="tabs-general">
<form role="form" method="post">
<input type="hidden" value="general" name="config_tab" />
<div class="form-group">
<input type="checkbox" id="local_db_enabled" name="local_db_enabled" class="checkbox" {% if SETTING.get('local_db_enabled') %}checked{% endif %}>
<label for="local_db_enabled">Local DB Authentication</label>
</div>
<div class="form-group">
<input type="checkbox" id="signup_enabled" name="signup_enabled" class="checkbox" {% if SETTING.get('signup_enabled') %}checked{% endif %}>
<label for="signup_enabled">Allow users to sign up</label>
</div>
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Save</button>
</div>
</form>
</div>
<div class="tab-pane active" id="tabs-ldap">
<div class="row">
<div class="col-md-4">
<form role="form" method="post" data-toggle="validator">
<input type="hidden" value="ldap" name="config_tab" />
<fieldset>
<legend>GENERAL</legend>
<div class="form-group">
<input type="checkbox" id="ldap_enabled" name="ldap_enabled" class="checkbox" {% if SETTING.get('ldap_enabled') %}checked{% endif %}>
<label for="ldap_enabled">Enable LDAP Authentication</label>
</div>
<div class="form-group">
<label>Type</label>
<div class="radio">
<label>
<input type="radio" name="ldap_type" id="ldap" value="ldap" {% if SETTING.get('ldap_type')=='ldap' %}checked{% endif %}> OpenLDAP
</label>
&nbsp;&nbsp;&nbsp;
<label>
<input type="radio" name="ldap_type" id="ad" value="ad" {% if SETTING.get('ldap_type')=='ad' %}checked{% endif %}> Active Directory
</label>
</div>
</div>
</fieldset>
<fieldset>
<legend>ADMINISTRATOR INFO</legend>
<div class="form-group">
<label for="ldap_uri">LDAP URI</label>
<input type="text" class="form-control" name="ldap_uri" id="ldap_uri" placeholder="e.g. ldaps://your-ldap-server:636" data-error="Please input LDAP URI" value="{{ SETTING.get('ldap_uri') }}">
</div>
<div class="form-group">
<label for="ldap_base_dn">LDAP Base DN</label>
<input type="text" class="form-control" name="ldap_base_dn" id="ldap_base_dn" placeholder="e.g. dc=mydomain,dc=com" data-error="Please input LDAP Base DN" value="{{ SETTING.get('ldap_base_dn') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="ldap_admin_username">LDAP admin username</label>
<input type="text" class="form-control" name="ldap_admin_username" id="ldap_admin_username" placeholder="e.g. cn=admin,dc=mydomain,dc=com" data-error="Please input LDAP admin username" value="{{ SETTING.get('ldap_admin_username') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="ldap_admin_password">LDAP admin password</label>
<input type="password" class="form-control" name="ldap_admin_password" id="ldap_admin_password" placeholder="LDAP Admin password" data-error="Please input LDAP admin password" value="{{ SETTING.get('ldap_admin_password') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<fieldset>
<legend>FILTERS</legend>
<div class="form-group">
<label for="ldap_filter_basic">Basic filter</label>
<input type="text" class="form-control" name="ldap_filter_basic" id="ldap_filter_basic" placeholder="e.g. (objectClass=inetorgperson)" data-error="Please input LDAP filter" value="{{ SETTING.get('ldap_filter_basic') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="ldap_filter_username">Username field</label>
<input type="text" class="form-control" name="ldap_filter_username" id="ldap_filter_username" placeholder="e.g. uid" data-error="Please input field for username filtering" value="{{ SETTING.get('ldap_filter_username') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<fieldset>
<legend>GROUP SECURITY</legend>
<div class="form-group">
<label>Status</label>
<div class="radio">
<label>
<input type="radio" name="ldap_sg_enabled" id="ldap_sg_off" value="OFF" {% if not SETTING.get('ldap_sg_enabled') %}checked{% endif %}> OFF
</label>
&nbsp;&nbsp;&nbsp;
<label>
<input type="radio" name="ldap_sg_enabled" id="ldap_sg_on" value="ON" {% if SETTING.get('ldap_sg_enabled') %}checked{% endif %}> ON
</label>
</div>
</div>
<div class="form-group">
<label for="ldap_admin_group">Admin group</label>
<input type="text" class="form-control" name="ldap_admin_group" id="ldap_admin_group" placeholder="e.g. cn=sysops,dc=mydomain,dc=com" data-error="Please input LDAP DN for Admin group" value="{{ SETTING.get('ldap_admin_group') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="ldap_user_group">User group</label>
<input type="text" class="form-control" name="ldap_user_group" id="ldap_user_group" placeholder="e.g. cn=users,dc=mydomain,dc=com" data-error="Please input LDAP DN for User group" value="{{ SETTING.get('ldap_user_group') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Save</button>
</div>
</form>
</div>
<div class="col-md-8">
<legend>Help</legend>
<dl class="dl-horizontal">
<dt>Enable LDAP Authentication</dt>
<dd>Turn on / off the LDAP authentication.</dd>
<dt>Type</dt>
<dd>Select your current directory service type.
<ul>
<li>
OpenLDAP - Open source implementation of the Lightweight Directory Access Protocol.
</li>
<li>
Active Directory - Active Directory is a directory service that Microsoft developed for the Windows domain networks.
</li>
</ul>
</dd>
<dt>ADMINISTRATOR INFO</dt>
<dd>Your LDAP connection string and admin credential used by PDA to query user information.
<ul>
<li>
LDAP URI - The fully qualified domain names of your directory servers. (e.g. ldap://127.0.0.1:389)
</li>
<li>
LDAP Base DN - The point from where a PDA will search for users.
</li>
<li>
LDAP admin username - Your LDAP administrator user which has permission to query information in the Base DN above.
</li>
<li>
LDAP admin password - The password of LDAP administrator user.
</li>
</ul>
</dd>
<dt>FILTERS</dt>
<dd>Define how you want to filter your user in LDAP query.
<ul>
<li>
Basic filter - The filter that will be applied to all LDAP query by PDA. (e.g. <i>(objectClass=inetorgperson)</i> for OpenLDAP and <i>(objectClass=organizationalPerson)</i> for Active Directory)
</li>
<li>
Username field - The field PDA will look for user's username. (e.g. <i>uid</i> for OpenLDAP and <i>sAMAccountName</i> or <i>userPrincipalName</i> for Active Directory)
</li>
</ul>
</dd>
<dt>GROUP SECURITY</dt>
<dd>User can be assigned to PDA's User or Admin group by matching following LDAP Group.
<ul>
<li>
Status - Turn on / off group security feature.
</li>
<li>
Admin group - Your LDAP admin group.
</li>
<li>
User group - Your LDAP user group.
</li>
</ul>
</dd>
</dl>
</div>
</div>
</div>
<div class="tab-pane" id="tabs-google">
<div class="row">
<div class="col-md-4">
<form role="form" method="post" data-toggle="validator">
<input type="hidden" value="google" name="config_tab" />
<fieldset>
<legend>GENERAL</legend>
<div class="form-group">
<input type="checkbox" id="google_oauth_enabled" name="google_oauth_enabled" class="checkbox" {% if SETTING.get('google_oauth_enabled') %}checked{% endif %}>
<label for="google_oauth_enabled">Enable Google OAuth</label>
</div>
<div class="form-group">
<label for="google_oauth_client_id">Client ID</label>
<input type="text" class="form-control" name="google_oauth_client_id" id="google_oauth_client_id" placeholder="Google OAuth client ID" data-error="Please input Client ID" value="{{ SETTING.get('google_oauth_client_id') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="google_oauth_client_secret">Client secret</label>
<input type="text" class="form-control" name="google_oauth_client_secret" id="google_oauth_client_secret" placeholder="Google OAuth client secret" data-error="Please input Client secret" value="{{ SETTING.get('google_oauth_client_secret') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<fieldset>
<legend>ADVANCE</legend>
<div class="form-group">
<label for="google_token_url">Token URL</label>
<input type="text" class="form-control" name="google_token_url" id="google_token_url" placeholder="e.g. https://accounts.google.com/o/oauth2/token" data-error="Please input token URL" value="{{ SETTING.get('google_token_url') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="google_token_params">Token params</label>
<input type="text" class="form-control" name="google_token_params" id="google_token_params" placeholder="e.g. {'scope': 'email profile'}" data-error="Please input token params" value="{{ SETTING.get('google_token_params') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="google_authorize_url">Authorize URL</label>
<input type="text" class="form-control" name="google_authorize_url" id="google_authorize_url" placeholder="e.g. https://accounts.google.com/o/oauth2/auth" data-error="Please input Authorize URL" value="{{ SETTING.get('google_authorize_url') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="google_base_url">Base URL</label>
<input type="text" class="form-control" name="google_base_url" id="google_base_url" placeholder="e.g. https://www.googleapis.com/oauth2/v1/" data-error="Please input base URL" value="{{ SETTING.get('google_base_url') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Save</button>
</div>
</form>
</div>
<div class="col-md-8">
<legend>Help</legend>
<p>Fill in all the fields in the left form.</p>
<p>Make sure you add PDA redirection URI (e.g http://localhost:9191/google/authorized) to your Google App Credentials Restriction.</p>
</div>
</div>
</div>
<div class="tab-pane" id="tabs-github">
<div class="row">
<div class="col-md-4">
<form role="form" method="post" data-toggle="validator">
<input type="hidden" value="github" name="config_tab" />
<fieldset>
<legend>GENERAL</legend>
<div class="form-group">
<input type="checkbox" id="github_oauth_enabled" name="github_oauth_enabled" class="checkbox" {% if SETTING.get('github_oauth_enabled') %}checked{% endif %}>
<label for="github_oauth_enabled">Enable Github OAuth</label>
</div>
<div class="form-group">
<label for="github_oauth_key">Client key</label>
<input type="text" class="form-control" name="github_oauth_key" id="github_oauth_key" placeholder="Google OAuth client ID" data-error="Please input Client key" value="{{ SETTING.get('github_oauth_key') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="github_oauth_secret">Client secret</label>
<input type="text" class="form-control" name="github_oauth_secret" id="github_oauth_secret" placeholder="Google OAuth client secret" data-error="Please input Client secret" value="{{ SETTING.get('github_oauth_secret') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<fieldset>
<legend>ADVANCE</legend>
<div class="form-group">
<label for="github_oauth_scope">Scope</label>
<input type="text" class="form-control" name="github_oauth_scope" id="github_oauth_scope" placeholder="e.g. email" data-error="Please input scope" value="{{ SETTING.get('github_oauth_scope') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="github_oauth_api_url">API URL</label>
<input type="text" class="form-control" name="github_oauth_api_url" id="github_oauth_api_url" placeholder="e.g. https://api.github.com/user" data-error="Please input API URL" value="{{ SETTING.get('github_oauth_api_url') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="github_oauth_token_url">Token URL</label>
<input type="text" class="form-control" name="github_oauth_token_url" id="github_oauth_token_url" placeholder="e.g. https://github.com/login/oauth/access_token" data-error="Please input Token URL" value="{{ SETTING.get('github_oauth_token_url') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="github_oauth_authorize_url">Authorize URL</label>
<input type="text" class="form-control" name="github_oauth_authorize_url" id="github_oauth_authorize_url" placeholder="e.g. https://github.com/login/oauth/authorize" data-error="Plesae input Authorize URL" value="{{ SETTING.get('github_oauth_authorize_url') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Save</button>
</div>
</form>
</div>
<div class="col-md-8">
<legend>Help</legend>
<p>Fill in all the fields in the left form.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extrascripts %}
{% assets "js_validation" -%}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{%- endassets %}
<script>
$(function() {
$('#tabs').tabs({
// add url anchor tags
activate: function(event, ui) {
window.location.hash = ui.newPanel.attr('id');
}
});
// re-set active tab (ui)
var activeTabIdx = $('#tabs').tabs('option','active');
$('#tabs li:eq('+activeTabIdx+')').tab('show')
});
// START: General tab js
$('#local_db_enabled').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
})
$('#signup_enabled').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
})
// END: General tab js
// START: LDAP tab js
// update validation requirement when checkbox is togged
$('#ldap_enabled').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
}).on('ifChanged', function(e) {
var is_enabled = e.currentTarget.checked;
if (is_enabled){
$('#ldap_uri').prop('required', true);
$('#ldap_base_dn').prop('required', true);
$('#ldap_admin_username').prop('required', true);
$('#ldap_admin_password').prop('required', true);
$('#ldap_filter_basic').prop('required', true);
$('#ldap_filter_username').prop('required', true);
if ($('#ldap_sg_on').is(":checked")) {
$('#ldap_admin_group').prop('required', true);
$('#ldap_user_group').prop('required', true);
}
} else {
$('#ldap_uri').prop('required', false);
$('#ldap_base_dn').prop('required', false);
$('#ldap_admin_username').prop('required', false);
$('#ldap_admin_password').prop('required', false);
$('#ldap_filter_basic').prop('required', false);
$('#ldap_filter_username').prop('required', false);
if ($('#ldap_sg_on').is(":checked")) {
$('#ldap_admin_group').prop('required', false);
$('#ldap_user_group').prop('required', false);
}
}
});
$("input[name='ldap_sg_enabled']" ).change(function(){
if ($('#ldap_sg_on').is(":checked") && $('#ldap_enabled').is(":checked")) {
$('#ldap_admin_group').prop('required', true);
$('#ldap_user_group').prop('required', true);
} else {
$('#ldap_admin_group').prop('required', false);
$('#ldap_user_group').prop('required', false);
}
});
// init validation reqirement at first time page load
{% if SETTING.get('ldap_enabled') %}
$('#ldap_uri').prop('required', true);
$('#ldap_base_dn').prop('required', true);
$('#ldap_admin_username').prop('required', true);
$('#ldap_admin_password').prop('required', true);
$('#ldap_filter_basic').prop('required', true);
$('#ldap_filter_username').prop('required', true);
if ($('#ldap_sg_on').is(":checked")) {
$('#ldap_admin_group').prop('required', true);
$('#ldap_user_group').prop('required', true);
}
{% endif %}
// END: LDAP tab js
// START: Google tab js
// update validation requirement when checkbox is togged
$('#google_oauth_enabled').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
}).on('ifChanged', function(e) {
var is_enabled = e.currentTarget.checked;
if (is_enabled){
$('#google_oauth_client_id').prop('required', true);
$('#google_oauth_client_secret').prop('required', true);
$('#google_token_url').prop('required', true);
$('#google_token_params').prop('required', true);
$('#google_authorize_url').prop('required', true);
$('#google_base_url').prop('required', true);
} else {
$('#google_oauth_client_id').prop('required', false);
$('#google_oauth_client_secret').prop('required', false);
$('#google_token_url').prop('required', false);
$('#google_token_params').prop('required', false);
$('#google_authorize_url').prop('required', false);
$('#google_base_url').prop('required', false);
}
});
// init validation reqirement at first time page load
{% if SETTING.get('google_oauth_enabled') %}
$('#google_oauth_client_id').prop('required', true);
$('#google_oauth_client_secret').prop('required', true);
$('#google_token_url').prop('required', true);
$('#google_token_params').prop('required', true);
$('#google_authorize_url').prop('required', true);
$('#google_base_url').prop('required', true);
{% endif %}
// END: Google tab js
// START: Github tab js
// update validation requirement when checkbox is togged
$('#github_oauth_enabled').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
}).on('ifChanged', function(e) {
var is_enabled = e.currentTarget.checked;
if (is_enabled){
$('#github_oauth_key').prop('required', true);
$('#github_oauth_secret').prop('required', true);
$('#github_oauth_scope').prop('required', true);
$('#github_oauth_api_url').prop('required', true);
$('#github_oauth_token_url').prop('required', true);
$('#github_oauth_authorize_url').prop('required', true);
} else {
$('#github_oauth_key').prop('required', false);
$('#github_oauth_secret').prop('required', false);
$('#github_oauth_scope').prop('required', false);
$('#github_oauth_api_url').prop('required', false);
$('#github_oauth_token_url').prop('required', false);
$('#github_oauth_authorize_url').prop('required', false);
}
});
// init validation reqirement at first time page load
{% if SETTING.get('google_oauth_enabled') %}
$('#github_oauth_key').prop('required', true);
$('#github_oauth_secret').prop('required', true);
$('#github_oauth_scope').prop('required', true);
$('#github_oauth_api_url').prop('required', true);
$('#github_oauth_token_url').prop('required', true);
$('#github_oauth_authorize_url').prop('required', true);
{% endif %}
// END: Github tab js
</script>
{% endblock %}

View File

@ -1,5 +1,7 @@
{% extends "base.html" %} {% block title %}
<title>DNS Control Panel - Settings</title>
{% extends "base.html" %}
{% set active_page = "admin_settings" %}
{% block title %}
<title>Basic Settings - {{ SITE_NAME }}</title>
{% endblock %} {% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
@ -7,9 +9,9 @@
Settings <small>PowerDNS-Admin settings</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i
class="fa fa-dashboard"></i> Home</a></li>
<li class="active">Settings</li>
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li><a href="#">Setting</a></li>
<li class="active">Basic</li>
</ol>
</section>
{% endblock %} {% block content %}
@ -18,7 +20,7 @@
<div class="col-xs-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Settings Management</h3>
<h3 class="box-title">Basic Settings</h3>
</div>
<div class="box-body">
<table id="tbl_settings" class="table table-bordered table-striped">
@ -30,21 +32,21 @@
</tr>
</thead>
<tbody>
{% for setting_name, setting_value in settings.items() %}
{% for setting in settings %}
<tr class="odd ">
<td>{{ setting_name }}</td>
{% if setting_value == "True" or setting_value == "False" %}
<td>{{ setting_value }}</td>
<td>{{ setting.name }}</td>
{% if setting.value == "True" or setting.value == "False" %}
<td>{{ setting.value }}</td>
{% else %}
<td><input name="value" id="value" value="{{ setting_value }}"></td>
<td><input name="value" id="value" value="{{ setting.value }}"></td>
{% endif %}
<td width="6%">
{% if setting_value == "True" or setting_value == "False" %}
<button type="button" class="btn btn-flat btn-warning setting-toggle-button" id="{{ setting_name }}">
{% if setting.value == "True" or setting.value == "False" %}
<button type="button" class="btn btn-flat btn-warning setting-toggle-button" id="{{ setting.name }}">
Toggle&nbsp;<i class="fa fa-info"></i>
</button>
{% else %}
<button type="button" class="btn btn-flat btn-warning setting-save-button" id="{{ setting_name }}">
<button type="button" class="btn btn-flat btn-warning setting-save-button" id="{{ setting.name }}">
Save&nbsp;<i class="fa fa-info"></i>
</button>
{% 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)
});
</script>
{% endblock %}

View File

@ -0,0 +1,85 @@
{% extends "base.html" %}
{% set active_page = "admin_settings" %}
{% block title %}
<title>PDNS Settings - {{ SITE_NAME }}</title>
{% endblock %} {% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
Settings <small>PowerDNS-Admin settings</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li><a href="#">Setting</a></li>
<li class="active">PDNS</li>
</ol>
</section>
{% endblock %}
{% block content %}
<section class="content">
<div class="row">
<div class="col-md-4">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">PDNS Settings</h3>
</div>
<!-- /.box-header -->
<!-- form start -->
<form role="form" method="post" data-toggle="validator">
<div class="box-body">
{% if not SETTING.get('pdns_api_url') or not SETTING.get('pdns_api_key') or not SETTING.get('pdns_version') %}
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<h4><i class="icon fa fa-ban"></i> Error!</h4>
Please complete your PowerDNS API configuration before continuing
</div>
{% endif %}
<div class="form-group has-feedback">
<label class="control-label" for="pdns_api_url">PDNS API URL</label>
<input type="url" class="form-control" placeholder="PowerDNS API url" name="pdns_api_url" data-error="Please input a valid PowerDNS API URL" required value="{{ pdns_api_url }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group has-feedback">
<label class="control-label" for="pdns_api_key">PDNS API KEY</label>
<input type="text" class="form-control" placeholder="PowerDNS API key" name="pdns_api_key" data-error="Please input a valid PowerDNS API key" required value="{{ pdns_api_key }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group has-feedback">
<label class="control-label" for="pdns_version">PDNS VERSION</label>
<input type="text" class="form-control" placeholder="PowerDNS version" name="pdns_version" data-error="Please input PowerDNS version" required value="{{ pdns_version }}">
<span class="help-block with-errors"></span>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-flat btn-primary">Update</button>
</div>
</form>
</div>
</div>
<div class="col-md-8">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Help</h3>
</div>
<div class="box-body">
<dl class="dl-horizontal">
<p>You must configure the API connection information before PowerDNS-Admiin can query your PowerDNS data. Following fields are required:</p>
<dt>PDNS API URL</dt>
<dd>Your PowerDNS API URL (eg. http://127.0.0.1:8081/).</dd>
<dt>PDNS API KEY</dt>
<dd>Your PowerDNS API key.</dd>
<dt>PDNS VERSION</dt>
<dd>Your PowerDNS version number (eg. 4.1.1).</dd>
</dl>
<p>Find more details at <a href="https://doc.powerdns.com/md/httpapi/README/">https://doc.powerdns.com/md/httpapi/README/</a></p>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extrascripts %}
{% assets "js_validation" -%}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{%- endassets %}
{% endblock %}

View File

@ -0,0 +1,78 @@
{% extends "base.html" %}
{% set active_page = "admin_settings" %}
{% block title %}
<title>DNS Records Settings - {{ SITE_NAME }}</title>
{% endblock %} {% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
Settings <small>PowerDNS-Admin settings</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li><a href="#">Setting</a></li>
<li class="active">Records</li>
</ol>
</section>
{% endblock %}
{% block content %}
<section class="content">
<div class="row">
<div class="col-md-5">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">DNS record Settings</h3>
</div>
<!-- /.box-header -->
<!-- form start -->
<form role="form" method="post">
<input type="hidden" name="create" value="{{ create }}">
<div class="box-body">
<table class="table table-bordered">
<tr>
<th style="width: 10px">#</th>
<th style="width: 40px">Record</th>
<th>Forward Zone</th>
<th>Reverse Zone</th>
</tr>
{% for record in f_records %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ record }}</td>
<td>
<input type="checkbox" id="fr_{{ record|lower }}" name="fr_{{ record|lower }}" class="checkbox" {% if f_records[record] %}checked{% endif %}>
</td>
<td>
<input type="checkbox" id="rr_{{ record|lower }}" name="rr_{{ record|lower }}" class="checkbox" {% if r_records[record] %}checked{% endif %}>
</td>
</tr>
{% endfor %}
</table>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-flat btn-primary">Update</button>
</div>
</form>
</div>
</div>
<div class="col-md-7">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Help</h3>
</div>
<div class="box-body">
<p>Select record types you allow user to edit in the forward zone and reverse zone. Take a look at <a href="https://doc.powerdns.com/authoritative/appendices/types.html">PowerDNS docs</a> for full list of supported record types.</p>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extrascripts %}
<script>
$('.checkbox').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
})
</script>
{% endblock %}

View File

@ -4,7 +4,7 @@
{% block head %}
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
{% block title %}<title>DNS Control Panel</title>{% endblock %}
{% block title %}<title>{{ SITE_NAME }}</title>{% endblock %}
<!-- Get Google Fonts we like -->
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto+Mono:400,300,700">
@ -21,7 +21,7 @@
<![endif]-->
{% endblock %}
</head>
<body class="hold-transition skin-blue sidebar-mini {% if not fullscreen_layout_setting %}layout-boxed{% endif %}">
<body class="hold-transition skin-blue sidebar-mini {% if not SETTING.get('fullscreen_layout') %}layout-boxed{% endif %}">
<div class="wrapper">
{% block pageheader %}
<header class="main-header">
@ -105,17 +105,45 @@
<!-- sidebar menu: : style can be found in sidebar.less -->
<ul class="sidebar-menu" data-widget="tree">
<li class="header">USER ACTIONS</li>
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i> <span>Dashboard</span></a></li>
<li class="{{ 'active' if active_page == 'dashboard' else '' }}">
<a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i> Dashboard</a>
</li>
{% if current_user.role.name == 'Administrator' %}
<li><a href="{{ url_for('domain_add') }}"><i class="fa fa-plus"></i> <span>New Domain</span></a></li>
<li class="{{ 'active' if active_page == 'new_domain' else '' }}">
<a href="{{ url_for('domain_add') }}"><i class="fa fa-plus"></i> New Domain</a>
</li>
<li class="header">ADMINISTRATION</li>
<li><a href="{{ url_for('admin') }}"><i class="fa fa-wrench"></i> <span>Admin Console</span></a></li>
<li><a href="{{ url_for('templates') }}"><i class="fa fa-clone"></i> <span>Domain Templates</span></a></li>
<li><a href="{{ url_for('admin_manageuser') }}"><i class="fa fa-users"></i> <span>Users</span></a></li>
<li><a href="{{ url_for('admin_manageaccount') }}"><i class="fa fa-industry"></i> <span>Accounts</span></a></li>
<li><a href="{{ url_for('admin_history') }}"><i class="fa fa-calendar"></i> <span>History</span></a></li>
<li><a href="{{ url_for('admin_settings') }}"><i class="fa fa-cog"></i> <span>Settings</span></a></li>
<li class="{{ 'active' if active_page == 'admin_console' else '' }}">
<a href="{{ url_for('admin') }}"><i class="fa fa-wrench"></i> Admin Console</a>
</li>
<li class="{{ 'active' if active_page == 'admin_domain_template' else '' }}">
<a href="{{ url_for('templates') }}"><i class="fa fa-clone"></i> Domain Templates</a>
</li>
<li class="{{ 'active' if active_page == 'admin_users' else '' }}">
<a href="{{ url_for('admin_manageuser') }}"><i class="fa fa-users"></i> Users</a>
</li>
<li class="{{ 'active' if active_page == 'admin_accounts' else '' }}">
<a href="{{ url_for('admin_manageaccount') }}"><i class="fa fa-industry"></i> Accounts</a>
</li>
<li class="{{ 'active' if active_page == 'admin_history' else '' }}">
<a href="{{ url_for('admin_history') }}"><i class="fa fa-calendar"></i> History</a>
</li>
<li class="{{ 'treeview active' if active_page == 'admin_settings' else 'treeview' }}">
<a href="#">
<i class="fa fa-cog"></i> Settings
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu" {% if active_page == 'admin_settings' %}style="display: block;"{% endif %}>
<li><a href="{{ url_for('admin_setting_basic') }}"><i class="fa fa-circle-o"></i></i> Basic</a></li>
<li><a href="{{ url_for('admin_setting_records') }}"><i class="fa fa-circle-o"></i> Records</a></li>
<li><a href="{{ url_for('admin_setting_pdns') }}"><i class="fa fa-circle-o"></i> PDNS</a></li>
<li><a href="{{ url_for('admin_setting_authentication') }}"><i class="fa fa-circle-o"></i> Authentication</a></li>
</ul>
</li>
{% endif %}
</ul>
{% endif %}
</section>
<!-- /.sidebar -->

View File

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - HOME</title>{% endblock %}
{% set active_page = "dashboard" %}
{% block title %}<title>Dashboard - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
@ -155,7 +156,7 @@
{% endblock %}
{% block extrascripts %}
<script>
PDNS_VERSION = '{{ pdns_version }}'
PDNS_VERSION = '{{ SETTING.get("pdns_version") }}'
// set up history data table
$("#tbl_history").DataTable({
"paging" : false,
@ -188,14 +189,14 @@
"ajax" : "{{ url_for('dashboard_domains') }}",
"info" : false,
"autoWidth" : false,
{% if default_domain_table_size_setting in ['10','25','50','100'] %}
{% if SETTING.get('default_domain_table_size') in ['10','25','50','100'] %}
"lengthMenu": [ [10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"]],
{% else %}
"lengthMenu": [ [10, 25, 50, 100, {{ default_domain_table_size_setting }}, -1],
[10, 25, 50, 100, {{ default_domain_table_size_setting }}, "All"]],
"lengthMenu": [ [10, 25, 50, 100, {{ SETTING.get('default_domain_table_size') }}, -1],
[10, 25, 50, 100, {{ SETTING.get('default_domain_table_size') }}, "All"]],
{% endif %}
"pageLength": {{ default_domain_table_size_setting }}
"pageLength": {{ SETTING.get('default_domain_table_size') }}
});
$(document.body).on('click', '.history-info-button', function() {
var modal = $("#modal_history_info");
@ -235,7 +236,7 @@
modal.modal('show');
});
{% if current_user.role.name == 'Administrator' or dnssec_adm_only == false %}
{% if current_user.role.name == 'Administrator' or not SETTING.get('dnssec_admins_only') %}
$(document.body).on("click", ".button_dnssec", function() {
var domain = $(this).prop('id');
getdnssec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec', domain);

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}<title>{{ domain.name }} - DNS Control Panel</title>{% endblock %}
{% block title %}<title>{{ domain.name }} - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<section class="content-header">
@ -104,7 +104,7 @@
{% endblock %}
{% block extrascripts %}
<script>
PDNS_VERSION = '{{ pdns_version }}'
PDNS_VERSION = '{{ SETTING.get("pdns_version") }}'
// superglobals
window.records_allow_edit = {{ editable_records|tojson }};
window.nEditing = null;
@ -118,14 +118,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"
},
@ -267,7 +267,7 @@
applyChanges({'domain': domain}, $SCRIPT_ROOT + '/domain/' + domain + '/update');
});
{% 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();

View File

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - Add Domain</title>{% endblock %}
{% set active_page = "new_domain" %}
{% block title %}<title>Add Domain - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - Domain Management</title>{% endblock %}
{% block title %}<title>Domain Management - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
{% if status %}
@ -105,9 +105,9 @@
</div>
<div class="box-body">
<p><input type="checkbox" id="{{ domain.name }}" class="auto_ptr_toggle"
{% for setting in domain.settings %}{% if setting.setting=='auto_ptr' and setting.value=='True' %}checked{% endif %}{% endfor %} {% if auto_ptr_setting %}disabled="True"{% endif %}>
{% for setting in domain.settings %}{% if setting.setting=='auto_ptr' and setting.value=='True' %}checked{% endif %}{% endfor %} {% if SETTING.get('auto_ptr') %}disabled="True"{% endif %}>
&nbsp;Allow automatic reverse pointer creation on record updates?{% if
auto_ptr_setting %}</br><code>Auto-ptr is enabled globally on the PDA system!</code>{% endif %}</p>
SETTING.get('auto_ptr') %}</br><code>Auto-ptr is enabled globally on the PDA system!</code>{% endif %}</p>
</div>
</div>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>DNS Control Panel - Log In</title>
<title>Log In - {{ SITE_NAME }}</title>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
{% assets "css_login" -%}
@ -20,7 +20,7 @@
<body class="hold-transition login-page">
<div class="login-box">
<div class="login-logo">
<a href="{{ url_for('index') }}">Sign In {{ login_title }}</a>
<a href="{{ url_for('index') }}"><b>PowerDNS</b>-Admin</a>
</div>
<!-- /.login-logo -->
<div class="login-box-body">
@ -31,42 +31,34 @@
{{ error }}
</div>
{% endif %}
<form action="" method="post">
<form action="" method="post" data-toggle="validator">
<div class="form-group">
{% if username %}
<input type="text" class="form-control" placeholder="Username" name="username" value="{{ username }}">
{% else %}
<input type="text" class="form-control" placeholder="Username" name="username">
{% endif %}
<span class="glyphicon glyphicon-user form-control-feedback"></span>
<input type="text" class="form-control" placeholder="Username" name="username" data-error="Please input your username" required {% if username %}value="{{ username }}"{% endif %}>
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
{% if password %}
<input type="password" class="form-control" placeholder="Password" name="password" value="{{ password }}">
{% else %}
<input type="password" class="form-control" placeholder="Password" name="password">
{% endif %}
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
<input type="password" class="form-control" placeholder="Password" name="password" data-error="Please input your password" required {% if password %}value="{{ password }}"{% endif %}>
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<input type="otptoken" class="form-control" placeholder="OTP Token" name="otptoken">
</div>
{% if ldap_enabled and basic_enabled %}
{% if SETTING.get('ldap_enabled') and SETTING.get('local_db_enabled') %}
<div class="form-group">
<select class="form-control" name="auth_method">
<option value="LOCAL">LOCAL Authentication</option>
{% if login_ldap_first_setting %}
{% if SETTING.get('login_ldap_first') %}
<option value="LDAP" selected="selected">LDAP Authentication</option>
{% else %}
<option value="LDAP">LDAP Authentication</option>
{% endif %}
</select>
</div>
{% elif ldap_enabled and not basic_enabled %}
{% elif SETTING.get('ldap_enabled') and not SETTING.get('local_db_enabled') %}
<div class="form-group">
<input type="hidden" name="auth_method" value="LDAP">
</div>
{% elif basic_enabled and not ldap_enabled %}
{% elif SETTING.get('local_db_enabled') and not SETTING.get('ldap_enabled') %}
<div class="form-group">
<input type="hidden" name="auth_method" value="LOCAL">
</div>
@ -91,25 +83,33 @@
<!-- /.col -->
</div>
</form>
{% if google_enabled %}
<a href="{{ url_for('google_login') }}">Google oauth login</a>
{% if SETTING.get('google_oauth_enabled') or SETTING.get('github_oauth_enabled') %}
<div class="social-auth-links text-center">
<p>- OR -</p>
{% if SETTING.get('github_oauth_enabled') %}
<a href="{{ url_for('github_login') }}" class="btn btn-block btn-social btn-github btn-flat"><i class="fa fa-github"></i> Sign in using
Github</a>
{% endif %}
{% if SETTING.get('google_oauth_enabled') %}
<a href="{{ url_for('google_login') }}" class="btn btn-block btn-social btn-google btn-flat"><i class="fa fa-google-plus"></i> Sign in using
Google</a>
{% endif %}
</div>
{% endif %}
{% if saml_enabled %}
<br>
<a href="{{ url_for('saml_login') }}">SAML login</a>
{% endif %}
{% if github_enabled %}
<br>
<a href="{{ url_for('github_login') }}">Github oauth login</a>
{% endif %}
{% if signup_enabled %}
{% if SETTING.get('signup_enabled') %}
<br>
<a href="{{ url_for('register') }}" class="text-center">Create an account </a>
{% endif %}
</div>
<!-- /.login-box-body -->
<div class="login-box-footer">
<center><p>2018 &copy; Khanh Ngo</p></center>
<center><p>Powered by <a href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</a></p></center>
</div>
</div>
<!-- /.login-box -->
@ -117,6 +117,10 @@
{% assets "js_login" -%}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{%- endassets %}
{% assets "js_validation" -%}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{%- endassets %}
<script>
$(function () {
$('input').iCheck({

View File

@ -12,7 +12,7 @@
<article>
<h1>We&rsquo;ll be back soon!</h1>
<div>
<p>Sorry for the inconvenience but we&rsquo;re performing some maintenance at the moment. If you need to you can always <a href="mailto:ngokhanhit@gmail.com">contact us</a>, otherwise we&rsquo;ll be back online shortly!</p>
<p>Sorry for the inconvenience but we&rsquo;re performing some maintenance at the moment. Please contact the System Administrator if you need more information</a>, otherwise we&rsquo;ll be back online shortly!</p>
<p>&mdash; Team</p>
</div>
</article>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>DNS Control Panel - Register</title>
<title>Register - {{ SITE_NAME }}</title>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
{% assets "css_login" -%}
@ -31,42 +31,41 @@
</div>
{% endif %}
<p class="login-box-msg">Enter your personal details below</p>
<form action="{{ url_for('login') }}" method="post">
<form action="{{ url_for('login') }}" method="post" data-toggle="validator">
<div class="form-group has-feedback">
<input type="text" class="form-control" placeholder="First Name"
name="firstname"> <span
class="glyphicon glyphicon-user form-control-feedback"></span>
<input type="text" class="form-control" placeholder="First Name" name="firstname" data-error="Please input your first name" required>
<span class="glyphicon glyphicon-user form-control-feedback"></span>
<span class="help-block with-errors"></span>
</div>
<div class="form-group has-feedback">
<input type="text" class="form-control" placeholder="Last name"
name="lastname"> <span
class="glyphicon glyphicon-user form-control-feedback"></span>
<input type="text" class="form-control" placeholder="Last name" name="lastname" data-error="Please input your last name" required>
<span class="glyphicon glyphicon-user form-control-feedback"></span>
<span class="help-block with-errors"></span>
</div>
<div class="form-group has-feedback">
<input type="email" class="form-control" placeholder="Email"
name="email"> <span
class="glyphicon glyphicon-envelope form-control-feedback"></span>
<input type="email" class="form-control" placeholder="Email" name="email" data-error="Please input your valid email address"
pattern="^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$" required>
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
<span class="help-block with-errors"></span>
</div>
<p class="login-box-msg">Enter your account details below</p>
<div class="form-group has-feedback">
<input type="text" class="form-control" placeholder="Username"
name="username"> <span
class="glyphicon glyphicon-user form-control-feedback"></span>
<input type="text" class="form-control" placeholder="Username" name="username" data-error="Please input your username" required>
<span class="glyphicon glyphicon-user form-control-feedback"></span>
<span class="help-block with-errors"></span>
</div>
<div class="form-group has-feedback">
<input type="password" class="form-control" placeholder="Password"
name="password"> <span
class="glyphicon glyphicon-lock form-control-feedback"></span>
<input type="password" class="form-control" placeholder="Password" id="password" name="password" data-error="Please input your password" required>
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<input type="password" class="form-control"
placeholder="Retype password" name="rpassword"> <span
class="glyphicon glyphicon-log-in form-control-feedback"></span>
<input type="password" class="form-control" placeholder="Retype password" name="rpassword" data-match="#password" data-match-error="Password confirmation does not match" required>
<span class="glyphicon glyphicon-log-in form-control-feedback"></span>
<span class="help-block with-errors"></span>
</div>
<div class="row">
<div class="col-xs-4 pull-left">
<button type="button" class="btn btn-flat btn-block"
id="button_back">Back</button>
<button type="button" class="btn btn-flat btn-block" id="button_back">Back</button>
</div>
<div class="col-xs-4 pull-right">
<button type="submit" class="btn btn-flat btn-primary btn-block">Register</button>
@ -77,7 +76,7 @@
</div>
<!-- /.form-box -->
<div class="login-box-footer">
<center><p>2018 &copy; Khanh Ngo</p></center>
<center><p>Powered by <a href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</a></p></center>
</div>
</div>
<!-- /.login-box -->
@ -85,6 +84,9 @@
{% assets "js_login" -%}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{%- endassets %}
{% assets "js_validation" -%}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{%- endassets %}
<script>
$(function () {
$('#button_back').click(function(){

View File

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - Templates</title>{% endblock %}
{% set active_page = "admin_domain_template" %}
{% block title %}<title>Templates - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->

View File

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - Create Template</title>{% endblock %}
{% set active_page = "admin_domain_template" %}
{% block title %}<title>Create Template - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->

View File

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - Edit Template</title>{% endblock %}
{% set active_page = "admin_domain_template" %}
{% block title %}<title>Edit Template - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<section class="content-header">
@ -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();

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - My Profile</title>{% endblock %}
{% block title %}<title>My Profile - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
@ -19,7 +19,7 @@
<div class="col-lg-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Edit my profile{% if external_account %} [Disabled - Authenticated externally]{% endif %}</h3>
<h3 class="box-title">Edit my profile{% if session['authentication_type'] != 'LOCAL' %} [Disabled - Authenticated externally]{% endif %}</h3>
</div>
<div class="box-body">
<!-- Custom Tabs -->
@ -29,10 +29,11 @@
Info</a></li>
<li><a href="#tabs-avatar" data-toggle="tab">Change
Avatar</a></li>
{% if not external_account %}<li><a href="#tabs-password" data-toggle="tab">Change
Password</a></li>
<li><a href="#tabs-authentication" data-toggle="tab">Authentication
</a></li>
{% if session['authentication_type'] == 'LOCAL' %}
<li><a href="#tabs-password" data-toggle="tab">Change Password</a></li>
{% endif %}
{% if session['authentication_type'] in ['LOCAL', 'LDAP'] %}
<li><a href="#tabs-authentication" data-toggle="tab">Authentication</a></li>
{% endif %}
</ul>
<div class="tab-content">
@ -41,18 +42,18 @@
<div class="form-group">
<label for="firstname">First Name</label> <input type="text"
class="form-control" name="firstname" id="firstname"
placeholder="{{ current_user.firstname }}" {% if external_account %}disabled{% endif %}>
placeholder="{{ current_user.firstname }}" {% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
</div>
<div class="form-group">
<label for="lastname">Last Name</label> <input type="text"
class="form-control" name="lastname" id="lastname"
placeholder="{{ current_user.lastname }}" {% if external_account %}disabled{% endif %}>
placeholder="{{ current_user.lastname }}" {% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
</div>
<div class="form-group">
<label for="email">E-mail</label> <input type="text"
class="form-control" name="email" id="email"
placeholder="{{ current_user.email }}" {% if external_account %}disabled{% endif %}>
</div>{% if not external_account %}
placeholder="{{ current_user.email }}" {% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
</div>{% if session['authentication_type'] == 'LOCAL' %}
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Submit</button>
</div>{% endif %}
@ -70,50 +71,50 @@
else %} <img
src="{{ current_user.email|email_to_gravatar_url(size=200) }}"
alt="" /> {% endif %}
</div>{% if not external_account %}
</div>{% if session['authentication_type'] == 'LOCAL' %}
<div>
<label for="file">Select image</label> <input type="file"
id="file" name="file">
</div>{% endif %}
</div>{% if not external_account %}
</div>{% if session['authentication_type'] == 'LOCAL' %}
<div>
<span class="label label-danger">NOTE! </span> <span>&nbsp;Only
supports <strong>.PNG, .JPG, .JPEG</strong>. The best size
to use is <strong>200x200</strong>.
</span>
</div>{% endif %}
</div>{% if not external_account %}
</div>{% if session['authentication_type'] == 'LOCAL' %}
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Submit</button>
</div>{% endif %}
</form>
</div>
{% if not external_account %}<div class="tab-pane" id="tabs-password">
{% 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' %}
<div class="tab-pane" id="tabs-password">
{% if not current_user.password %}
Your account password is managed via LDAP which isn't supported to change here.
{% else %}
<form action="{{ user_profile }}" method="post">
<div class="form-group">
<label for="password">New Password</label> <input
type="password" class="form-control" name="password"
id="newpassword" {% if external_account %}disabled{% endif %} />
type="password" class="form-control" name="password" id="newpassword"/>
</div>
<div class="form-group">
<label for="rpassword">Re-type New Password</label> <input
type="password" class="form-control" name="rpassword"
id="rpassword" {% if external_account %}disabled{% endif %} />
type="password" class="form-control" name="rpassword" id="rpassword"/>
</div>
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary" {% if external_account %}disabled{% endif %}>Change
password</button>
<button type="submit" class="btn btn-flat btn-primary">Change Password</button>
</div>
</form>
{% endif %}
</div>
{% endif %}
<!-- {% if session['authentication_type'] in ['LOCAL', 'LDAP'] %} -->
<div class="tab-pane" id="tabs-authentication">
<form action="{{ user_profile }}" method="post">
<div class="form-group">
<input type="checkbox" id="otp_toggle" class="otp_toggle" {% if current_user.otp_secret %}checked{% endif %} {% if external_account %}disabled{% endif %}>
<input type="checkbox" id="otp_toggle" class="otp_toggle" {% if current_user.otp_secret %}checked{% endif %}>
<label for="otp_toggle">Enable Two Factor Authentication</label>
{% if current_user.otp_secret %}
<div id="token_information">
@ -125,7 +126,8 @@
{% endif %}
</div>
</form>
</div>{% endif %}
</div>
<!-- {% endif %} -->
</div>
</div>
</div>

View File

@ -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'):
@ -481,6 +426,7 @@ def logout():
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/<path:setting>/toggle', methods=['POST'])
@app.route('/admin/setting/basic/<path:setting>/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/<path: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,34 +1354,152 @@ def admin_settings_edit(setting):
return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to toggle setting.' } ), 500)
@app.route('/admin/setting/basic/<path:setting>/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
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':
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:
if session['authentication_type'] in ['LOCAL', 'LDAP']:
file = request.files['file']
if file:
filename = secure_filename(file.filename)
@ -1453,12 +1508,14 @@ def user_profile():
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/<path:filename>')

View File

@ -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

View File

@ -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))

View File

@ -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 ###

View File

@ -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",

View File

@ -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"