mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2024-11-09 15:10:27 +00:00
Merge pull request #339 from ngoduykhanh/app_config_adjustment
Reading app config from DB and authentication adjustment
This commit is contained in:
commit
601859c952
20
.travis.yml
20
.travis.yml
@ -2,11 +2,23 @@ language: python
|
||||
python:
|
||||
- "3.5.2"
|
||||
before_install:
|
||||
- 'travis_retry sudo apt-get update'
|
||||
- 'travis_retry sudo apt-get install python3-dev libxml2-dev libxmlsec1-dev'
|
||||
- sudo apt-key adv --fetch-keys http://dl.yarnpkg.com/debian/pubkey.gpg
|
||||
- echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
- travis_retry sudo apt-get update
|
||||
- travis_retry sudo apt-get install python3-dev libxml2-dev libxmlsec1-dev yarn
|
||||
- mysql -e 'CREATE DATABASE pda';
|
||||
- mysql -e "GRANT ALL PRIVILEGES ON pda.* to pda@'%' IDENTIFIED BY 'changeme'";
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
before_script:
|
||||
- mv config_template.py config.py
|
||||
- mv config_template.py config.py
|
||||
- export FLASK_APP=app/__init__.py
|
||||
- flask db upgrade
|
||||
- yarn install --pure-lockfile
|
||||
- flask assets build
|
||||
script:
|
||||
- sh run_travis.sh
|
||||
- sh run_travis.sh
|
||||
cache:
|
||||
yarn: true
|
||||
services:
|
||||
- mysql
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
352
app/models.py
352
app/models.py
@ -9,9 +9,12 @@ import traceback
|
||||
import pyotp
|
||||
import re
|
||||
import dns.reversename
|
||||
import dns.inet
|
||||
import dns.name
|
||||
import sys
|
||||
import logging as logger
|
||||
|
||||
from ast import literal_eval
|
||||
from datetime import datetime
|
||||
from urllib.parse import urljoin
|
||||
from distutils.util import strtobool
|
||||
@ -23,38 +26,6 @@ from app.lib import utils
|
||||
|
||||
logging = logger.getLogger(__name__)
|
||||
|
||||
if 'LDAP_TYPE' in app.config.keys():
|
||||
LDAP_URI = app.config['LDAP_URI']
|
||||
LDAP_SEARCH_BASE = app.config['LDAP_SEARCH_BASE']
|
||||
LDAP_TYPE = app.config['LDAP_TYPE']
|
||||
LDAP_FILTER = app.config['LDAP_FILTER']
|
||||
LDAP_USERNAMEFIELD = app.config['LDAP_USERNAMEFIELD']
|
||||
|
||||
LDAP_GROUP_SECURITY = app.config.get('LDAP_GROUP_SECURITY')
|
||||
if LDAP_GROUP_SECURITY == True:
|
||||
LDAP_ADMIN_GROUP = app.config['LDAP_ADMIN_GROUP']
|
||||
LDAP_USER_GROUP = app.config['LDAP_USER_GROUP']
|
||||
else:
|
||||
LDAP_TYPE = False
|
||||
|
||||
if 'PRETTY_IPV6_PTR' in app.config.keys():
|
||||
import dns.inet
|
||||
import dns.name
|
||||
PRETTY_IPV6_PTR = app.config['PRETTY_IPV6_PTR']
|
||||
else:
|
||||
PRETTY_IPV6_PTR = False
|
||||
|
||||
PDNS_STATS_URL = app.config['PDNS_STATS_URL']
|
||||
PDNS_API_KEY = app.config['PDNS_API_KEY']
|
||||
PDNS_VERSION = app.config['PDNS_VERSION']
|
||||
API_EXTENDED_URL = utils.pdns_api_extended_uri(PDNS_VERSION)
|
||||
|
||||
# Flag for pdns v4.x.x
|
||||
# TODO: Find another way to do this
|
||||
if StrictVersion(PDNS_VERSION) >= StrictVersion('4.0.0'):
|
||||
NEW_SCHEMA = True
|
||||
else:
|
||||
NEW_SCHEMA = False
|
||||
|
||||
class Anonymous(AnonymousUserMixin):
|
||||
def __init__(self):
|
||||
@ -147,7 +118,7 @@ class User(db.Model):
|
||||
|
||||
def ldap_init_conn(self):
|
||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||
conn = ldap.initialize(LDAP_URI)
|
||||
conn = ldap.initialize(Setting().get('ldap_uri'))
|
||||
conn.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
|
||||
conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
|
||||
conn.set_option(ldap.OPT_X_TLS,ldap.OPT_X_TLS_DEMAND)
|
||||
@ -162,7 +133,7 @@ class User(db.Model):
|
||||
|
||||
try:
|
||||
conn = self.ldap_init_conn()
|
||||
conn.simple_bind_s(app.config['LDAP_ADMIN_USERNAME'], app.config['LDAP_ADMIN_PASSWORD'])
|
||||
conn.simple_bind_s(Setting().get('ldap_admin_username'), Setting().get('ldap_admin_password'))
|
||||
ldap_result_id = conn.search(baseDN, searchScope, searchFilter, retrieveAttributes)
|
||||
result_set = []
|
||||
|
||||
@ -177,6 +148,8 @@ class User(db.Model):
|
||||
|
||||
except ldap.LDAPError as e:
|
||||
logging.error(e)
|
||||
logging.debug('baseDN: {0}'.format(baseDN))
|
||||
logging.debug(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def ldap_auth(self, ldap_username, password):
|
||||
@ -207,34 +180,53 @@ class User(db.Model):
|
||||
|
||||
if method == 'LDAP':
|
||||
isadmin = False
|
||||
if not LDAP_TYPE:
|
||||
logging.error('LDAP authentication is disabled')
|
||||
return False
|
||||
LDAP_TYPE = Setting().get('ldap_type')
|
||||
LDAP_BASE_DN = Setting().get('ldap_base_dn')
|
||||
LDAP_FILTER_BASIC = Setting().get('ldap_filter_basic')
|
||||
LDAP_FILTER_USERNAME = Setting().get('ldap_filter_username')
|
||||
LDAP_ADMIN_GROUP = Setting().get('ldap_admin_group')
|
||||
LDAP_USER_GROUP = Setting().get('ldap_user_group')
|
||||
LDAP_GROUP_SECURITY_ENABLED = Setting().get('ldap_sg_enabled')
|
||||
|
||||
if LDAP_TYPE == 'ldap':
|
||||
searchFilter = "(&({0}={1}){2})".format(LDAP_USERNAMEFIELD, self.username, LDAP_FILTER)
|
||||
logging.debug('Ldap searchFilter "{0}"'.format(searchFilter))
|
||||
elif LDAP_TYPE == 'ad':
|
||||
searchFilter = "(&(objectcategory=person)({0}={1}){2})".format(LDAP_USERNAMEFIELD, self.username, LDAP_FILTER)
|
||||
searchFilter = "(&({0}={1}){2})".format(LDAP_FILTER_USERNAME, self.username, LDAP_FILTER_BASIC)
|
||||
logging.debug('Ldap searchFilter {0}'.format(searchFilter))
|
||||
|
||||
ldap_result = self.ldap_search(searchFilter, LDAP_BASE_DN)
|
||||
logging.debug('Ldap search result: {0}'.format(ldap_result))
|
||||
|
||||
ldap_result = self.ldap_search(searchFilter, LDAP_SEARCH_BASE)
|
||||
if not ldap_result:
|
||||
logging.warning('LDAP User "{0}" does not exist. Authentication request from {1}'.format(self.username, src_ip))
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
ldap_username = ldap.filter.escape_filter_chars(ldap_result[0][0][0])
|
||||
# check if LDAP_SECURITY_GROUP is enabled
|
||||
# check if LDAP_GROUP_SECURITY_ENABLED is True
|
||||
# user can be assigned to ADMIN or USER role.
|
||||
if LDAP_GROUP_SECURITY:
|
||||
if LDAP_GROUP_SECURITY_ENABLED:
|
||||
try:
|
||||
if (self.ldap_search(searchFilter, LDAP_ADMIN_GROUP)):
|
||||
isadmin = True
|
||||
logging.info('User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'.format(self.username,LDAP_ADMIN_GROUP))
|
||||
elif (self.ldap_search(searchFilter, LDAP_USER_GROUP)):
|
||||
logging.info('User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'.format(self.username,LDAP_USER_GROUP))
|
||||
if LDAP_TYPE == 'ldap':
|
||||
if (self.ldap_search(searchFilter, LDAP_ADMIN_GROUP)):
|
||||
isadmin = True
|
||||
logging.info('User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP))
|
||||
elif (self.ldap_search(searchFilter, LDAP_USER_GROUP)):
|
||||
logging.info('User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'.format(self.username, LDAP_USER_GROUP))
|
||||
else:
|
||||
logging.error('User {0} is not part of the "{1}" or "{2}" groups that allow access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP, LDAP_USER_GROUP))
|
||||
return False
|
||||
elif LDAP_TYPE == 'ad':
|
||||
user_ldap_groups = [g.decode("utf-8") for g in ldap_result[0][0][1]['memberOf']]
|
||||
logging.debug('user_ldap_groups: {0}'.format(user_ldap_groups))
|
||||
|
||||
if (LDAP_ADMIN_GROUP in user_ldap_groups):
|
||||
isadmin = True
|
||||
logging.info('User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP))
|
||||
elif (LDAP_USER_GROUP in user_ldap_groups):
|
||||
logging.info('User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'.format(self.username, LDAP_USER_GROUP))
|
||||
else:
|
||||
logging.error('User {0} is not part of the "{1}" or "{2}" groups that allow access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP, LDAP_USER_GROUP))
|
||||
return False
|
||||
else:
|
||||
logging.error('User {0} is not part of the "{1}" or "{2}" groups that allow access to PowerDNS-Admin'.format(self.username,LDAP_ADMIN_GROUP,LDAP_USER_GROUP))
|
||||
logging.error('Invalid LDAP type')
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error('LDAP group lookup for user "{0}" has failed. Authentication request from {1}'.format(self.username, src_ip))
|
||||
@ -256,13 +248,16 @@ class User(db.Model):
|
||||
self.firstname = self.username
|
||||
self.lastname = ''
|
||||
try:
|
||||
# try to get user's firstname & lastname from LDAP
|
||||
# this might be changed in the future
|
||||
self.firstname = ldap_result[0][0][1]['givenName'][0].decode("utf-8")
|
||||
self.lastname = ldap_result[0][0][1]['sn'][0].decode("utf-8")
|
||||
self.email = ldap_result[0][0][1]['mail'][0].decode("utf-8")
|
||||
# try to get user's firstname, lastname and email address from LDAP attributes
|
||||
if LDAP_TYPE == 'ldap':
|
||||
self.firstname = ldap_result[0][0][1]['givenName'][0].decode("utf-8")
|
||||
self.lastname = ldap_result[0][0][1]['sn'][0].decode("utf-8")
|
||||
self.email = ldap_result[0][0][1]['mail'][0].decode("utf-8")
|
||||
elif LDAP_TYPE == 'ad':
|
||||
self.firstname = ldap_result[0][0][1]['name'][0].decode("utf-8")
|
||||
self.email = ldap_result[0][0][1]['userPrincipalName'][0].decode("utf-8")
|
||||
except Exception as e:
|
||||
logging.info("Reading ldap data threw an exception {0}".format(e))
|
||||
logging.warning("Reading ldap data threw an exception {0}".format(e))
|
||||
logging.debug(traceback.format_exc())
|
||||
|
||||
# first register user will be in Administrator role
|
||||
@ -271,7 +266,7 @@ class User(db.Model):
|
||||
self.role_id = Role.query.filter_by(name='Administrator').first().id
|
||||
|
||||
# user will be in Administrator role if part of LDAP Admin group
|
||||
if LDAP_GROUP_SECURITY:
|
||||
if LDAP_GROUP_SECURITY_ENABLED:
|
||||
if isadmin == True:
|
||||
self.role_id = Role.query.filter_by(name='Administrator').first().id
|
||||
|
||||
@ -279,9 +274,9 @@ class User(db.Model):
|
||||
logging.info('Created user "{0}" in the DB'.format(self.username))
|
||||
|
||||
# user already exists in database, set their admin status based on group membership (if enabled)
|
||||
if LDAP_GROUP_SECURITY:
|
||||
if LDAP_GROUP_SECURITY_ENABLED:
|
||||
self.set_admin(isadmin)
|
||||
self.update_profile()
|
||||
|
||||
return True
|
||||
else:
|
||||
logging.error('Unsupported authentication method')
|
||||
@ -319,9 +314,9 @@ class User(db.Model):
|
||||
if User.query.count() == 0:
|
||||
self.role_id = Role.query.filter_by(name='Administrator').first().id
|
||||
|
||||
self.password = self.get_hashed_password(self.plain_text_password)
|
||||
self.password = self.get_hashed_password(self.plain_text_password) if self.plain_text_password else '*'
|
||||
|
||||
if self.password:
|
||||
if self.password and self.password != '*':
|
||||
self.password = self.password.decode("utf-8")
|
||||
|
||||
db.session.add(self)
|
||||
@ -741,6 +736,16 @@ class Domain(db.Model):
|
||||
self.last_check = last_check
|
||||
self.dnssec = dnssec
|
||||
self.account_id = account_id
|
||||
# PDNS configs
|
||||
self.PDNS_STATS_URL = Setting().get('pdns_api_url')
|
||||
self.PDNS_API_KEY = Setting().get('pdns_api_key')
|
||||
self.PDNS_VERSION = Setting().get('pdns_version')
|
||||
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
|
||||
|
||||
if StrictVersion(self.PDNS_VERSION) >= StrictVersion('4.0.0'):
|
||||
self.NEW_SCHEMA = True
|
||||
else:
|
||||
self.NEW_SCHEMA = False
|
||||
|
||||
def __repr__(self):
|
||||
return '<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
78
app/oauth.py
Normal 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
|
@ -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) -->
|
||||
|
@ -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) -->
|
||||
|
@ -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) -->
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
495
app/templates/admin_setting_authentication.html
Normal file
495
app/templates/admin_setting_authentication.html
Normal 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">×</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>
|
||||
|
||||
<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>
|
||||
|
||||
<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 %}
|
@ -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 <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 <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 %}
|
85
app/templates/admin_setting_pdns.html
Normal file
85
app/templates/admin_setting_pdns.html
Normal 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">×</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 %}
|
78
app/templates/admin_setting_records.html
Normal file
78
app/templates/admin_setting_records.html
Normal 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 %}
|
@ -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 -->
|
||||
@ -146,7 +174,7 @@
|
||||
</div>
|
||||
<!-- ./wrapper -->
|
||||
<script type="text/javascript">
|
||||
$SCRIPT_ROOT = {{ request.script_root|tojson|safe }};
|
||||
$SCRIPT_ROOT = {{ request.script_root|tojson|safe }};
|
||||
</script>
|
||||
{% block scripts %}
|
||||
{% assets "js_main" -%}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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) -->
|
||||
@ -45,7 +46,7 @@
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="radio" name="radio_type"id="radio_type_master" value="master"> Master
|
||||
<input type="radio" name="radio_type" id="radio_type_master" value="master"> Master
|
||||
</label>
|
||||
|
||||
<label>
|
||||
|
@ -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 %}>
|
||||
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>
|
||||
|
@ -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 © 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({
|
||||
|
@ -12,7 +12,7 @@
|
||||
<article>
|
||||
<h1>We’ll be back soon!</h1>
|
||||
<div>
|
||||
<p>Sorry for the inconvenience but we’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’ll be back online shortly!</p>
|
||||
<p>Sorry for the inconvenience but we’re performing some maintenance at the moment. Please contact the System Administrator if you need more information</a>, otherwise we’ll be back online shortly!</p>
|
||||
<p>— Team</p>
|
||||
</div>
|
||||
</article>
|
@ -1,96 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>DNS Control Panel - Register</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" -%}
|
||||
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
||||
{%- endassets %}
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
|
||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body class="hold-transition register-page">
|
||||
<div class="register-box">
|
||||
<div class="register-logo">
|
||||
<a href="{{ url_for('index') }}"><b>PowerDNS</b>-Admin</a>
|
||||
</div>
|
||||
<div class="register-box-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert"
|
||||
aria-hidden="true">×</button>
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="login-box-msg">Enter your personal details below</p>
|
||||
<form action="{{ url_for('login') }}" method="post">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
<div class="col-xs-4 pull-right">
|
||||
<button type="submit" class="btn btn-flat btn-primary btn-block">Register</button>
|
||||
</div>
|
||||
<!-- /.col -->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- /.form-box -->
|
||||
<div class="login-box-footer">
|
||||
<center><p>2018 © Khanh Ngo</p></center>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.login-box -->
|
||||
|
||||
{% assets "js_login" -%}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{%- endassets %}
|
||||
<script>
|
||||
$(function () {
|
||||
$('#button_back').click(function(){
|
||||
window.location.href='{{ url_for('login') }}';
|
||||
})
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<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" -%}
|
||||
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
||||
{%- endassets %}
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
|
||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body class="hold-transition register-page">
|
||||
<div class="register-box">
|
||||
<div class="register-logo">
|
||||
<a href="{{ url_for('index') }}"><b>PowerDNS</b>-Admin</a>
|
||||
</div>
|
||||
<div class="register-box-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert"
|
||||
aria-hidden="true">×</button>
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="login-box-msg">Enter your personal details below</p>
|
||||
<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" 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" 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" 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" 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" 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" 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>
|
||||
</div>
|
||||
<div class="col-xs-4 pull-right">
|
||||
<button type="submit" class="btn btn-flat btn-primary btn-block">Register</button>
|
||||
</div>
|
||||
<!-- /.col -->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- /.form-box -->
|
||||
<div class="login-box-footer">
|
||||
<center><p>Powered by <a href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</a></p></center>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.login-box -->
|
||||
|
||||
{% 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(){
|
||||
window.location.href='{{ url_for('login') }}';
|
||||
})
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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) -->
|
||||
|
@ -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) -->
|
||||
|
@ -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();
|
||||
|
@ -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> 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>
|
||||
|
429
app/views.py
429
app/views.py
@ -8,6 +8,7 @@ from distutils.util import strtobool
|
||||
from distutils.version import StrictVersion
|
||||
from functools import wraps
|
||||
from io import BytesIO
|
||||
from ast import literal_eval
|
||||
|
||||
import jinja2
|
||||
import qrcode as qrc
|
||||
@ -18,79 +19,58 @@ from werkzeug import secure_filename
|
||||
from werkzeug.security import gen_salt
|
||||
|
||||
from .models import User, Account, Domain, Record, Role, Server, History, Anonymous, Setting, DomainSetting, DomainTemplate, DomainTemplateRecord
|
||||
from app import app, login_manager, github, google
|
||||
from app import app, login_manager
|
||||
from app.lib import utils
|
||||
from app.oauth import github_oauth, google_oauth
|
||||
from app.decorators import admin_role_required, can_access_domain, can_configure_dnssec
|
||||
|
||||
if app.config['SAML_ENABLED']:
|
||||
from onelogin.saml2.auth import OneLogin_Saml2_Auth
|
||||
from onelogin.saml2.utils import OneLogin_Saml2_Utils
|
||||
|
||||
google = None
|
||||
github = None
|
||||
logging = logger.getLogger(__name__)
|
||||
|
||||
|
||||
# FILTERS
|
||||
app.jinja_env.filters['display_record_name'] = utils.display_record_name
|
||||
app.jinja_env.filters['display_master_name'] = utils.display_master_name
|
||||
app.jinja_env.filters['display_second_to_time'] = utils.display_time
|
||||
app.jinja_env.filters['email_to_gravatar_url'] = utils.email_to_gravatar_url
|
||||
|
||||
# Flag for pdns v4.x.x
|
||||
# TODO: Find another way to do this
|
||||
PDNS_VERSION = app.config['PDNS_VERSION']
|
||||
if StrictVersion(PDNS_VERSION) >= StrictVersion('4.0.0'):
|
||||
NEW_SCHEMA = True
|
||||
else:
|
||||
NEW_SCHEMA = False
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_fullscreen_layout_setting():
|
||||
setting_value = Setting().get('fullscreen_layout')
|
||||
return dict(fullscreen_layout_setting=strtobool(setting_value))
|
||||
|
||||
def inject_sitename():
|
||||
setting = Setting().get('site_name')
|
||||
return dict(SITE_NAME=setting)
|
||||
|
||||
@app.context_processor
|
||||
def inject_record_helper_setting():
|
||||
setting_value = Setting().get('record_helper')
|
||||
return dict(record_helper_setting=strtobool(setting_value))
|
||||
def inject_setting():
|
||||
setting = Setting()
|
||||
return dict(SETTING=setting)
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_login_ldap_first_setting():
|
||||
setting_value = Setting().get('login_ldap_first')
|
||||
return dict(login_ldap_first_setting=strtobool(setting_value))
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_default_record_table_size_setting():
|
||||
setting_value = Setting().get('default_record_table_size')
|
||||
return dict(default_record_table_size_setting=setting_value)
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_default_domain_table_size_setting():
|
||||
setting_value = Setting().get('default_domain_table_size')
|
||||
return dict(default_domain_table_size_setting=setting_value)
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_auto_ptr_setting():
|
||||
setting_value = Setting().get('auto_ptr')
|
||||
return dict(auto_ptr_setting=strtobool(setting_value))
|
||||
@app.before_first_request
|
||||
def register_modules():
|
||||
global google
|
||||
global github
|
||||
google = google_oauth()
|
||||
github = github_oauth()
|
||||
|
||||
|
||||
# START USER AUTHENTICATION HANDLER
|
||||
@app.before_request
|
||||
def before_request():
|
||||
# check site maintenance mode first
|
||||
maintenance = Setting().get('maintenance')
|
||||
if strtobool(maintenance):
|
||||
return render_template('maintenance.html')
|
||||
|
||||
# check if user is anonymous
|
||||
g.user = current_user
|
||||
login_manager.anonymous_user = Anonymous
|
||||
|
||||
# check site maintenance mode
|
||||
maintenance = Setting().get('maintenance')
|
||||
if maintenance and current_user.is_authenticated and current_user.role.name != 'Administrator':
|
||||
return render_template('maintenance.html')
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(id):
|
||||
@ -164,8 +144,7 @@ def error(code, msg=None):
|
||||
|
||||
@app.route('/register', methods=['GET'])
|
||||
def register():
|
||||
SIGNUP_ENABLED = app.config['SIGNUP_ENABLED']
|
||||
if SIGNUP_ENABLED:
|
||||
if Setting().get('signup_enabled'):
|
||||
return render_template('register.html')
|
||||
else:
|
||||
return render_template('errors/404.html'), 404
|
||||
@ -173,16 +152,21 @@ def register():
|
||||
|
||||
@app.route('/google/login')
|
||||
def google_login():
|
||||
if not app.config.get('GOOGLE_OAUTH_ENABLE'):
|
||||
if not Setting().get('google_oauth_enabled') or google is None:
|
||||
logging.error('Google OAuth is disabled or you have not yet reloaded the pda application after enabling.')
|
||||
return abort(400)
|
||||
return google.authorize(callback=url_for('authorized', _external=True))
|
||||
else:
|
||||
return google.authorize(callback=url_for('google_authorized', _external=True))
|
||||
|
||||
|
||||
@app.route('/github/login')
|
||||
def github_login():
|
||||
if not app.config.get('GITHUB_OAUTH_ENABLE'):
|
||||
if not Setting().get('github_oauth_enabled') or github is None:
|
||||
logging.error('Github OAuth is disabled or you have not yet reloaded the pda application after enabling.')
|
||||
return abort(400)
|
||||
return github.authorize(callback=url_for('authorized', _external=True))
|
||||
else:
|
||||
return github.authorize(callback=url_for('github_authorized', _external=True))
|
||||
|
||||
|
||||
@app.route('/saml/login')
|
||||
def saml_login():
|
||||
@ -193,6 +177,7 @@ def saml_login():
|
||||
redirect_url=OneLogin_Saml2_Utils.get_self_url(req) + url_for('saml_authorized')
|
||||
return redirect(auth.login(return_to=redirect_url))
|
||||
|
||||
|
||||
@app.route('/saml/metadata')
|
||||
def saml_metadata():
|
||||
if not app.config.get('SAML_ENABLED'):
|
||||
@ -210,6 +195,7 @@ def saml_metadata():
|
||||
resp = make_response(errors.join(', '), 500)
|
||||
return resp
|
||||
|
||||
|
||||
@app.route('/saml/authorized', methods=['GET', 'POST'])
|
||||
def saml_authorized():
|
||||
errors = []
|
||||
@ -288,21 +274,17 @@ def saml_authorized():
|
||||
history.add()
|
||||
user.plain_text_password = None
|
||||
user.update_profile()
|
||||
session['external_auth'] = True
|
||||
session['authentication_type'] = 'SAML'
|
||||
login_user(user, remember=False)
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
return render_template('errors/SAML.html', errors=errors)
|
||||
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
@login_manager.unauthorized_handler
|
||||
def login():
|
||||
LOGIN_TITLE = app.config['LOGIN_TITLE'] if 'LOGIN_TITLE' in app.config.keys() else ''
|
||||
BASIC_ENABLED = app.config['BASIC_ENABLED']
|
||||
SIGNUP_ENABLED = app.config['SIGNUP_ENABLED']
|
||||
LDAP_ENABLED = app.config.get('LDAP_ENABLED')
|
||||
GITHUB_ENABLE = app.config.get('GITHUB_OAUTH_ENABLE')
|
||||
GOOGLE_ENABLE = app.config.get('GOOGLE_OAUTH_ENABLE')
|
||||
SAML_ENABLED = app.config.get('SAML_ENABLED')
|
||||
|
||||
if g.user is not None and current_user.is_authenticated:
|
||||
@ -315,7 +297,6 @@ def login():
|
||||
email = user_data['email']
|
||||
user = User.query.filter_by(username=email).first()
|
||||
if not user:
|
||||
# create user
|
||||
user = User(username=email,
|
||||
firstname=first_name,
|
||||
lastname=surname,
|
||||
@ -329,18 +310,23 @@ def login():
|
||||
|
||||
session['user_id'] = user.id
|
||||
login_user(user, remember = False)
|
||||
session['external_auth'] = True
|
||||
session['authentication_type'] = 'OAuth'
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if 'github_token' in session:
|
||||
me = github.get('user')
|
||||
user_info = me.data
|
||||
user = User.query.filter_by(username=user_info['name']).first()
|
||||
me = github.get('user').data
|
||||
|
||||
github_username = me['login']
|
||||
github_name = me['name']
|
||||
github_email = me['email']
|
||||
|
||||
user = User.query.filter_by(username=github_username).first()
|
||||
if not user:
|
||||
# create user
|
||||
user = User(username=user_info['name'],
|
||||
user = User(username=github_username,
|
||||
plain_text_password=None,
|
||||
email=user_info['email'])
|
||||
firstname=github_name,
|
||||
lastname='',
|
||||
email=github_email)
|
||||
|
||||
result = user.create_local_user()
|
||||
if not result['status']:
|
||||
@ -348,18 +334,12 @@ def login():
|
||||
return redirect(url_for('login'))
|
||||
|
||||
session['user_id'] = user.id
|
||||
session['external_auth'] = True
|
||||
session['authentication_type'] = 'OAuth'
|
||||
login_user(user, remember = False)
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if request.method == 'GET':
|
||||
return render_template('login.html', github_enabled=GITHUB_ENABLE,
|
||||
google_enabled=GOOGLE_ENABLE,
|
||||
saml_enabled=SAML_ENABLED,
|
||||
ldap_enabled=LDAP_ENABLED,
|
||||
login_title=LOGIN_TITLE,
|
||||
basic_enabled=BASIC_ENABLED,
|
||||
signup_enabled=SIGNUP_ENABLED)
|
||||
return render_template('login.html', saml_enabled=SAML_ENABLED)
|
||||
|
||||
# process login
|
||||
username = request.form['username']
|
||||
@ -373,8 +353,7 @@ def login():
|
||||
email = request.form.get('email')
|
||||
rpassword = request.form.get('rpassword')
|
||||
|
||||
if auth_method != 'LOCAL':
|
||||
session['external_auth'] = True
|
||||
session['authentication_type'] = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL'
|
||||
|
||||
if None in [firstname, lastname, email]:
|
||||
#login case
|
||||
@ -387,46 +366,18 @@ def login():
|
||||
try:
|
||||
auth = user.is_validate(method=auth_method, src_ip=request.remote_addr)
|
||||
if auth == False:
|
||||
return render_template('login.html', error='Invalid credentials',
|
||||
github_enabled=GITHUB_ENABLE,
|
||||
google_enabled=GOOGLE_ENABLE,
|
||||
saml_enabled=SAML_ENABLED,
|
||||
ldap_enabled=LDAP_ENABLED,
|
||||
login_title=LOGIN_TITLE,
|
||||
basic_enabled=BASIC_ENABLED,
|
||||
signup_enabled=SIGNUP_ENABLED)
|
||||
return render_template('login.html', saml_enabled=SAML_ENABLED, error='Invalid credentials')
|
||||
except Exception as e:
|
||||
return render_template('login.html', error=e,
|
||||
github_enabled=GITHUB_ENABLE,
|
||||
google_enabled=GOOGLE_ENABLE,
|
||||
saml_enabled=SAML_ENABLED,
|
||||
ldap_enabled=LDAP_ENABLED,
|
||||
login_title=LOGIN_TITLE,
|
||||
basic_enabled=BASIC_ENABLED,
|
||||
signup_enabled=SIGNUP_ENABLED)
|
||||
return render_template('login.html', saml_enabled=SAML_ENABLED, error=e)
|
||||
|
||||
# check if user enabled OPT authentication
|
||||
if user.otp_secret:
|
||||
if otp_token and otp_token.isdigit():
|
||||
good_token = user.verify_totp(otp_token)
|
||||
if not good_token:
|
||||
return render_template('login.html', error='Invalid credentials',
|
||||
github_enabled=GITHUB_ENABLE,
|
||||
google_enabled=GOOGLE_ENABLE,
|
||||
saml_enabled=SAML_ENABLED,
|
||||
ldap_enabled=LDAP_ENABLED,
|
||||
login_title=LOGIN_TITLE,
|
||||
basic_enabled=BASIC_ENABLED,
|
||||
signup_enabled=SIGNUP_ENABLED)
|
||||
return render_template('login.html', saml_enabled=SAML_ENABLED, error='Invalid credentials')
|
||||
else:
|
||||
return render_template('login.html', error='Token required',
|
||||
github_enabled=GITHUB_ENABLE,
|
||||
google_enabled=GOOGLE_ENABLE,
|
||||
saml_enabled=SAML_ENABLED,
|
||||
ldap_enabled=LDAP_ENABLED,
|
||||
login_title=LOGIN_TITLE,
|
||||
basic_enabled=BASIC_ENABLED,
|
||||
signup_enabled=SIGNUP_ENABLED)
|
||||
return render_template('login.html', saml_enabled=SAML_ENABLED, error='Token required')
|
||||
|
||||
login_user(user, remember = remember_me)
|
||||
return redirect(request.args.get('next') or url_for('index'))
|
||||
@ -437,35 +388,29 @@ def login():
|
||||
# registration case
|
||||
user = User(username=username, plain_text_password=password, firstname=firstname, lastname=lastname, email=email)
|
||||
|
||||
# TODO: Move this into the JavaScript
|
||||
# validate password and password confirmation
|
||||
if password != rpassword:
|
||||
error = "Password confirmation does not match"
|
||||
return render_template('register.html', error=error)
|
||||
|
||||
try:
|
||||
result = user.create_local_user()
|
||||
if result == True:
|
||||
return render_template('login.html', username=username, password=password,
|
||||
github_enabled=GITHUB_ENABLE,
|
||||
google_enabled=GOOGLE_ENABLE,
|
||||
saml_enabled=SAML_ENABLED,
|
||||
ldap_enabled=LDAP_ENABLED,
|
||||
login_title=LOGIN_TITLE,
|
||||
basic_enabled=BASIC_ENABLED,
|
||||
signup_enabled=SIGNUP_ENABLED)
|
||||
if result and result['status']:
|
||||
return render_template('login.html', saml_enabled=SAML_ENABLED, username=username, password=password)
|
||||
else:
|
||||
return render_template('register.html', error=result['msg'])
|
||||
except Exception as e:
|
||||
return render_template('register.html', error=e)
|
||||
|
||||
|
||||
def clear_session():
|
||||
session.pop('user_id', None)
|
||||
session.pop('github_token', None)
|
||||
session.pop('google_token', None)
|
||||
session.pop('authentication_type', None)
|
||||
session.clear()
|
||||
logout_user()
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
if app.config.get('SAML_ENABLED') and 'samlSessionIndex' in session and app.config.get('SAML_LOGOUT'):
|
||||
@ -474,13 +419,14 @@ def logout():
|
||||
if app.config.get('SAML_LOGOUT_URL'):
|
||||
return redirect(auth.logout(name_id_format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
return_to = app.config.get('SAML_LOGOUT_URL'),
|
||||
session_index = session['samlSessionIndex'], name_id=session['samlNameId']))
|
||||
session_index = session['samlSessionIndex'], name_id=session['samlNameId']))
|
||||
return redirect(auth.logout(name_id_format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
session_index = session['samlSessionIndex'],
|
||||
session_index = session['samlSessionIndex'],
|
||||
name_id=session['samlNameId']))
|
||||
clear_session()
|
||||
return redirect(url_for('login'))
|
||||
|
||||
|
||||
@app.route('/saml/sls')
|
||||
def saml_logout():
|
||||
req = utils.prepare_flask_request(request)
|
||||
@ -498,10 +444,15 @@ def saml_logout():
|
||||
else:
|
||||
return render_template('errors/SAML.html', errors=errors)
|
||||
|
||||
|
||||
@app.route('/dashboard', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def dashboard():
|
||||
if not app.config.get('BG_DOMAIN_UPDATES'):
|
||||
if not Setting().get('pdns_api_url') or not Setting().get('pdns_api_key') or not Setting().get('pdns_version'):
|
||||
return redirect(url_for('admin_setting_pdns'))
|
||||
|
||||
BG_DOMAIN_UPDATE = Setting().get('bg_domain_updates')
|
||||
if not BG_DOMAIN_UPDATE:
|
||||
logging.debug('Update domains in foreground')
|
||||
d = Domain().update()
|
||||
else:
|
||||
@ -519,7 +470,7 @@ def dashboard():
|
||||
else:
|
||||
uptime = 0
|
||||
|
||||
return render_template('dashboard.html', domain_count=domain_count, users=users, history_number=history_number, uptime=uptime, histories=history, dnssec_adm_only=app.config['DNSSEC_ADMINS_ONLY'], pdns_version=app.config['PDNS_VERSION'], show_bg_domain_button=app.config['BG_DOMAIN_UPDATES'])
|
||||
return render_template('dashboard.html', domain_count=domain_count, users=users, history_number=history_number, uptime=uptime, histories=history, show_bg_domain_button=BG_DOMAIN_UPDATE)
|
||||
|
||||
|
||||
@app.route('/dashboard-domains', methods=['GET'])
|
||||
@ -621,31 +572,33 @@ def domain(domain_name):
|
||||
# can not get any record, API server might be down
|
||||
return redirect(url_for('error', code=500))
|
||||
|
||||
quick_edit = strtobool(Setting().get('allow_quick_edit'))
|
||||
|
||||
quick_edit = Setting().get('allow_quick_edit')
|
||||
records_allow_to_edit = Setting().get_records_allow_to_edit()
|
||||
forward_records_allow_to_edit = Setting().get_forward_records_allow_to_edit()
|
||||
reverse_records_allow_to_edit = Setting().get_reverse_records_allow_to_edit()
|
||||
records = []
|
||||
#TODO: This should be done in the "model" instead of "view"
|
||||
if NEW_SCHEMA:
|
||||
|
||||
if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'):
|
||||
for jr in jrecords:
|
||||
if jr['type'] in app.config['RECORDS_ALLOW_EDIT']:
|
||||
if jr['type'] in Setting().get_records_allow_to_edit():
|
||||
for subrecord in jr['records']:
|
||||
record = Record(name=jr['name'], type=jr['type'], status='Disabled' if subrecord['disabled'] else 'Active', ttl=jr['ttl'], data=subrecord['content'])
|
||||
records.append(record)
|
||||
if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name):
|
||||
editable_records = app.config['RECORDS_ALLOW_EDIT']
|
||||
editable_records = forward_records_allow_to_edit
|
||||
else:
|
||||
editable_records = app.config['REVERSE_RECORDS_ALLOW_EDIT']
|
||||
editable_records = reverse_records_allow_to_edit
|
||||
return render_template('domain.html', domain=domain, records=records, editable_records=editable_records, quick_edit=quick_edit)
|
||||
else:
|
||||
for jr in jrecords:
|
||||
if jr['type'] in app.config['RECORDS_ALLOW_EDIT']:
|
||||
if jr['type'] in Setting().get_records_allow_to_edit():
|
||||
record = Record(name=jr['name'], type=jr['type'], status='Disabled' if jr['disabled'] else 'Active', ttl=jr['ttl'], data=jr['content'])
|
||||
records.append(record)
|
||||
if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name):
|
||||
editable_records = app.config['FORWARD_RECORDS_ALLOW_EDIT']
|
||||
editable_records = forward_records_allow_to_edit
|
||||
else:
|
||||
editable_records = app.config['REVERSE_RECORDS_ALLOW_EDIT']
|
||||
return render_template('domain.html', domain=domain, records=records, editable_records=editable_records, quick_edit=quick_edit, pdns_version=app.config['PDNS_VERSION'])
|
||||
editable_records = reverse_records_allow_to_edit
|
||||
return render_template('domain.html', domain=domain, records=records, editable_records=editable_records, quick_edit=quick_edit)
|
||||
|
||||
|
||||
@app.route('/admin/domain/add', methods=['GET', 'POST'])
|
||||
@ -1036,16 +989,16 @@ def create_template_from_zone():
|
||||
if zone_info:
|
||||
jrecords = zone_info['records']
|
||||
|
||||
if NEW_SCHEMA:
|
||||
if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'):
|
||||
for jr in jrecords:
|
||||
if jr['type'] in app.config['RECORDS_ALLOW_EDIT']:
|
||||
if jr['type'] in Setting().get_records_allow_to_edit():
|
||||
name = '@' if jr['name'] == domain_name else re.sub('\.{}$'.format(domain_name), '', jr['name'])
|
||||
for subrecord in jr['records']:
|
||||
record = DomainTemplateRecord(name=name, type=jr['type'], status=True if subrecord['disabled'] else False, ttl=jr['ttl'], data=subrecord['content'])
|
||||
records.append(record)
|
||||
else:
|
||||
for jr in jrecords:
|
||||
if jr['type'] in app.config['RECORDS_ALLOW_EDIT']:
|
||||
if jr['type'] in Setting().get_records_allow_to_edit():
|
||||
name = '@' if jr['name'] == domain_name else re.sub('\.{}$'.format(domain_name), '', jr['name'])
|
||||
record = DomainTemplateRecord(name=name, type=jr['type'], status=True if jr['disabled'] else False, ttl=jr['ttl'], data=jr['content'])
|
||||
records.append(record)
|
||||
@ -1071,14 +1024,15 @@ def create_template_from_zone():
|
||||
def edit_template(template):
|
||||
try:
|
||||
t = DomainTemplate.query.filter(DomainTemplate.name == template).first()
|
||||
records_allow_to_edit = Setting().get_records_allow_to_edit()
|
||||
if t is not None:
|
||||
records = []
|
||||
for jr in t.records:
|
||||
if jr.type in app.config['RECORDS_ALLOW_EDIT']:
|
||||
if jr.type in records_allow_to_edit:
|
||||
record = DomainTemplateRecord(name=jr.name, type=jr.type, status='Disabled' if jr.status else 'Active', ttl=jr.ttl, data=jr.data)
|
||||
records.append(record)
|
||||
|
||||
return render_template('template_edit.html', template=t.name, records=records, editable_records=app.config['RECORDS_ALLOW_EDIT'])
|
||||
return render_template('template_edit.html', template=t.name, records=records, editable_records=records_allow_to_edit)
|
||||
except:
|
||||
logging.error(traceback.print_exc())
|
||||
return redirect(url_for('error', code=500))
|
||||
@ -1140,6 +1094,9 @@ def delete_template(template):
|
||||
@login_required
|
||||
@admin_role_required
|
||||
def admin():
|
||||
if not Setting().get('pdns_api_url') or not Setting().get('pdns_api_key') or not Setting().get('pdns_version'):
|
||||
return redirect(url_for('admin_setting_pdns'))
|
||||
|
||||
domains = Domain.query.all()
|
||||
users = User.query.all()
|
||||
|
||||
@ -1374,39 +1331,19 @@ def admin_history():
|
||||
return render_template('admin_history.html', histories=histories)
|
||||
|
||||
|
||||
@app.route('/admin/settings', methods=['GET'])
|
||||
@app.route('/admin/setting/basic', methods=['GET'])
|
||||
@login_required
|
||||
@admin_role_required
|
||||
def admin_settings():
|
||||
def admin_setting_basic():
|
||||
if request.method == 'GET':
|
||||
# start with a copy of the setting defaults (ignore maintenance setting)
|
||||
settings = Setting.defaults.copy()
|
||||
settings.pop('maintenance', None)
|
||||
|
||||
# update settings info with any customizations
|
||||
for s in settings:
|
||||
value = Setting().get(s)
|
||||
if value is not None:
|
||||
settings[s] = value
|
||||
|
||||
return render_template('admin_settings.html', settings=settings)
|
||||
settings = Setting.query.filter(Setting.view=='basic').all()
|
||||
return render_template('admin_setting_basic.html', settings=settings)
|
||||
|
||||
|
||||
@app.route('/admin/setting/<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,48 +1354,168 @@ def admin_settings_edit(setting):
|
||||
return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to toggle setting.' } ), 500)
|
||||
|
||||
|
||||
@app.route('/admin/setting/basic/<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
|
||||
firstname = request.form['firstname'] if 'firstname' in request.form else ''
|
||||
lastname = request.form['lastname'] if 'lastname' in request.form else ''
|
||||
email = request.form['email'] if 'email' in request.form else ''
|
||||
new_password = request.form['password'] if 'password' in request.form else ''
|
||||
if session['authentication_type'] == 'LOCAL':
|
||||
firstname = request.form['firstname'] if 'firstname' in request.form else ''
|
||||
lastname = request.form['lastname'] if 'lastname' in request.form else ''
|
||||
email = request.form['email'] if 'email' in request.form else ''
|
||||
new_password = request.form['password'] if 'password' in request.form else ''
|
||||
else:
|
||||
firstname = lastname = email = new_password = ''
|
||||
logging.warning('Authenticated externally. User {0} information will not allowed to update the profile'.format(current_user.username))
|
||||
|
||||
# json data
|
||||
if request.data:
|
||||
jdata = request.json
|
||||
data = jdata['data']
|
||||
if jdata['action'] == 'enable_otp':
|
||||
enable_otp = data['enable_otp']
|
||||
user = User(username=current_user.username)
|
||||
user.update_profile(enable_otp=enable_otp)
|
||||
return make_response(jsonify( { 'status': 'ok', 'msg': 'Change OTP Authentication successfully. Status: {0}'.format(enable_otp) } ), 200)
|
||||
if session['authentication_type'] in ['LOCAL', 'LDAP']:
|
||||
enable_otp = data['enable_otp']
|
||||
user = User(username=current_user.username)
|
||||
user.update_profile(enable_otp=enable_otp)
|
||||
return make_response(jsonify( { 'status': 'ok', 'msg': 'Change OTP Authentication successfully. Status: {0}'.format(enable_otp) } ), 200)
|
||||
else:
|
||||
return make_response(jsonify( { 'status': 'error', 'msg': 'User {0} is externally. You are not allowed to update the OTP'.format(current_user.username) } ), 400)
|
||||
|
||||
# get new avatar
|
||||
save_file_name = None
|
||||
if 'file' in request.files:
|
||||
file = request.files['file']
|
||||
if file:
|
||||
filename = secure_filename(file.filename)
|
||||
file_extension = filename.rsplit('.', 1)[1]
|
||||
if session['authentication_type'] in ['LOCAL', 'LDAP']:
|
||||
file = request.files['file']
|
||||
if file:
|
||||
filename = secure_filename(file.filename)
|
||||
file_extension = filename.rsplit('.', 1)[1]
|
||||
|
||||
if file_extension.lower() in ['jpg', 'jpeg', 'png']:
|
||||
save_file_name = current_user.username + '.' + file_extension
|
||||
file.save(os.path.join(app.config['UPLOAD_DIR'], 'avatar', save_file_name))
|
||||
if file_extension.lower() in ['jpg', 'jpeg', 'png']:
|
||||
save_file_name = current_user.username + '.' + file_extension
|
||||
file.save(os.path.join(app.config['UPLOAD_DIR'], 'avatar', save_file_name))
|
||||
else:
|
||||
logging.error('Authenticated externally. User {0} is not allowed to update the avatar')
|
||||
abort(400)
|
||||
|
||||
# update user profile
|
||||
user = User(username=current_user.username, plain_text_password=new_password, firstname=firstname, lastname=lastname, email=email, avatar=save_file_name, reload_info=False)
|
||||
user.update_profile()
|
||||
|
||||
return render_template('user_profile.html', external_account=external_account)
|
||||
return render_template('user_profile.html')
|
||||
|
||||
|
||||
@app.route('/user/avatar/<path:filename>')
|
||||
|
@ -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
|
||||
|
@ -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))
|
@ -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 ###
|
@ -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",
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user