Merge pull request #229 from ngoduykhanh/development

Merge development branch to master
This commit is contained in:
Khanh Ngo 2018-04-01 16:16:11 +07:00 committed by GitHub
commit 6e07017361
32 changed files with 1789 additions and 437 deletions

1
.gitignore vendored
View file

@ -23,6 +23,7 @@ nosetests.xml
flask
config.py
logfile.log
log.txt
db_repository/*
upload/avatar/*

17
Dockerfile Normal file
View file

@ -0,0 +1,17 @@
FROM ubuntu:latest
MAINTAINER Khanh Ngo "ngokhanhit@gmail.com"
ARG ENVIRONMENT=development
ENV ENVIRONMENT=${ENVIRONMENT}
WORKDIR /powerdns-admin
RUN apt-get update -y
RUN apt-get install -y python3-pip python3-dev libmysqlclient-dev supervisor
RUN apt-get install -y libsasl2-dev libldap2-dev libssl-dev
COPY ./requirements.txt /powerdns-admin/requirements.txt
RUN pip3 install -r requirements.txt
ADD ./supervisord.conf /etc/supervisord.conf
ADD . /powerdns-admin/
COPY ./configs/${ENVIRONMENT}.py /powerdns-admin/config.py

View file

@ -1,13 +1,14 @@
# PowerDNS-Admin
PowerDNS Web-GUI - Built by Flask
A PowerDNS web interface with advanced features.
#### Features:
- Multiple domain management
- Local / LDAP user authentication
- Support Two-factor authentication (TOTP)
- Domain template
- User management
- User access management based on domain
- User activity logging
- Local DB / LDAP / Active Directory user authentication
- Support Two-factor authentication (TOTP)
- Dashboard and pdns service statistics
- DynDNS 2 protocol support
- Edit IPv6 PTRs using IPv6 addresses directly (no more editing of literal addresses!)
@ -18,7 +19,7 @@ PowerDNS Web-GUI - Built by Flask
PowerDNS-Admin supports PowerDNS autoritative server versions **3.4.2** and higher.
### pdns Service
I assume that you have already installed powerdns service. Make sure that your `/etc/pdns/pdns.conf` has these contents
I assume that you have already installed pdns service. Make sure that your `pdns.conf` config file has these contents
PowerDNS 4.0.0 and later
```
@ -34,55 +35,35 @@ experimental-api-key=your-powerdns-api-key
webserver=yes
```
This will enable API access in PowerDNS so PowerDNS-Admin can intergrate with PowerDNS.
This will enable API access in pdns service so PowerDNS-Admin can intergrate with it.
### Create Database
We will create a database which used by this web application. Please note that this database is difference from pdns database itself.
You could use any database that SQLAlchemy supports. For example MySQL (you will need to `pip install MySQL-python` to use MySQL backend):
PowerDNS-Admin supports MySQL server, Maria DB, PostgresQL and SQL Lite.
```
MariaDB [(none)]> CREATE DATABASE powerdnsadmin;
MariaDB [(none)]> GRANT ALL PRIVILEGES ON powerdnsadmin.* TO powerdnsadmin@'%' IDENTIFIED BY 'your-password';
```
For testing purpose, you could also use SQLite as backend. This way you do not have to install `MySQL-python` dependency.
### Running PowerDNS-Admin
There are several ways to run PowerDNS-Admin. Following is a simple way to start PowerDNS-Admin with docker in development environment.
### PowerDNS-Admin
Firstly, let's edit `configs/developments.py` configuration file.
In this installation guide, I am using CentOS 7 and run my python stuffs with *virtualenv*. If you don't have it, lets install it:
```
$ sudo yum install python-pip
$ sudo pip install virtualenv
```
Secondly, build the docker image of PowerDNS-Admin
In your python web app directory, create a `flask` directory via `virtualenv`
```
$ virtualenv flask
```
``` $docker-compose -f docker-compose.dev.yml build```
Enable virtualenv and install python 3rd libraries
```
$ source ./flask/bin/activate
(flask)$ pip install -r requirements.txt
```
Finally, start it
Web application configuration is stored in `config.py` file. Let's clone it from `config_template.py` file and then edit it
```
(flask)$ cp config_template.py config.py
(flask)$ vim config.py
```
```$ docker-compose -f docker-compose.dev.yml up```
Create database after having proper configs
```
(flask)% ./create_db.py
```
You can now access PowerDNS-Admin at url http://localhost:9191
Run the application and enjoy!
```
(flask)$ ./run.py
```
NOTE: For other methods to run PowerDNS-Admin, please take look at WIKI pages.
### Screenshots
![login page](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki/images/readme_screenshots/fullscreen-login.png?raw=true)

View file

@ -11,6 +11,7 @@ login_manager = LoginManager()
login_manager.init_app(app)
db = SQLAlchemy(app)
def enable_github_oauth(GITHUB_ENABLE):
if not GITHUB_ENABLE:
return None, None
@ -46,7 +47,47 @@ def enable_github_oauth(GITHUB_ENABLE):
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

28
app/decorators.py Normal file
View file

@ -0,0 +1,28 @@
from functools import wraps
from flask import g, request, redirect, url_for
from app import app
from app.models import Role
def admin_role_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user.role.name != 'Administrator':
return redirect(url_for('error', code=401))
return f(*args, **kwargs)
return decorated_function
def can_access_domain(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user.role.name != 'Administrator':
domain_name = kwargs.get('domain_name')
user_domain = [d.name for d in g.user.get_domain()]
if domain_name not in user_domain:
return redirect(url_for('error', code=401))
return f(*args, **kwargs)
return decorated_function

View file

@ -2,20 +2,21 @@ import re
import sys
import json
import requests
import urlparse
import hashlib
from app import app
from distutils.version import StrictVersion
from urllib.parse import urlparse
if 'TIMEOUT' in app.config.keys():
TIMEOUT = app.config['TIMEOUT']
else:
TIMEOUT = 10
def auth_from_url(url):
auth = None
parsed_url = urlparse.urlparse(url).netloc
parsed_url = urlparse(url).netloc
if '@' in parsed_url:
auth = parsed_url.split('@')[0].split(':')
auth = requests.auth.HTTPBasicAuth(auth[0], auth[1])
@ -55,7 +56,7 @@ def fetch_remote(remote_url, method='GET', data=None, accept=None, params=None,
if r.status_code not in (200, 400, 422):
r.raise_for_status()
except Exception as e:
raise RuntimeError("While fetching " + remote_url + ": " + str(e)), None, sys.exc_info()[2]
raise RuntimeError('Error while fetching {0}'.format(remote_url)) from e
return r
@ -72,16 +73,16 @@ def fetch_json(remote_url, method='GET', data=None, params=None, headers=None):
try:
assert('json' in r.headers['content-type'])
except Exception as e:
raise Exception("While fetching " + remote_url + ": " + str(e)), None, sys.exc_info()[2]
raise RuntimeError('Error while fetching {0}'.format(remote_url)) from e
# don't use r.json here, as it will read from r.text, which will trigger
# content encoding auto-detection in almost all cases, WHICH IS EXTREMELY
# SLOOOOOOOOOOOOOOOOOOOOOOW. just don't.
data = None
try:
data = json.loads(r.content)
except UnicodeDecodeError:
data = json.loads(r.content, 'iso-8859-1')
data = json.loads(r.content.decode('utf-8'))
except Exception as e:
raise RuntimeError('Error while loading JSON data from {0}'.format(remote_url)) from e
return data
@ -92,6 +93,7 @@ def display_record_name(data):
else:
return record_name.replace('.'+domain_name, '')
def display_master_name(data):
"""
input data: "[u'127.0.0.1', u'8.8.8.8']"
@ -99,6 +101,7 @@ def display_master_name(data):
matches = re.findall(r'\'(.+?)\'', data)
return ", ".join(matches)
def display_time(amount, units='s', remove_seconds=True):
"""
Convert timestamp to normal time format
@ -139,6 +142,7 @@ def display_time(amount, units='s', remove_seconds=True):
return final_string
def pdns_api_extended_uri(version):
"""
Check the pdns version
@ -148,14 +152,10 @@ def pdns_api_extended_uri(version):
else:
return ""
def email_to_gravatar_url(email, size=100):
def email_to_gravatar_url(email="", size=100):
"""
AD doesn't necessarily have email
"""
if not email:
email=""
hash_string = hashlib.md5(email).hexdigest()
return "https://s.gravatar.com/avatar/%s?s=%s" % (hash_string, size)
hash_string = hashlib.md5(email.encode('utf-8')).hexdigest()
return "https://s.gravatar.com/avatar/{0}?s={1}".format(hash_string, size)

View file

@ -1,33 +1,49 @@
import os
import ldap
import ldap.filter
import time
import base64
import bcrypt
import urlparse
import itertools
import traceback
import pyotp
import re
import dns.reversename
import sys
from datetime import datetime
from urllib.parse import urljoin
from distutils.util import strtobool
from distutils.version import StrictVersion
from flask_login import AnonymousUserMixin
from app import app, db
from lib import utils
from lib.log import logger
from app.lib import utils
from app.lib.log import logger
# LOG CONFIGS
logging = logger('MODEL', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config()
if 'LDAP_TYPE' in app.config.keys():
LDAP_URI = app.config['LDAP_URI']
LDAP_USERNAME = app.config['LDAP_USERNAME']
LDAP_PASSWORD = app.config['LDAP_PASSWORD']
if 'LDAP_USERNAME' in app.config.keys() and 'LDAP_PASSWORD' in app.config.keys(): #backward compatability
LDAP_BIND_TYPE = 'search'
if 'LDAP_BIND_TYPE' in app.config.keys():
LDAP_BIND_TYPE = app.config['LDAP_BIND_TYPE']
if LDAP_BIND_TYPE == 'search':
LDAP_USERNAME = app.config['LDAP_USERNAME']
LDAP_PASSWORD = app.config['LDAP_PASSWORD']
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
@ -107,10 +123,10 @@ class User(db.Model):
return str(self.id) # python 3
def __repr__(self):
return '<User %r>' % (self.username)
return '<User {0}>'.format(self.username)
def get_totp_uri(self):
return 'otpauth://totp/PowerDNS-Admin:%s?secret=%s&issuer=PowerDNS-Admin' % (self.username, self.otp_secret)
return "otpauth://totp/PowerDNS-Admin:{0}?secret={1}&issuer=PowerDNS-Admin".format(self.username, self.otp_secret)
def verify_totp(self, token):
totp = pyotp.TOTP(self.otp_secret)
@ -141,12 +157,16 @@ class User(db.Model):
try:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
l = ldap.initialize(LDAP_URI)
l.set_option(ldap.OPT_REFERRALS, 0)
l.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
l.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
l.set_option(ldap.OPT_X_TLS,ldap.OPT_X_TLS_DEMAND)
l.set_option( ldap.OPT_X_TLS_DEMAND, True )
l.set_option( ldap.OPT_DEBUG_LEVEL, 255 )
l.protocol_version = ldap.VERSION3
if LDAP_BIND_TYPE == "direct":
global LDAP_USERNAME; LDAP_USERNAME = self.username
global LDAP_PASSWORD; LDAP_PASSWORD = self.password
l.simple_bind_s(LDAP_USERNAME, LDAP_PASSWORD)
ldap_result_id = l.search(baseDN, searchScope, searchFilter, retrieveAttributes)
@ -160,7 +180,7 @@ class User(db.Model):
result_set.append(result_data)
return result_set
except ldap.LDAPError, e:
except ldap.LDAPError as e:
logging.error(e)
raise
@ -173,66 +193,88 @@ class User(db.Model):
if user_info:
if user_info.password and self.check_password(user_info.password):
logging.info('User "%s" logged in successfully' % self.username)
logging.info('User "{0}" logged in successfully'.format(self.username))
return True
logging.error('User "%s" input a wrong password' % self.username)
logging.error('User "{0}" input a wrong password'.format(self.username))
return False
logging.warning('User "%s" does not exist' % self.username)
logging.warning('User "{0}" does not exist'.format(self.username))
return False
if method == 'LDAP':
allowedlogin = False
isadmin = False
if not LDAP_TYPE:
logging.error('LDAP authentication is disabled')
return False
searchFilter = "(&(objectcategory=person)(samaccountname=%s))" % self.username
if LDAP_TYPE == 'ldap':
searchFilter = "(&(%s=%s)%s)" % (LDAP_USERNAMEFIELD, self.username, LDAP_FILTER)
logging.info('Ldap searchFilter "%s"' % searchFilter)
searchFilter = "(&({0}={1}){2})".format(LDAP_USERNAMEFIELD, self.username, LDAP_FILTER)
logging.info('Ldap searchFilter "{0}"'.format(searchFilter))
elif LDAP_TYPE == 'ad':
searchFilter = "(&(objectcategory=person)({0}={1}){2})".format(LDAP_USERNAMEFIELD, self.username, LDAP_FILTER)
result = self.ldap_search(searchFilter, LDAP_SEARCH_BASE)
if not result:
logging.warning('User "%s" does not exist' % self.username)
logging.warning('LDAP User "{0}" does not exist'.format(self.username))
return False
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
l = ldap.initialize(LDAP_URI)
l.set_option(ldap.OPT_REFERRALS, 0)
l.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
l.set_option(ldap.OPT_X_TLS,ldap.OPT_X_TLS_DEMAND)
l.set_option( ldap.OPT_X_TLS_DEMAND, True )
l.set_option( ldap.OPT_DEBUG_LEVEL, 255 )
l.protocol_version = ldap.VERSION3
try:
ldap_username = result[0][0][0]
l.simple_bind_s(ldap_username, self.password)
logging.info('User "%s" logged in successfully' % self.username)
except Exception:
logging.error('User "%s" input a wrong password' % self.username)
ldap_username = ldap.filter.escape_filter_chars(result[0][0][0])
if LDAP_GROUP_SECURITY:
try:
if LDAP_TYPE == 'ldap':
ldap_user_dn = ldap.filter.escape_filter_chars(result[0][0][0])
logging.info(result[0][0][0])
if (self.ldap_search('(member={0})'.format(ldap_user_dn) ,LDAP_ADMIN_GROUP)):
allowedlogin = True
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))
if (self.ldap_search('(member={0})'.format(ldap_user_dn) ,LDAP_USER_GROUP)):
#if (group == LDAP_USER_GROUP):
allowedlogin = True
logging.info('User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'.format(self.username,LDAP_USER_GROUP))
if allowedlogin == False:
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
except Exception as e:
logging.error('LDAP group lookup for user "{0}" has failed'.format(e))
return False
logging.info('User "{0}" logged in successfully'.format(self.username))
except Exception as e:
logging.error('User "{0}" input a wrong LDAP password'.format(e))
return False
# create user if not exist in the db
if not User.query.filter(User.username == self.username).first():
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 = result[0][0][1]['givenName'][0]
self.lastname = result[0][0][1]['sn'][0]
self.email = result[0][0][1]['mail'][0]
except Exception:
self.firstname = self.username
self.lastname = ''
self.firstname = result[0][0][1]['givenName']
self.lastname = result[0][0][1]['sn']
self.email = result[0][0][1]['mail']
except Exception as e:
logging.info("reading ldap data threw an exception {0}".format(e))
# first register user will be in Administrator role
self.role_id = Role.query.filter_by(name='User').first().id
if User.query.count() == 0:
self.role_id = Role.query.filter_by(name='Administrator').first().id
self.create_user()
logging.info('Created user "%s" in the DB' % self.username)
# user will be in Administrator role if part of LDAP Admin group
if LDAP_GROUP_SECURITY:
if isadmin == True:
self.role_id = Role.query.filter_by(name='Administrator').first().id
self.create_user()
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:
self.set_admin(isadmin)
self.update_profile()
return True
logging.error('Unsupported authentication method')
@ -258,22 +300,23 @@ class User(db.Model):
# check if username existed
user = User.query.filter(User.username == self.username).first()
if user:
return 'Username already existed'
return {'status': False, 'msg': 'Username is already in use'}
# check if email existed
user = User.query.filter(User.email == self.email).first()
if user:
return 'Email already existed'
return {'status': False, 'msg': 'Email address is already in use'}
# first register user will be in Administrator role
self.role_id = Role.query.filter_by(name='User').first().id
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)
db.session.add(self)
db.session.commit()
return True
return {'status': True, 'msg': 'Created user successfully'}
def update_profile(self, enable_otp=None):
"""
@ -303,16 +346,18 @@ class User(db.Model):
db.session.rollback()
return False
def get_domain_query(self):
return db.session.query(User, DomainUser, Domain) \
.filter(User.id == self.id) \
.filter(User.id == DomainUser.user_id) \
.filter(Domain.id == DomainUser.domain_id)
def get_domain(self):
"""
Get domains which user has permission to
access
"""
user_domains = []
query = db.session.query(User, DomainUser, Domain).filter(User.id==self.id).filter(User.id==DomainUser.user_id).filter(Domain.id==DomainUser.domain_id).all()
for q in query:
user_domains.append(q[2])
return user_domains
return [q[2] for q in self.get_domain_query()]
def delete(self):
"""
@ -327,7 +372,7 @@ class User(db.Model):
return True
except:
db.session.rollback()
logging.error('Cannot delete user %s from DB' % self.username)
logging.error('Cannot delete user {0} from DB'.format(self.username))
return False
def revoke_privilege(self):
@ -344,7 +389,7 @@ class User(db.Model):
return True
except:
db.session.rollback()
logging.error('Cannot revoke user %s privielges.' % self.username)
logging.error('Cannot revoke user {0} privielges'.format(self.username))
return False
return False
@ -390,7 +435,7 @@ class Role(db.Model):
self.description = description
def __repr__(self):
return '<Role %r>' % (self.name)
return '<Role {0}r>'.format(self.name)
class DomainSetting(db.Model):
__tablename__ = 'domain_setting'
@ -406,7 +451,7 @@ class DomainSetting(db.Model):
self.value = value
def __repr__(self):
return '<DomainSetting %r for $d>' % (setting, self.domain.name)
return '<DomainSetting {0} for {1}>'.format(setting, self.domain.name)
def __eq__(self, other):
return self.setting == other.setting
@ -444,15 +489,15 @@ class Domain(db.Model):
self.dnssec = dnssec
def __repr__(self):
return '<Domain %r>' % (self.name)
return '<Domain {0}>'.format(self.name)
def add_setting(self, setting, value):
try:
self.settings.append(DomainSetting(setting=setting, value=value))
db.session.commit()
return True
except Exception, e:
logging.error('Can not create setting %s for domain %s. %s' % (setting, self.name, str(e)))
except Exception as e:
logging.error('Can not create setting {0} for domain {1}. {2}'.format(setting, self.name, e))
return False
def get_domains(self):
@ -476,7 +521,7 @@ class Domain(db.Model):
"""
headers = {}
headers['X-API-Key'] = PDNS_API_KEY
jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers)
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers)
return jdata
def get_id_by_name(self, name):
@ -500,7 +545,7 @@ class Domain(db.Model):
headers = {}
headers['X-API-Key'] = PDNS_API_KEY
try:
jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers)
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, 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
@ -564,8 +609,8 @@ class Domain(db.Model):
except:
db.session.rollback()
return {'status': 'ok', 'msg': 'Domain table has been updated successfully'}
except Exception, e:
logging.error('Can not update domain table.' + str(e))
except Exception as e:
logging.error('Can not update domain table. Error: {0}'.format(e))
return {'status': 'error', 'msg': 'Can not update domain table'}
def add(self, domain_name, domain_type, soa_edit_api, domain_ns=[], domain_master_ips=[]):
@ -596,17 +641,16 @@ class Domain(db.Model):
}
try:
jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers, method='POST', data=post_data)
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, 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']}
else:
logging.info('Added domain %s successfully' % domain_name)
logging.info('Added domain {0} successfully'.format(domain_name))
return {'status': 'ok', 'msg': 'Added domain successfully'}
except Exception, e:
print traceback.format_exc()
logging.error('Cannot add domain %s' % domain_name)
logging.debug(str(e))
except Exception as e:
logging.error('Cannot add domain {0}'.format(domain_name))
logging.debug(traceback.print_exc())
return {'status': 'error', 'msg': 'Cannot add this domain.'}
def create_reverse_domain(self, domain_name, domain_reverse_name):
@ -629,7 +673,7 @@ class Domain(db.Model):
result = self.add(domain_reverse_name, 'Master', 'INCEPTION-INCREMENT', '', '')
self.update()
if result['status'] == 'ok':
history = History(msg='Add reverse lookup domain %s' % domain_reverse_name, detail=str({'domain_type': 'Master', 'domain_master_ips': ''}), created_by='System')
history = History(msg='Add reverse lookup domain {0}'.format(domain_reverse_name), detail=str({'domain_type': 'Master', 'domain_master_ips': ''}), created_by='System')
history.add()
else:
return {'status': 'error', 'msg': 'Adding reverse lookup domain failed'}
@ -671,13 +715,12 @@ class Domain(db.Model):
headers = {}
headers['X-API-Key'] = PDNS_API_KEY
try:
jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain_name), headers=headers, method='DELETE')
logging.info('Delete domain %s successfully' % domain_name)
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, 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, e:
print traceback.format_exc()
logging.error('Cannot delete domain %s' % domain_name)
logging.debug(str(e))
except Exception as e:
logging.error('Cannot delete domain {0}'.format(domain_name))
logging.debug(traceback.print_exc())
return {'status': 'error', 'msg': 'Cannot delete domain'}
def get_user(self):
@ -709,7 +752,7 @@ class Domain(db.Model):
db.session.commit()
except:
db.session.rollback()
logging.error('Cannot revoke user privielges on domain %s' % self.name)
logging.error('Cannot revoke user privielges on domain {0}'.format(self.name))
try:
for uid in added_ids:
@ -718,7 +761,7 @@ class Domain(db.Model):
db.session.commit()
except:
db.session.rollback()
logging.error('Cannot grant user privielges to domain %s' % self.name)
logging.error('Cannot grant user privielges to domain {0}'.format(self.name))
def update_from_master(self, domain_name):
@ -730,7 +773,7 @@ class Domain(db.Model):
headers = {}
headers['X-API-Key'] = PDNS_API_KEY
try:
jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s/axfr-retrieve' % domain), headers=headers, method='PUT')
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}/axfr-retrieve'.format(domain)), headers=headers, method='PUT')
return {'status': 'ok', 'msg': 'Update from Master successfully'}
except:
return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'}
@ -746,7 +789,7 @@ class Domain(db.Model):
headers = {}
headers['X-API-Key'] = PDNS_API_KEY
try:
jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s/cryptokeys' % domain.name), headers=headers, method='GET')
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, 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:
@ -768,7 +811,7 @@ class DomainUser(db.Model):
self.user_id = user_id
def __repr__(self):
return '<Domain_User %r %r>' % (self.domain_id, self.user_id)
return '<Domain_User {0} {1}>'.format(self.domain_id, self.user_id)
class Record(object):
@ -791,7 +834,7 @@ class Record(object):
headers = {}
headers['X-API-Key'] = PDNS_API_KEY
try:
jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers)
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, 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
@ -865,11 +908,11 @@ class Record(object):
}
try:
jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=data)
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, 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, e:
logging.error("Cannot add record %s/%s/%s to domain %s. DETAIL: %s" % (self.name, self.type, self.data, domain, str(e)))
except Exception as e:
logging.error("Cannot add record {0}/{1}/{2} to domain {3}. DETAIL: {4}".format(self.name, self.type, self.data, domain, e))
return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'}
@ -891,7 +934,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']]
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')]
# return a tuple
return deleted_records, new_records
@ -1039,12 +1082,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)))
try:
headers = {}
headers['X-API-Key'] = PDNS_API_KEY
jdata1 = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=postdata_for_delete)
jdata2 = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=postdata_for_new)
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)
if 'error' in jdata2.keys():
logging.error('Cannot apply record changes.')
@ -1054,8 +1099,8 @@ class Record(object):
self.auto_ptr(domain, new_records, deleted_records)
logging.info('Record was applied successfully.')
return {'status': 'ok', 'msg': 'Record was applied successfully'}
except Exception, e:
logging.error("Cannot apply record changes to domain %s. DETAIL: %s" % (str(e), domain))
except Exception as e:
logging.error("Cannot apply record changes to domain {0}. DETAIL: {1}".format(e, domain))
return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'}
def auto_ptr(self, domain, new_records, deleted_records):
@ -1097,7 +1142,7 @@ class Record(object):
self.delete(domain_reverse_name)
return {'status': 'ok', 'msg': 'Auto-PTR record was updated successfully'}
except Exception as e:
logging.error("Cannot update auto-ptr record changes to domain %s. DETAIL: %s" % (str(e), domain))
logging.error("Cannot update auto-ptr record changes to domain {0}. DETAIL: {1}".format(domain, e))
return {'status': 'error', 'msg': 'Auto-PTR creation failed. There was something wrong, please contact administrator.'}
def delete(self, domain):
@ -1117,19 +1162,25 @@ class Record(object):
]
}
try:
jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=data)
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, 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:
logging.error("Cannot remove record %s/%s/%s from domain %s" % (self.name, self.type, self.data, domain))
logging.error("Cannot remove record {0}/{1}/{2} from domain {3}".format(self.name, self.type, self.data, domain))
return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'}
def is_allowed(self):
def is_allowed_edit(self):
"""
Check if record is allowed to edit/removed
Check if record is allowed to edit
"""
return self.type in app.config['RECORDS_ALLOW_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')
def exists(self, domain):
"""
Check if record is present within domain records, and if it's present set self to found record
@ -1191,11 +1242,11 @@ class Record(object):
]
}
try:
jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/%s' % domain), headers=headers, method='PATCH', data=data)
logging.debug("dyndns data: " % data)
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, 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, e:
logging.error("Cannot add record %s/%s/%s to domain %s. DETAIL: %s" % (self.name, self.type, self.data, domain, str(e)))
except Exception as e:
logging.error("Cannot add record {0}/{1}/{2} to domain {3}. DETAIL: {4}".format(self.name, self.type, self.data, domain, e))
return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'}
@ -1217,7 +1268,7 @@ class Server(object):
headers['X-API-Key'] = PDNS_API_KEY
try:
jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/%s/config' % self.server_id), headers=headers, method='GET')
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/{0}/config'.format(self.server_id)), headers=headers, method='GET')
return jdata
except:
logging.error("Can not get server configuration.")
@ -1232,7 +1283,7 @@ class Server(object):
headers['X-API-Key'] = PDNS_API_KEY
try:
jdata = utils.fetch_json(urlparse.urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/%s/statistics' % self.server_id), headers=headers, method='GET')
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/{0}/statistics'.format(self.server_id)), headers=headers, method='GET')
return jdata
except:
logging.error("Can not get server statistics.")
@ -1254,7 +1305,7 @@ class History(db.Model):
self.created_by = created_by
def __repr__(self):
return '<History %r>' % (self.msg)
return '<History {0}>'.format(self.msg)
def add(self):
"""
@ -1316,7 +1367,7 @@ class Setting(db.Model):
db.session.commit()
return True
except:
logging.error('Cannot set maintenance to %s' % mode)
logging.error('Cannot set maintenance to {0}'.format(mode))
logging.debug(traceback.format_exc())
db.session.rollback()
return False
@ -1333,10 +1384,10 @@ class Setting(db.Model):
db.session.commit()
return True
else:
logging.error('Setting %s does not exist' % setting)
logging.error('Setting {0} does not exist'.format(setting))
return False
except:
logging.error('Cannot toggle setting %s' % setting)
logging.error('Cannot toggle setting {0}'.format(setting))
logging.debug(traceback.format_exec())
db.session.rollback()
return False
@ -1351,10 +1402,90 @@ class Setting(db.Model):
db.session.commit()
return True
else:
logging.error('Setting %s does not exist' % setting)
logging.error('Setting {0} does not exist'.format(setting))
return False
except:
logging.error('Cannot edit setting %s' % setting)
logging.error('Cannot edit setting {0}'.format(setting))
logging.debug(traceback.format_exec())
db.session.rollback()
return False
class DomainTemplate(db.Model):
__tablename__ = "domain_template"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), index=True, unique=True)
description = db.Column(db.String(255))
records = db.relationship('DomainTemplateRecord', back_populates='template', cascade="all, delete-orphan")
def __repr__(self):
return '<DomainTemplate {0}>'.format(self.name)
def __init__(self, name=None, description=None):
self.id = None
self.name = name
self.description = description
def replace_records(self, records):
try:
self.records = []
for record in records:
self.records.append(record)
db.session.commit()
return {'status': 'ok', 'msg': 'Template records have been modified'}
except Exception as e:
logging.error('Cannot create template records Error: {0}'.format(e))
db.session.rollback()
return {'status': 'error', 'msg': 'Can not create template records'}
def create(self):
try:
db.session.add(self)
db.session.commit()
return {'status': 'ok', 'msg': 'Template has been created'}
except Exception as e:
logging.error('Can not update domain template table. Error: {0}'.format(e))
db.session.rollback()
return {'status': 'error', 'msg': 'Can not update domain template table'}
def delete_template(self):
try:
self.records = []
db.session.delete(self)
db.session.commit()
return {'status': 'ok', 'msg': 'Template has been deleted'}
except Exception as e:
logging.error('Can not delete domain template. Error: {0}'.format(e))
db.session.rollback()
return {'status': 'error', 'msg': 'Can not delete domain template'}
class DomainTemplateRecord(db.Model):
__tablename__ = "domain_template_record"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255))
type = db.Column(db.String(64))
ttl = db.Column(db.Integer)
data = db.Column(db.String(255))
status = db.Column(db.Boolean)
template_id = db.Column(db.Integer, db.ForeignKey('domain_template.id'))
template = db.relationship('DomainTemplate', back_populates='records')
def __repr__(self):
return '<DomainTemplateRecord {0}>'.format(self.id)
def __init__(self, id=None, name=None, type=None, ttl=None, data=None, status=None):
self.id = id
self.name = name
self.type = type
self.ttl = ttl
self.data = data
self.status = status
def apply(self):
try:
db.session.commit()
except Exception as e:
logging.error('Can not update domain template table. Error: {0}'.format(e))
db.session.rollback()
return {'status': 'error', 'msg': 'Can not update domain template table'}

View file

@ -167,7 +167,7 @@ json_library = {
return r + (pEnd || '');
},
prettyPrint: function(obj) {
obj = obj.replace(/u'/g, "\'").replace(/'/g, "\"").replace(/(False|None)/g, "\"$1\"");
obj = obj.replace(/"/g, "\\\"").replace(/u'/g, "\'").replace(/'/g, "\"").replace(/(False|None)/g, "\"$1\"");
var jsonData = JSON.parse(obj);
var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
return JSON.stringify(jsonData, null, 3)
@ -175,4 +175,4 @@ json_library = {
.replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(jsonLine, json_library.replacer);
}
};
};

View file

@ -28,6 +28,14 @@
<!-- form start -->
<form role="form" method="post" action="{{ url_for('admin_createuser') }}">
<div class="box-body">
{% if error %}
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<h4><i class="icon fa fa-ban"></i> Error!</h4>
{{ error }}
</div>
<span class="help-block">{{ error }}</span>
{% endif %}
<div class="form-group has-feedback">
<label class="control-label" for="firstname">First Name</label>
<input type="text" class="form-control" placeholder="First Name"
@ -40,24 +48,18 @@
name="lastname" {% if user %}value={{ user.lastname }}{% endif %}> <span
class="glyphicon glyphicon-user form-control-feedback"></span>
</div>
<div class="form-group has-feedback {% if duplicate_email %}has-error{% endif %}">
<div class="form-group has-feedback">
<label class="control-label" for="email">E-mail address</label>
<input type="email" class="form-control" placeholder="Email"
name="email" id="email" {% if user %}value={{ user.email }}{% endif %}> <span
class="glyphicon glyphicon-envelope form-control-feedback"></span>
{% if duplicate_email %}
<span class="help-block">This e-mail address is already in use.</span>
{% endif %}
</div>
<p class="login-box-msg">Enter the account details below</p>
<div class="form-group has-feedback {% if duplicate_username %}has-error{% endif %}">
<div class="form-group has-feedback">
<label class="control-label" for="username">Username</label>
<input type="text" class="form-control" placeholder="Username"
name="username" {% if user %}value={{ user.username }}{% endif %}> <span
class="glyphicon glyphicon-user form-control-feedback"></span>
{% if duplicate_username %}
<span class="help-block">This username is already in use.</span>
{% endif %}
</div>
<div class="form-group has-feedback {% if blank_password %}has-error{% endif %}">
<label class="control-label" for="username">Password</label>

View file

@ -33,6 +33,7 @@
<th>Username</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Admin</th>
<th>Privileges</th>
<th>Deletion</th>
@ -44,6 +45,7 @@
<td>{{ user.username }}</td>
<td>{{ user.firstname }}</td>
<td>{{ user.lastname }}</td>
<td>{{ user.email }}</td>
<td>
<input type="checkbox" id="{{ user.username }}" class="admin_toggle" {% if user.role.name=='Administrator' %}checked{% endif %}>
</td>
@ -76,13 +78,24 @@
// set up user data table
$("#tbl_users").DataTable({
"paging" : true,
"lengthChange" : false,
"lengthChange" : true,
"searching" : true,
"ordering" : true,
"info" : true,
"autoWidth" : false
"info" : false,
"autoWidth" : false,
"lengthMenu": [ [10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"]],
"pageLength": 10
});
// avoid losing icheck box style when database refreshed
$('#tbl_users').on('draw.dt', function () {
$('.admin_toggle').iCheck({
handle: 'checkbox',
checkboxClass: 'icheckbox_square-blue'
});
});
// handle revocation of privileges
$(document.body).on('click', '.button_revoke', function() {
var modal = $("#modal_revoke");

View file

@ -127,6 +127,7 @@
<li><a href="{{ url_for('domain_add') }}"><i class="fa fa-plus"></i> <span>New Domain</span></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_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>

View file

@ -125,16 +125,6 @@
<div class="box-header">
<h3 class="box-title">Hosted Domains</h3>
</div>
<!-- /.box-header -->
<!--
{% if current_user.role.name == 'Administrator' %}
<div class="box-body">
<button type="button" class="btn btn-flat btn-primary" onclick="window.location.href='{{ url_for('domain_add') }}'">
New Domain&nbsp;<i class="fa fa-plus"></i>
</button>
</div>
{% endif %}
-->
<div class="box-body">
<table id="tbl_domain_list" class="table table-bordered table-striped">
<thead>
@ -144,53 +134,11 @@
<th>Type</th>
<th>Serial</th>
<th>Master</th>
<th>Action</th>
<th {% if current_user.role.name !='Administrator' %}width="6%"{% else %}width="25%"{% endif %}>Action</th>
</tr>
</thead>
<tbody>
{% for domain in domains %}
<tr>
<td>
<a href="{{ url_for('domain', domain_name=domain.name) }}"><strong>{{ domain.name }}</strong></a>
</td>
<td>
{% if domain.dnssec %}
<button type="button" class="btn btn-flat dnssec btn-success button_dnssec" id="{{ domain.name }}" style="width:100%;">
<i class="fa fa-lock"></i>&nbsp;Enabled
</button>
{% else %}
<button type="button" class="btn btn-flat dnssec button_dnssec" id="{{ domain.name }}" style="width:100%;">
<i class="fa fa-unlock-alt"></i>&nbsp;Disabled
</button>
{% endif %}
</td>
<td>
{{ domain.type }}
</td>
<td>
{% if domain.serial == 0 %}{{ domain.notified_serial }}{% else %}{{domain.serial}}{% endif %}
</td>
<td>
{% if domain.master == '[]'%}N/A {% else %}{{ domain.master|display_master_name }}{% endif %}
</td>
{% if current_user.role.name !='Administrator' %}
<td width="6%">
<button type="button" class="btn btn-flat btn-success" onclick="window.location.href='{{ url_for('domain', domain_name=domain.name) }}'">
Manage&nbsp;<i class="fa fa-cog"></i>
</button>
</td>
{% else %}
<td width="20%">
<button type="button" class="btn btn-flat btn-success" onclick="window.location.href='{{ url_for('domain', domain_name=domain.name) }}'">
Manage&nbsp;<i class="fa fa-cog"></i>
</button>
<button type="button" class="btn btn-flat btn-danger" onclick="window.location.href='{{ url_for('domain_management', domain_name=domain.name) }}'">
Admin&nbsp;<i class="fa fa-cog"></i>
</button>
</td>
{% endif %}
</tr>
{% endfor %}
<!-- Content loaded via AJAX. -->
</tbody>
</table>
</div>
@ -214,23 +162,26 @@
"ordering" : false,
"info" : false,
"autoWidth" : false
});
});
// set up domain list
$("#tbl_domain_list").DataTable({
"paging" : true,
"lengthChange" : true,
"searching" : true,
"ordering" : true,
"processing" : true,
"serverSide" : true,
"ajax" : "{{ url_for('dashboard_domains') }}",
"info" : false,
"autoWidth" : false,
{% if default_domain_table_size_setting 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"]],
{% endif %}
"pageLength": {{ default_domain_table_size_setting }}
{% if default_domain_table_size_setting 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"]],
{% endif %}
"pageLength": {{ default_domain_table_size_setting }}
});
$(document.body).on('click', '.history-info-button', function() {
var modal = $("#modal_history_info");
@ -238,9 +189,30 @@
$('#modal-code-content').html(json_library.prettyPrint(info));
modal.modal('show');
});
$(document.body).on("click", ".button_dnssec", function() {
$(document.body).on("click", ".button_template", function (e) {
var modal = $("#modal_template");
var domain = $(this).prop('id');
getdnssec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec');
var form = " <label for=\"template_name\">Template name</label> \
<input type=\"text\" class=\"form-control\" name=\"template_name\" id=\"template_name\" placeholder=\"Enter a valid template name (required)\"> \
<label for=\"template_description\">Template description</label> \
<input type=\"text\" class=\"form-control\" name=\"template_description\" id=\"template_description\" placeholder=\"Enter a template description (optional)\"> \
<input id=\"domain\" name=\"domain\" type=\"hidden\" value=\""+domain+"\"> \
";
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
var data = {};
data['name'] = modal.find('#template_name').val();
data['description'] = modal.find('#template_description').val();
data['domain'] = modal.find('#domain').val();
applyChanges(data, $SCRIPT_ROOT + "{{ url_for('create_template_from_zone') }}", true);
modal.modal('hide');
})
modal.find('#button_close').click(function() {
modal.modal('hide');
})
modal.modal('show');
});
</script>
{% endblock %}
@ -267,8 +239,7 @@
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
<div class="modal fade" id="modal_dnssec_info">
<div class="modal fade modal-primary" id="modal_template">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@ -276,19 +247,19 @@
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">DNSSEC</h4>
<h4 class="modal-title">Clone to template</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-right"
data-dismiss="modal">Close</button>
<button type="button" class="btn btn-flat btn-default pull-left"
id="button_close" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-flat btn-primary" id="button_save">Save</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
{% endblock %}

View file

@ -0,0 +1,45 @@
{% macro name(domain) %}
<a href="{{ url_for('domain', domain_name=domain.name) }}"><strong>{{ domain.name }}</strong></a>
{% endmacro %}
{% macro dnssec(domain) %}
{% if domain.dnssec %}
<td><span class="label label-success"><i class="fa fa-lock-alt"></i>&nbsp;Enabled</span></td>
{% else %}
<td><span class="label label-primary"><i class="fa fa-unlock-alt"></i>&nbsp;Disabled</span></td>
{% endif %}
{% endmacro %}
{% macro type(domain) %}
{{ domain.type }}
{% endmacro %}
{% macro serial(domain) %}
{% if domain.serial == 0 %}{{ domain.notified_serial }}{% else %}{{domain.serial}}{% endif %}
{% endmacro %}
{% macro master(domain) %}
{% if domain.master == '[]'%}N/A{% else %}{{ domain.master|display_master_name }}{% endif %}
{% endmacro %}
{% macro actions(domain) %}
{% if current_user.role.name =='Administrator' %}
<td width="25%">
<button type="button" class="btn btn-flat btn-success button_template" id="{{ domain.name }}">
Template&nbsp;<i class="fa fa-clone"></i>
</button>
<button type="button" class="btn btn-flat btn-success" onclick="window.location.href='{{ url_for('domain', domain_name=domain.name) }}'">
Manage&nbsp;<i class="fa fa-cog"></i>
</button>
<button type="button" class="btn btn-flat btn-danger" onclick="window.location.href='{{ url_for('domain_management', domain_name=domain.name) }}'">
Admin&nbsp;<i class="fa fa-cog"></i>
</button>
</td>
{% else %}
<td width="6%">
<button type="button" class="btn btn-flat btn-success" onclick="window.location.href='{{ url_for('domain', domain_name=domain.name) }}'">
Manage&nbsp;<i class="fa fa-cog"></i>
</button>
</td>
{% endif %}
{% endmacro %}

View file

@ -1,10 +1,10 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - DOMAIN</title>{% endblock %}
{% block title %}<title>{{ domain.name }} - DNS Control Panel</title>{% endblock %}
{% block dashboard_stat %}
<section class="content-header">
<h1>
Manage domain <small>{{ domain.name }}</small>
Manage domain: <b>{{ domain.name }}</b>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i
@ -20,9 +20,6 @@
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Manage Records for {{ domain.name }}</h3>
</div>
<div class="box-body">
{% if domain.type != 'Slave' %}
<button type="button" class="btn btn-flat btn-primary pull-left button_add_record" id="{{ domain.name }}">
@ -70,25 +67,23 @@
</td>
{% if domain.type != 'Slave' %}
<td width="6%">
{% if record.is_allowed() %}
{% if record.is_allowed_edit() %}
<button type="button" class="btn btn-flat btn-warning button_edit" id="{{ (record.name,domain.name)|display_record_name }}">Edit&nbsp;<i class="fa fa-edit"></i></button>
{% else %}
<button type="button" class="btn btn-flat btn-warning"">&nbsp;&nbsp;<i class="fa fa-exclamation-circle"></i>&nbsp;&nbsp;</button>
{% endif %}
</td>
<td width="6%">
{% if record.is_allowed() %}
{% if record.is_allowed_delete() %}
<button type="button" class="btn btn-flat btn-danger button_delete" id="{{ (record.name,domain.name)|display_record_name }}">Delete&nbsp;<i class="fa fa-trash"></i></button>
{% else %}
<button type="button" class="btn btn-flat btn-warning"">&nbsp;&nbsp;<i class="fa fa-exclamation-circle"></i>&nbsp;&nbsp;</button>
{% endif %}
{% else %}
<td width="6%">
<button type="button" class="btn btn-flat btn-warning"">&nbsp;&nbsp;<i class="fa fa-exclamation-circle"></i>&nbsp;&nbsp;</button>
<button type="button" class="btn btn-flat btn-warning">&nbsp;&nbsp;<i class="fa fa-exclamation-circle"></i>&nbsp;&nbsp;</button>
</td>
<td width="6%">
<button type="button" class="btn btn-flat btn-warning"">&nbsp;&nbsp;<i class="fa fa-exclamation-circle"></i>&nbsp;&nbsp;</button>
</td>
<button type="button" class="btn btn-flat btn-warning">&nbsp;&nbsp;<i class="fa fa-exclamation-circle"></i>&nbsp;&nbsp;</button>
</td>
{% endif %}
</td>
<!-- hidden column that we can sort on -->
@ -265,7 +260,37 @@
$(document.body).on("focus", "#current_edit_record_data", function (e) {
var record_type = $(this).parents("tr").find('#record_type').val();
var record_data = $(this);
if (record_type == "MX") {
if (record_type == "CAA") {
var modal = $("#modal_custom_record");
if (record_data.val() == "") {
var form = " <label for=\"caa_flag\">CAA Flag</label> \
<input type=\"text\" class=\"form-control\" name=\"caa_flag\" id=\"caa_flag\" placeholder=\"0\"> \
<label for=\"caa_tag\">CAA Tag</label> \
<input type=\"text\" class=\"form-control\" name=\"caa_tag\" id=\"caa_tag\" placeholder=\"issue\"> \
<label for=\"caa_value\">CAA Value</label> \
<input type=\"text\" class=\"form-control\" name=\"caa_value\" id=\"caa_value\" placeholder=\"eg. letsencrypt.org\"> \
";
} else {
var parts = record_data.val().split(" ");
var form = " <label for=\"caa_flag\">CAA Flag</label> \
<input type=\"text\" class=\"form-control\" name=\"caa_flag\" id=\"caa_flag\" placeholder=\"0\" value=\"" + parts[0] + "\"> \
<label for=\"caa_tag\">CAA Tag</label> \
<input type=\"text\" class=\"form-control\" name=\"caa_tag\" id=\"caa_tag\" placeholder=\"issue\" value=\"" + parts[1] + "\"> \
<label for=\"caa_value\">CAA Value</label> \
<input type=\"text\" class=\"form-control\" name=\"caa_value\" id=\"caa_value\" placeholder=\"eg. letsencrypt.org\" value=\"" + parts[2] + "\"> \
";
}
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
caa_flag = modal.find('#caa_flag').val();
caa_tag = modal.find('#caa_tag').val();
caa_value = modal.find('#caa_value').val();
data = caa_flag + " " + caa_tag + " " + '"' + caa_value + '"';
record_data.val(data);
modal.modal('hide');
})
modal.modal('show');
} else if (record_type == "MX") {
var modal = $("#modal_custom_record");
if (record_data.val() == "") {
var form = " <label for=\"mx_priority\">MX Priority</label> \
@ -324,6 +349,7 @@
record_data.val(data);
modal.modal('hide');
})
modal.modal('show');
} else if (record_type == "SOA") {
var modal = $("#modal_custom_record");
if (record_data.val() == "") {

View file

@ -47,6 +47,15 @@
</label>
</div>
</div>
<div class="form-group">
<label>Select a template</label>
<select class="form-control" id="domain_template" name="domain_template">
<option value="0">No template</option>
{% for template in templates %}
<option value="{{ template.id }}">{{ template.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group" style="display: none;" id="domain_master_address_div">
<input type="text" class="form-control" name="domain_master_address" id="domain_master_address" placeholder="Enter valid master ip addresses (separated by commas)">
</div>
@ -93,7 +102,7 @@
<div class="box-footer">
<button type="submit" class="btn btn-flat btn-primary">Submit</button>
<button type="submit" class="btn btn-flat btn-default" onclick="window.location.href='{{ url_for('dashboard') }}'">Cancel</button>
<button type="button" class="btn btn-flat btn-default" onclick="window.location.href='{{ url_for('dashboard') }}'">Cancel</button>
</div>
</form>
</div>

View file

@ -98,6 +98,9 @@
<!-- /.col -->
</div>
</form>
{% if google_enabled %}
<a href="{{ url_for('google_login') }}">Google oauth login</a>
{% endif %}
{% if github_enabled %}
<a href="{{ url_for('github_login') }}">Github oauth login</a>
{% endif %}

116
app/templates/template.html Normal file
View file

@ -0,0 +1,116 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - Templates</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
Templates
<small>List</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('templates') }}"><i class="fa fa-dashboard"></i> Templates</a></li>
<li class="active">List</li>
</ol>
</section>
{% endblock %}
{% block content %}
<!-- Main content -->
<section class="content">
{% with errors = get_flashed_messages(category_filter=["error"]) %} {% if errors %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert"
aria-hidden="true">&times;</button>
<h4>
<i class="icon fa fa-ban"></i> Error!
</h4>
<div class="alert-message block-message error">
<a class="close" href="#">x</a>
<ul>
{%- for msg in errors %}
<li>{{ msg }}</li> {% endfor -%}
</ul>
</div>
</div>
</div>
</div>
{% endif %} {% endwith %}
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Templates</h3>
</div>
<div class="box-body">
<a href="{{ url_for('create_template') }}">
<button type="button" class="btn btn-flat btn-primary pull-left">
Create Template&nbsp;<i class="fa fa-plus"></i>
</button>
</a>
</div>
<div class="box-body">
<table id="tbl_template_list" class="table table-bordered table-striped">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Number of Records</th>
<th width="20%">Action</th>
</tr>
</thead>
<tbody>
{% for template in templates %}
<tr>
<td>
<a href="{{ url_for('edit_template', template=template.name) }}"><strong>{{ template.name }}</strong></a>
</td>
<td>
{{ template.description }}
</td>
<td>
{{ template.records|count }}
</td>
<td>
<a href="{{ url_for('edit_template', template=template.name) }}">
<button type="button" class="btn btn-flat btn-warning button_edit" id="">
Edit&nbsp;<i class="fa fa-edit"></i>
</button>
</a>
<a href="{{ url_for('delete_template', template=template.name) }}">
<button type="button" class="btn btn-flat btn-danger button_delete" id="">
Delete&nbsp;<i class="fa fa-trash"></i>
</button>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</section>
<!-- /.content -->
{% endblock %}
{% block extrascripts %}
<script>
// set up history data table
$("#tbl_template_list").DataTable({
"paging" : true,
"lengthChange" : true,
"searching" : true,
"ordering" : true,
"info" : false,
"autoWidth" : false
});
</script>
{% endblock %}
{% block modals %}
{% endblock %}

View file

@ -0,0 +1,104 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - Create Template</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
Template
<small>Create</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('templates') }}"><i class="fa fa-dashboard"></i> Templates</a></li>
<li class="active">Create</li>
</ol>
</section>
{% endblock %}
{% block content %}
<section class="content">
{% with errors = get_flashed_messages(category_filter=["error"]) %} {%
if errors %}
<div class="row">
<div class="col-md-12">
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert"
aria-hidden="true">&times;</button>
<h4>
<i class="icon fa fa-ban"></i> Error!
</h4>
<div class="alert-message block-message error">
<a class="close" href="#">x</a>
<ul>
{%- for msg in errors %}
<li>{{ msg }}</li> {% endfor -%}
</ul>
</div>
</div>
</div>
</div>
{% endif %} {% endwith %}
<div class="row">
<div class="col-md-4">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Create new template</h3>
</div>
<!-- /.box-header -->
<!-- form start -->
<form role="form" method="post"
action="{{ url_for('create_template') }}">
<div class="box-body">
<div class="form-group">
<input type="text" class="form-control" name="name" id="name"
placeholder="Enter a valid template name (required)">
</div>
<div class="form-group">
<input type="text" class="form-control" name="description"
id="description"
placeholder="Enter a template description (optional)">
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-flat btn-primary">Submit</button>
<button type="button" class="btn btn-flat btn-default"
onclick="window.location.href='{{ url_for('templates') }}'">Cancel</button>
</div>
</form>
</div>
<!-- /.box -->
</div>
<div class="col-md-8">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Help with creating a new template</h3>
</div>
<div class="box-body">
<dl class="dl-horizontal">
<dt>Template name</dt>
<dd>Enter your template name, this is the name of the template that
will be shown to users. The name should not have any spaces but
can have symbols.</dd>
<dt>Template description</dt>
<dd>Enter your template description, this is to help better
identify the template.</dd>
</dl>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extrascripts %}
<script>
$("input[name=radio_type]").change(function() {
var type = $(this).val();
if (type == "slave") {
$("#domain_master_address_div").show();
} else {
$("#domain_master_address_div").hide();
}
});
</script>
{% endblock %}

View file

@ -0,0 +1,415 @@
{% extends "base.html" %}
{% block title %}<title>DNS Control Panel - Edit Template</title>{% endblock %}
{% block dashboard_stat %}
<section class="content-header">
<h1>
Edit template <small>{{ template }}</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i
class="fa fa-dashboard"></i> Home</a></li>
<li>Templates</li>
<li class="active">{{ template }}</li>
</ol>
</section>
{% endblock %}
{% block content %}
<section class="content">
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Manage Template Records for {{ template }}</h3>
</div>
<div class="box-body">
<button type="button" class="btn btn-flat btn-primary pull-left button_add_record" id="{{ template }}">
Add Record&nbsp;<i class="fa fa-plus"></i>
</button>
<button type="button" class="btn btn-flat btn-primary pull-right button_apply_changes" id="{{ template }}">
Apply Changes&nbsp;<i class="fa fa-floppy-o"></i>
</button>
</div>
<div class="box-body">
<table id="tbl_records" class="table table-bordered table-striped">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Status</th>
<th>TTL</th>
<th>Data</th>
<th>Edit</th>
<th>Delete</th>
<th>ID</th>
</tr>
</thead>
<tbody>
{% for record in records %}
<tr class="odd row_record" id="{{ record.name }}">
<td>
{{ record.name }}
</td>
<td>
{{ record.type }}
</td>
<td>
{{ record.status }}
</td>
<td>
{{ record.ttl }}
</td>
<td class="length-break">
{{ record.data }}
</td>
<td width="6%">
<button type="button" class="btn btn-flat btn-warning button_edit" id="{{ record.name }}">
Edit&nbsp;<i class="fa fa-edit"></i>
</button>
</td>
<td width="6%">
<button type="button" class="btn btn-flat btn-danger button_delete" id="{{ record.name }}">
Delete&nbsp;<i class="fa fa-trash"></i>
</button>
</td>
<td>
{{ record.id }}
</td>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extrascripts %}
<script>
// superglobals
window.records_allow_edit = {{ editable_records|tojson }};
window.nEditing = null;
window.nNew = false;
// set up user data table
$("#tbl_records").DataTable({
"paging" : true,
"lengthChange" : true,
"searching" : true,
"ordering" : true,
"info" : true,
"autoWidth" : false,
{% if default_record_table_size_setting 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"]],
{% endif %}
"pageLength": {{ default_record_table_size_setting }},
"language": {
"lengthMenu": " _MENU_ records"
},
"retrieve" : true,
"columnDefs": [{
"targets": [ 7 ],
"visible": false,
"searchable": false
}]
});
// handle delete button
$(document.body).on("click", ".button_delete", function(e) {
e.stopPropagation();
var modal = $("#modal_delete");
var table = $("#tbl_records").DataTable();
var record = $(this).prop('id');
var nRow = $(this).parents('tr')[0];
var info = "Are you sure you want to delete " + record + "?";
modal.find('.modal-body p').text(info);
modal.find('#button_delete_confirm').click(function() {
table.row(nRow).remove().draw();
modal.modal('hide');
})
modal.modal('show');
});
// handle edit button
$(document.body).on("click", ".button_edit, .row_record", function(e) {
e.stopPropagation();
if ($(this).is('tr')) {
var nRow = $(this)[0];
} else {
var nRow = $(this).parents('tr')[0];
}
var table = $("#tbl_records").DataTable();
if (nEditing == nRow) {
/* click on row already being edited, do nothing */
} else if (nEditing !== null && nEditing != nRow && nNew == false) {
/* Currently editing - but not this row - restore the old before continuing to edit mode */
restoreRow(table, nEditing);
editRow(table, nRow);
nEditing = nRow;
} else if (nNew == true) {
/* adding a new row, delete it and start editing */
table.row(nEditing).remove().draw();
nNew = false;
editRow(table, nRow);
nEditing = nRow;
} else {
/* No edit in progress - let's start one */
editRow(table, nRow);
nEditing = nRow;
}
});
// handle apply changes button
$(document.body).on("click",".button_apply_changes", function() {
var modal = $("#modal_apply_changes");
var table = $("#tbl_records").DataTable();
var template = $(this).prop('id');
var info = "Are you sure you want to apply your changes?";
modal.find('.modal-body p').text(info);
modal.find('#button_apply_confirm').click(function() {
var data = getTableData(table);
applyChanges(data, '/template/' + template + '/apply', true);
modal.modal('hide');
})
modal.modal('show');
});
// handle add record button
$(document.body).on("click", ".button_add_record", function (e) {
if (nNew || nEditing) {
// TODO: replace this alert with modal
alert("Previous record not saved. Please save it before adding more record.")
return;
}
var table = $("#tbl_records").DataTable();
var aiNew = table.row.add(['', 'A', 'Active', 3600, '', '', '', '']).draw();
var nRow = aiNew.index();
editRow(table, nRow);
nEditing = nRow;
nNew = true;
});
//handle cancel button
$(document.body).on("click", ".button_cancel", function (e) {
e.stopPropagation();
var oTable = $("#tbl_records").DataTable();
if (nNew) {
oTable.row(nEditing).remove().draw();
nEditing = null;
nNew = false;
} else {
restoreRow(oTable, nEditing);
nEditing = null;
}
});
//handle save button
$(document.body).on("click", ".button_save", function (e) {
e.stopPropagation();
var table = $("#tbl_records").DataTable();
saveRow(table, nEditing);
nEditing = null;
nNew = false;
});
{% if record_helper_setting %}
//handle wacky record types
$(document.body).on("focus", "#current_edit_record_data", function (e) {
var record_type = $(this).parents("tr").find('#record_type').val();
var record_data = $(this);
if (record_type == "MX") {
var modal = $("#modal_custom_record");
if (record_data.val() == "") {
var form = " <label for=\"mx_priority\">MX Priority</label> \
<input type=\"text\" class=\"form-control\" name=\"mx_priority\" id=\"mx_priority\" placeholder=\"10\"> \
<label for=\"mx_server\">MX Server</label> \
<input type=\"text\" class=\"form-control\" name=\"mx_server\" id=\"mx_server\" placeholder=\"postfix.example.com\"> \
";
} else {
var parts = record_data.val().split(" ");
var form = " <label for=\"mx_priority\">MX Priority</label> \
<input type=\"text\" class=\"form-control\" name=\"mx_priority\" id=\"mx_priority\" placeholder=\"10\" value=\"" + parts[0] + "\"> \
<label for=\"mx_server\">MX Server</label> \
<input type=\"text\" class=\"form-control\" name=\"mx_server\" id=\"mx_server\" placeholder=\"postfix.example.com\" value=\"" + parts[1] + "\"> \
";
}
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
mx_server = modal.find('#mx_server').val();
mx_priority = modal.find('#mx_priority').val();
data = mx_priority + " " + mx_server;
record_data.val(data);
modal.modal('hide');
})
modal.modal('show');
} else if (record_type == "SRV") {
var modal = $("#modal_custom_record");
if (record_data.val() == "") {
var form = " <label for=\"srv_priority\">SRV Priority</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_priority\" id=\"srv_priority\" placeholder=\"0\"> \
<label for=\"srv_weight\">SRV Weight</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_weight\" id=\"srv_weight\" placeholder=\"10\"> \
<label for=\"srv_port\">SRV Port</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_port\" id=\"srv_port\" placeholder=\"5060\"> \
<label for=\"srv_target\">SRV Target</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_target\" id=\"srv_target\" placeholder=\"sip.example.com\"> \
";
} else {
var parts = record_data.val().split(" ");
var form = " <label for=\"srv_priority\">SRV Priority</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_priority\" id=\"srv_priority\" placeholder=\"0\" value=\"" + parts[0] + "\"> \
<label for=\"srv_weight\">SRV Weight</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_weight\" id=\"srv_weight\" placeholder=\"10\" value=\"" + parts[1] + "\"> \
<label for=\"srv_port\">SRV Port</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_port\" id=\"srv_port\" placeholder=\"5060\" value=\"" + parts[2] + "\"> \
<label for=\"srv_target\">SRV Target</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_target\" id=\"srv_target\" placeholder=\"sip.example.com\" value=\"" + parts[3] + "\"> \
";
}
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
srv_priority = modal.find('#srv_priority').val();
srv_weight = modal.find('#srv_weight').val();
srv_port = modal.find('#srv_port').val();
srv_target = modal.find('#srv_target').val();
data = srv_priority + " " + srv_weight + " " + srv_port + " " + srv_target;
record_data.val(data);
modal.modal('hide');
})
modal.modal('show');
} else if (record_type == "SOA") {
var modal = $("#modal_custom_record");
if (record_data.val() == "") {
var form = " <label for=\"soa_primaryns\">Primary Name Server</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_primaryns\" id=\"soa_primaryns\" placeholder=\"ns1.example.com\"> \
<label for=\"soa_adminemail\">Primary Contact</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_adminemail\" id=\"soa_adminemail\" placeholder=\"admin.example.com\"> \
<label for=\"soa_serial\">Serial</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_serial\" id=\"soa_serial\" placeholder=\"2016010101\"> \
<label for=\"soa_zonerefresh\">Zone refresh timer</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_zonerefresh\" id=\"soa_zonerefresh\" placeholder=\"86400\"> \
<label for=\"soa_failedzonerefresh\">Failed refresh retry timer</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_failedzonerefresh\" id=\"soa_failedzonerefresh\" placeholder=\"7200\"> \
<label for=\"soa_zoneexpiry\">Zone expiry timer</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_zoneexpiry\" id=\"soa_zoneexpiry\" placeholder=\"604800\"> \
<label for=\"soa_minimumttl\">Minimum TTL</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_minimumttl\" id=\"soa_minimumttl\" placeholder=\"300\"> \
";
} else {
var parts = record_data.val().split(" ");
var form = " <label for=\"soa_primaryns\">Primary Name Server</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_primaryns\" id=\"soa_primaryns\" value=\"" + parts[0] + "\"> \
<label for=\"soa_adminemail\">Primary Contact</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_adminemail\" id=\"soa_adminemail\" value=\"" + parts[1] + "\"> \
<label for=\"soa_serial\">Serial</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_serial\" id=\"soa_serial\" value=\"" + parts[2] + "\"> \
<label for=\"soa_zonerefresh\">Zone refresh timer</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_zonerefresh\" id=\"soa_zonerefresh\" value=\"" + parts[3] + "\"> \
<label for=\"soa_failedzonerefresh\">Failed refresh retry timer</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_failedzonerefresh\" id=\"soa_failedzonerefresh\" value=\"" + parts[4] + "\"> \
<label for=\"soa_zoneexpiry\">Zone expiry timer</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_zoneexpiry\" id=\"soa_zoneexpiry\" value=\"" + parts[5] + "\"> \
<label for=\"soa_minimumttl\">Minimum TTL</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_minimumttl\" id=\"soa_minimumttl\" value=\"" + parts[6] + "\"> \
";
}
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
soa_primaryns = modal.find('#soa_primaryns').val();
soa_adminemail = modal.find('#soa_adminemail').val();
soa_serial = modal.find('#soa_serial').val();
soa_zonerefresh = modal.find('#soa_zonerefresh').val();
soa_failedzonerefresh = modal.find('#soa_failedzonerefresh').val();
soa_zoneexpiry = modal.find('#soa_zoneexpiry').val();
soa_minimumttl = modal.find('#soa_minimumttl').val();
data = soa_primaryns + " " + soa_adminemail + " " + soa_serial + " " + soa_zonerefresh + " " + soa_failedzonerefresh + " " + soa_zoneexpiry + " " + soa_minimumttl;
record_data.val(data);
modal.modal('hide');
})
modal.modal('show');
}
});
{% endif %}
</script>
{% endblock %}
{% block modals %}
<div class="modal fade modal-warning" id="modal_delete">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Confirmation</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-left"
data-dismiss="modal">Close</button>
<button type="button" class="btn btn-flat btn-danger" id="button_delete_confirm">Delete</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<div class="modal fade modal-primary" id="modal_apply_changes">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Confirmation</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-left"
data-dismiss="modal">Close</button>
<button type="button" class="btn btn-flat btn-primary" id="button_apply_confirm">Apply</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<div class="modal fade modal-primary" id="modal_custom_record">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Custom Record</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-primary" id="button_save">Save</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
{% endblock %}

View file

@ -165,7 +165,7 @@
'enable_otp' : enable_otp
}
};
applyChanges(postdata, $SCRIPT_ROOT + '/user/profile');
applyChanges(postdata, $SCRIPT_ROOT + '/user/profile', false, true);
location.reload();
});
</script>

View file

@ -11,16 +11,21 @@ from io import BytesIO
import jinja2
import qrcode as qrc
import qrcode.image.svg as qrc_svg
from flask import g, request, make_response, jsonify, render_template, session, redirect, url_for, send_from_directory, abort
from flask import g, request, make_response, jsonify, render_template, session, redirect, url_for, send_from_directory, abort, flash
from flask_login import login_user, logout_user, current_user, login_required
from werkzeug import secure_filename
from werkzeug.security import gen_salt
from .models import User, Domain, Record, Server, History, Anonymous, Setting, DomainSetting
from app import app, login_manager, github
from lib import utils
from .models import User, Domain, Record, Server, History, Anonymous, Setting, DomainSetting, DomainTemplate, DomainTemplateRecord
from app import app, login_manager, github, google
from app.lib import utils
from app.lib.log import logger
from app.decorators import admin_role_required, can_access_domain
# LOG CONFIG
logging = logger('MODEL', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config()
# FILTERS
jinja2.filters.FILTERS['display_record_name'] = utils.display_record_name
jinja2.filters.FILTERS['display_master_name'] = utils.display_master_name
jinja2.filters.FILTERS['display_second_to_time'] = utils.display_time
@ -34,35 +39,45 @@ if StrictVersion(PDNS_VERSION) >= StrictVersion('4.0.0'):
else:
NEW_SCHEMA = False
@app.context_processor
def inject_fullscreen_layout_setting():
fullscreen_layout_setting = Setting.query.filter(Setting.name == 'fullscreen_layout').first()
return dict(fullscreen_layout_setting=strtobool(fullscreen_layout_setting.value))
@app.context_processor
def inject_record_helper_setting():
record_helper_setting = Setting.query.filter(Setting.name == 'record_helper').first()
return dict(record_helper_setting=strtobool(record_helper_setting.value))
@app.context_processor
def inject_login_ldap_first_setting():
login_ldap_first_setting = Setting.query.filter(Setting.name == 'login_ldap_first').first()
return dict(login_ldap_first_setting=strtobool(login_ldap_first_setting.value))
@app.context_processor
def inject_default_record_table_size_setting():
default_record_table_size_setting = Setting.query.filter(Setting.name == 'default_record_table_size').first()
return dict(default_record_table_size_setting=default_record_table_size_setting.value)
@app.context_processor
def inject_default_domain_table_size_setting():
default_domain_table_size_setting = Setting.query.filter(Setting.name == 'default_domain_table_size').first()
return dict(default_domain_table_size_setting=default_domain_table_size_setting.value)
@app.context_processor
def inject_auto_ptr_setting():
auto_ptr_setting = Setting.query.filter(Setting.name == 'auto_ptr').first()
return dict(auto_ptr_setting=strtobool(auto_ptr_setting.value))
if auto_ptr_setting is None:
return dict(auto_ptr_setting=False)
else:
return dict(auto_ptr_setting=strtobool(auto_ptr_setting.value))
# START USER AUTHENTICATION HANDLER
@app.before_request
@ -92,6 +107,7 @@ def dyndns_login_required(f):
return f(*args, **kwargs)
return decorated_function
@login_manager.request_loader
def login_via_authorization_header(request):
auth_header = request.headers.get('Authorization')
@ -100,8 +116,7 @@ def login_via_authorization_header(request):
try:
auth_header = base64.b64decode(auth_header)
username,password = auth_header.split(":")
except TypeError, e:
error = e.message['desc'] if 'desc' in e.message else e
except TypeError as e:
return None
user = User(username=username, password=password, plain_text_password=password)
try:
@ -111,47 +126,42 @@ def login_via_authorization_header(request):
else:
login_user(user, remember = False)
return user
except Exception, e:
except:
return None
return None
# END USER AUTHENTICATION HANDLER
# START CUSTOMIZE DECORATOR
def admin_role_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user.role.name != 'Administrator':
return redirect(url_for('error', code=401))
return f(*args, **kwargs)
return decorated_function
# END CUSTOMIZE DECORATOR
# START VIEWS
@app.errorhandler(400)
def http_bad_request(e):
return redirect(url_for('error', code=400))
@app.errorhandler(401)
def http_unauthorized(e):
return redirect(url_for('error', code=401))
@app.errorhandler(404)
def http_internal_server_error(e):
return redirect(url_for('error', code=404))
@app.errorhandler(500)
def http_page_not_found(e):
return redirect(url_for('error', code=500))
@app.route('/error/<string:code>')
@app.route('/error/<path:code>')
def error(code, msg=None):
supported_code = ('400', '401', '404', '500')
if code in supported_code:
return render_template('errors/%s.html' % code, msg=msg), int(code)
return render_template('errors/{0}.html'.format(code), msg=msg), int(code)
else:
return render_template('errors/404.html'), 404
@app.route('/register', methods=['GET'])
def register():
SIGNUP_ENABLED = app.config['SIGNUP_ENABLED']
@ -160,25 +170,57 @@ def register():
else:
return render_template('errors/404.html'), 404
@app.route('/google/login')
def google_login():
if not app.config.get('GOOGLE_OAUTH_ENABLE'):
return abort(400)
return google.authorize(callback=url_for('authorized', _external=True))
@app.route('/github/login')
def github_login():
if not app.config.get('GITHUB_OAUTH_ENABLE'):
return abort(400)
return github.authorize(callback=url_for('authorized', _external=True))
@app.route('/login', methods=['GET', 'POST'])
@login_manager.unauthorized_handler
def login():
# these parameters will be needed in multiple paths
LDAP_ENABLED = True if 'LDAP_TYPE' in app.config.keys() else False
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_ENABLE = app.config.get('LDAP_ENABLE')
GITHUB_ENABLE = app.config.get('GITHUB_OAUTH_ENABLE')
GOOGLE_ENABLE = app.config.get('GOOGLE_OAUTH_ENABLE')
if g.user is not None and current_user.is_authenticated:
return redirect(url_for('dashboard'))
if 'google_token' in session:
user_data = google.get('userinfo').data
first_name = user_data['given_name']
surname = user_data['family_name']
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,
plain_text_password=gen_salt(7),
email=email)
result = user.create_local_user()
if not result['status']:
session.pop('google_token', None)
return redirect(url_for('login'))
session['user_id'] = user.id
login_user(user, remember = False)
return redirect(url_for('index'))
if 'github_token' in session:
me = github.get('user')
user_info = me.data
@ -188,7 +230,11 @@ def login():
user = User(username=user_info['name'],
plain_text_password=gen_salt(7),
email=user_info['email'])
user.create_local_user()
result = user.create_local_user()
if not result['status']:
session.pop('github_token', None)
return redirect(url_for('login'))
session['user_id'] = user.id
login_user(user, remember = False)
@ -197,7 +243,8 @@ def login():
if request.method == 'GET':
return render_template('login.html',
github_enabled=GITHUB_ENABLE,
ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE,
google_enabled=GOOGLE_ENABLE,
ldap_enabled=LDAP_ENABLE, login_title=LOGIN_TITLE,
basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
# process login
@ -223,19 +270,18 @@ def login():
try:
auth = user.is_validate(method=auth_method)
if auth == False:
return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
except Exception, e:
error = e.message['desc'] if 'desc' in e.message else e
return render_template('login.html', error=error, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLE, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
except Exception as e:
return render_template('login.html', error=e, ldap_enabled=LDAP_ENABLE, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
# check if user enabled OPT authentication
if user.otp_secret:
if otp_token:
good_token = user.verify_totp(otp_token)
if not good_token:
return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLE, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
else:
return render_template('login.html', error='Token required', ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
return render_template('login.html', error='Token required', ldap_enabled=LDAP_ENABLE, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
login_user(user, remember = remember_me)
return redirect(request.args.get('next') or url_for('index'))
@ -251,18 +297,19 @@ def login():
try:
result = user.create_local_user()
if result == True:
return render_template('login.html', username=username, password=password, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
if result['status'] == True:
return render_template('login.html', username=username, password=password, ldap_enabled=LDAP_ENABLE, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
else:
return render_template('register.html', error=result)
except Exception, e:
error = e.message['desc'] if 'desc' in e.message else e
return render_template('register.html', error=error)
return render_template('register.html', error=result['msg'])
except Exception as e:
return render_template('register.html', error=e)
@app.route('/logout')
def logout():
session.pop('user_id', None)
session.pop('github_token', None)
session.pop('google_token', None)
logout_user()
return redirect(url_for('login'))
@ -271,10 +318,6 @@ def logout():
@login_required
def dashboard():
d = Domain().update()
if current_user.role.name == 'Administrator':
domains = Domain.query.all()
else:
domains = User(id=current_user.id).get_domain()
# stats for dashboard
domain_count = Domain.query.count()
@ -284,15 +327,83 @@ def dashboard():
server = Server(server_id='localhost')
statistics = server.get_statistic()
if statistics:
uptime = filter(lambda uptime: uptime['name'] == 'uptime', statistics)[0]['value']
uptime = list([uptime for uptime in statistics if uptime['name'] == 'uptime'])[0]['value']
else:
uptime = 0
return render_template('dashboard.html', domains=domains, domain_count=domain_count, users=users, history_number=history_number, uptime=uptime, histories=history)
return render_template('dashboard.html', domain_count=domain_count, users=users, history_number=history_number, uptime=uptime, histories=history)
@app.route('/dashboard-domains', methods=['GET'])
@login_required
def dashboard_domains():
if current_user.role.name == 'Administrator':
domains = Domain.query
else:
domains = User(id=current_user.id).get_domain_query()
template = app.jinja_env.get_template("dashboard_domain.html")
render = template.make_module(vars={"current_user": current_user})
columns = [Domain.name, Domain.dnssec, Domain.type, Domain.serial, Domain.master]
# History.created_on.desc()
order_by = []
for i in range(len(columns)):
column_index = request.args.get("order[{0}][column]".format(i))
sort_direction = request.args.get("order[{0}][dir]".format(i))
if column_index is None:
break
if sort_direction != "asc" and sort_direction != "desc":
sort_direction = "asc"
column = columns[int(column_index)]
order_by.append(getattr(column, sort_direction)())
if order_by:
domains = domains.order_by(*order_by)
total_count = domains.count()
search = request.args.get("search[value]")
if search:
start = "" if search.startswith("^") else "%"
end = "" if search.endswith("$") else "%"
domains = domains.filter(Domain.name.ilike(start + search.strip("^$") + end))
filtered_count = domains.count()
start = int(request.args.get("start", 0))
length = min(int(request.args.get("length", 0)), 100)
if length != -1:
domains = domains[start:start + length]
if current_user.role.name != 'Administrator':
domains = [d[2] for d in domains]
data = []
for domain in domains:
data.append([
render.name(domain),
render.dnssec(domain),
render.type(domain),
render.serial(domain),
render.master(domain),
render.actions(domain),
])
response_data = {
"draw": int(request.args.get("draw", 0)),
"recordsTotal": total_count,
"recordsFiltered": filtered_count,
"data": data,
}
return jsonify(response_data)
@app.route('/domain/<path:domain_name>', methods=['GET', 'POST'])
@app.route('/domain', methods=['GET', 'POST'])
@login_required
@can_access_domain
def domain(domain_name):
r = Record()
domain = Domain.query.filter(Domain.name == domain_name).first()
@ -321,7 +432,7 @@ def domain(domain_name):
if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name):
editable_records = app.config['RECORDS_ALLOW_EDIT']
else:
editable_records = ['PTR']
editable_records = app.config['REVERSE_ALLOW_EDIT']
return render_template('domain.html', domain=domain, records=records, editable_records=editable_records)
else:
return redirect(url_for('error', code=404))
@ -331,10 +442,13 @@ def domain(domain_name):
@login_required
@admin_role_required
def domain_add():
templates = DomainTemplate.query.all()
if request.method == 'POST':
try:
domain_name = request.form.getlist('domain_name')[0]
domain_type = request.form.getlist('radio_type')[0]
domain_template = request.form.getlist('domain_template')[0]
logging.info("Selected template ==== {0}".format(domain_template))
soa_edit_api = request.form.getlist('radio_type_soa_edit_api')[0]
if ' ' in domain_name or not domain_name or not domain_type:
@ -350,17 +464,33 @@ def domain_add():
d = Domain()
result = d.add(domain_name=domain_name, domain_type=domain_type, soa_edit_api=soa_edit_api, domain_master_ips=domain_master_ips)
if result['status'] == 'ok':
history = History(msg='Add domain %s' % domain_name, detail=str({'domain_type': domain_type, 'domain_master_ips': domain_master_ips}), created_by=current_user.username)
history = History(msg='Add domain {0}'.format(domain_name), detail=str({'domain_type': domain_type, 'domain_master_ips': domain_master_ips}), created_by=current_user.username)
history.add()
if domain_template != '0':
template = DomainTemplate.query.filter(DomainTemplate.id == domain_template).first()
template_records = DomainTemplateRecord.query.filter(DomainTemplateRecord.template_id == domain_template).all()
record_data = []
for template_record in template_records:
record_row = {'record_data': template_record.data, 'record_name': template_record.name, 'record_status': template_record.status, 'record_ttl': template_record.ttl, 'record_type': template_record.type}
record_data.append(record_row)
r = Record()
result = r.apply(domain_name, record_data)
if result['status'] == 'ok':
history = History(msg='Applying template {0} to {1}, created records successfully.'.format(template.name, domain_name), detail=str(result), created_by=current_user.username)
history.add()
else:
history = History(msg='Applying template {0} to {1}, FAILED to created records.'.format(template.name, domain_name), detail=str(result), created_by=current_user.username)
history.add()
return redirect(url_for('dashboard'))
else:
return render_template('errors/400.html', msg=result['msg']), 400
except:
logging.error(traceback.print_exc())
return redirect(url_for('error', code=500))
return render_template('domain_add.html')
return render_template('domain_add.html', templates=templates)
@app.route('/admin/domain/<string:domain_name>/delete', methods=['GET'])
@app.route('/admin/domain/<path:domain_name>/delete', methods=['GET'])
@login_required
@admin_role_required
def domain_delete(domain_name):
@ -370,13 +500,13 @@ def domain_delete(domain_name):
if result['status'] == 'error':
return redirect(url_for('error', code=500))
history = History(msg='Delete domain %s' % domain_name, created_by=current_user.username)
history = History(msg='Delete domain {0}'.format(domain_name), created_by=current_user.username)
history.add()
return redirect(url_for('dashboard'))
@app.route('/admin/domain/<string:domain_name>/manage', methods=['GET', 'POST'])
@app.route('/admin/domain/<path:domain_name>/manage', methods=['GET', 'POST'])
@login_required
@admin_role_required
def domain_management(domain_name):
@ -403,46 +533,46 @@ def domain_management(domain_name):
# grant/revoke user privielges
d.grant_privielges(new_user_list)
history = History(msg='Change domain %s access control' % domain_name, detail=str({'user_has_access': new_user_list}), created_by=current_user.username)
history = History(msg='Change domain {0} access control'.format(domain_name), detail=str({'user_has_access': new_user_list}), created_by=current_user.username)
history.add()
return redirect(url_for('domain_management', domain_name=domain_name))
@app.route('/domain/<string:domain_name>/apply', methods=['POST'], strict_slashes=False)
@app.route('/domain/<path:domain_name>/apply', methods=['POST'], strict_slashes=False)
@login_required
@can_access_domain
def record_apply(domain_name):
"""
example jdata: {u'record_ttl': u'1800', u'record_type': u'CNAME', u'record_name': u'test4', u'record_status': u'Active', u'record_data': u'duykhanh.me'}
"""
#TODO: filter removed records / name modified records.
try:
pdata = request.data
jdata = json.loads(pdata)
jdata = request.json
r = Record()
result = r.apply(domain_name, jdata)
if result['status'] == 'ok':
history = History(msg='Apply record changes to domain %s' % domain_name, detail=str(jdata), created_by=current_user.username)
history = History(msg='Apply record changes to domain {0}'.format(domain_name), detail=str(jdata), created_by=current_user.username)
history.add()
return make_response(jsonify( result ), 200)
else:
return make_response(jsonify( result ), 400)
except:
print traceback.format_exc()
logging.error(traceback.print_exc())
return make_response(jsonify( {'status': 'error', 'msg': 'Error when applying new changes'} ), 500)
@app.route('/domain/<string:domain_name>/update', methods=['POST'], strict_slashes=False)
@app.route('/domain/<path:domain_name>/update', methods=['POST'], strict_slashes=False)
@login_required
@can_access_domain
def record_update(domain_name):
"""
This route is used for domain work as Slave Zone only
Pulling the records update from its Master
"""
try:
pdata = request.data
jdata = json.loads(pdata)
jdata = request.json
domain_name = jdata['domain']
d = Domain()
@ -452,11 +582,11 @@ def record_update(domain_name):
else:
return make_response(jsonify( {'status': 'error', 'msg': result['msg']} ), 500)
except:
print traceback.format_exc()
logging.error(traceback.print_exc())
return make_response(jsonify( {'status': 'error', 'msg': 'Error when applying new changes'} ), 500)
@app.route('/domain/<string:domain_name>/record/<string:record_name>/type/<string:record_type>/delete', methods=['GET'])
@app.route('/domain/<path:domain_name>/record/<path:record_name>/type/<path:record_type>/delete', methods=['GET'])
@login_required
@admin_role_required
def record_delete(domain_name, record_name, record_type):
@ -464,21 +594,23 @@ def record_delete(domain_name, record_name, record_type):
r = Record(name=record_name, type=record_type)
result = r.delete(domain=domain_name)
if result['status'] == 'error':
print result['msg']
print(result['msg'])
except:
print traceback.format_exc()
logging.error(traceback.print_exc())
return redirect(url_for('error', code=500)), 500
return redirect(url_for('domain', domain_name=domain_name))
@app.route('/domain/<string:domain_name>/dnssec', methods=['GET'])
@app.route('/domain/<path:domain_name>/dnssec', methods=['GET'])
@can_access_domain
@login_required
def domain_dnssec(domain_name):
domain = Domain()
dnssec = domain.get_domain_dnssec(domain_name)
return make_response(jsonify(dnssec), 200)
@app.route('/domain/<string:domain_name>/managesetting', methods=['GET', 'POST'])
@app.route('/domain/<path:domain_name>/managesetting', methods=['GET', 'POST'])
@login_required
@admin_role_required
def admin_setdomainsetting(domain_name):
@ -488,9 +620,9 @@ def admin_setdomainsetting(domain_name):
# {'action': 'set_setting', 'setting': 'default_action, 'value': 'True'}
#
try:
pdata = request.data
jdata = json.loads(pdata)
jdata = request.json
data = jdata['data']
if jdata['action'] == 'set_setting':
new_setting = data['setting']
new_value = str(data['value'])
@ -499,14 +631,14 @@ def admin_setdomainsetting(domain_name):
if setting:
if setting.set(new_value):
history = History(msg='Setting %s changed value to %s for %s' % (new_setting, new_value, domain.name), created_by=current_user.username)
history = History(msg='Setting {0} changed value to {1} for {2}'.format(new_setting, new_value, domain.name), created_by=current_user.username)
history.add()
return make_response(jsonify( { 'status': 'ok', 'msg': 'Setting updated.' } ))
else:
return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to set value of setting.' } ))
else:
if domain.add_setting(new_setting, new_value):
history = History(msg='New setting %s with value %s for %s has been created' % (new_setting, new_value, domain.name), created_by=current_user.username)
history = History(msg='New setting {0} with value {1} for {2} has been created'.format(new_setting, new_value, domain.name), created_by=current_user.username)
history.add()
return make_response(jsonify( { 'status': 'ok', 'msg': 'New setting created and updated.' } ))
else:
@ -514,10 +646,182 @@ def admin_setdomainsetting(domain_name):
else:
return make_response(jsonify( { 'status': 'error', 'msg': 'Action not supported.' } ), 400)
except:
print traceback.format_exc()
logging.error(traceback.print_exc())
return make_response(jsonify( { 'status': 'error', 'msg': 'There is something wrong, please contact Administrator.' } ), 400)
@app.route('/templates', methods=['GET', 'POST'])
@app.route('/templates/list', methods=['GET', 'POST'])
@login_required
@admin_role_required
def templates():
templates = DomainTemplate.query.all()
return render_template('template.html', templates=templates)
@app.route('/template/create', methods=['GET', 'POST'])
@login_required
@admin_role_required
def create_template():
if request.method == 'GET':
return render_template('template_add.html')
if request.method == 'POST':
try:
name = request.form.getlist('name')[0]
description = request.form.getlist('description')[0]
if ' ' in name or not name or not type:
flash("Please correct your input", 'error')
return redirect(url_for('create_template'))
if DomainTemplate.query.filter(DomainTemplate.name == name).first():
flash("A template with the name {0} already exists!".format(name), 'error')
return redirect(url_for('create_template'))
t = DomainTemplate(name=name, description=description)
result = t.create()
if result['status'] == 'ok':
history = History(msg='Add domain template {0}'.format(name), detail=str({'name': name, 'description': description}), created_by=current_user.username)
history.add()
return redirect(url_for('templates'))
else:
flash(result['msg'], 'error')
return redirect(url_for('create_template'))
except:
logging.error(traceback.print_exc())
return redirect(url_for('error', code=500))
return redirect(url_for('templates'))
@app.route('/template/createfromzone', methods=['POST'])
@login_required
@admin_role_required
def create_template_from_zone():
try:
jdata = request.json
name = jdata['name']
description = jdata['description']
domain_name = jdata['domain']
if ' ' in name or not name or not type:
return make_response(jsonify({'status': 'error', 'msg': 'Please correct template name'}), 500)
if DomainTemplate.query.filter(DomainTemplate.name == name).first():
return make_response(jsonify({'status': 'error', 'msg': 'A template with the name {0} already exists!'.format(name)}), 500)
t = DomainTemplate(name=name, description=description)
result = t.create()
if result['status'] == 'ok':
history = History(msg='Add domain template {0}'.format(name), detail=str({'name': name, 'description': description}), created_by=current_user.username)
history.add()
records = []
r = Record()
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
# query domain info from PowerDNS API
zone_info = r.get_record_data(domain.name)
if zone_info:
jrecords = zone_info['records']
if NEW_SCHEMA:
for jr in jrecords:
name = '@' if jr['name'] == domain_name else jr['name']
if jr['type'] in app.config['RECORDS_ALLOW_EDIT']:
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']:
record = DomainTemplateRecord(name=name, type=jr['type'], status=True if jr['disabled'] else False, ttl=jr['ttl'], data=jr['content'])
records.append(record)
result_records = t.replace_records(records)
if result_records['status'] == 'ok':
return make_response(jsonify({'status': 'ok', 'msg': result['msg']}), 200)
else:
result = t.delete_template()
return make_response(jsonify({'status': 'error', 'msg': result_records['msg']}), 500)
else:
return make_response(jsonify({'status': 'error', 'msg': result['msg']}), 500)
except:
logging.error(traceback.print_exc())
return make_response(jsonify({'status': 'error', 'msg': 'Error when applying new changes'}), 500)
@app.route('/template/<string:template>/edit', methods=['GET'])
@login_required
@admin_role_required
def edit_template(template):
try:
t = DomainTemplate.query.filter(DomainTemplate.name == template).first()
if t is not None:
records = []
for jr in t.records:
if jr.type in app.config['RECORDS_ALLOW_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'])
except:
logging.error(traceback.print_exc())
return redirect(url_for('error', code=500))
return redirect(url_for('templates'))
@app.route('/template/<string:template>/apply', methods=['POST'], strict_slashes=False)
@login_required
def apply_records(template):
try:
jdata = request.json
records = []
for j in jdata:
name = '@' if j['record_name'] in ['@', ''] else j['record_name']
type = j['record_type']
data = j['record_data']
disabled = True if j['record_status'] == 'Disabled' else False
ttl = int(j['record_ttl']) if j['record_ttl'] else 3600
dtr = DomainTemplateRecord(name=name, type=type, data=data, status=disabled, ttl=ttl)
records.append(dtr)
t = DomainTemplate.query.filter(DomainTemplate.name == template).first()
result = t.replace_records(records)
if result['status'] == 'ok':
history = History(msg='Apply domain template record changes to domain template {0}'.format(template), detail=str(jdata), created_by=current_user.username)
history.add()
return make_response(jsonify(result), 200)
else:
return make_response(jsonify(result), 400)
except:
logging.error(traceback.print_exc())
return make_response(jsonify({'status': 'error', 'msg': 'Error when applying new changes'}), 500)
@app.route('/template/<string:template>/delete', methods=['GET'])
@login_required
@admin_role_required
def delete_template(template):
try:
t = DomainTemplate.query.filter(DomainTemplate.name == template).first()
if t is not None:
result = t.delete_template()
if result['status'] == 'ok':
history = History(msg='Deleted domain template {0}'.format(template), detail=str({'name': template}), created_by=current_user.username)
history.add()
return redirect(url_for('templates'))
else:
flash(result['msg'], 'error')
return redirect(url_for('templates'))
except:
logging.error(traceback.print_exc())
return redirect(url_for('error', code=500))
return redirect(url_for('templates'))
@app.route('/admin', methods=['GET', 'POST'])
@login_required
@admin_role_required
@ -531,12 +835,13 @@ def admin():
history_number = History.query.count()
if statistics:
uptime = filter(lambda uptime: uptime['name'] == 'uptime', statistics)[0]['value']
uptime = list([uptime for uptime in statistics if uptime['name'] == 'uptime'])[0]['value']
else:
uptime = 0
return render_template('admin.html', domains=domains, users=users, configs=configs, statistics=statistics, uptime=uptime, history_number=history_number)
@app.route('/admin/user/create', methods=['GET', 'POST'])
@login_required
@admin_role_required
@ -553,14 +858,11 @@ def admin_createuser():
return render_template('admin_createuser.html', user=user, blank_password=True)
result = user.create_local_user();
if result['status']:
return redirect(url_for('admin_manageuser'))
if result == 'Email already existed':
return render_template('admin_createuser.html', user=user, duplicate_email=True)
return render_template('admin_createuser.html', user=user, error=result['msg'])
if result == 'Username already existed':
return render_template('admin_createuser.html', user=user, duplicate_username=True)
return redirect(url_for('admin_manageuser'))
@app.route('/admin/manageuser', methods=['GET', 'POST'])
@login_required
@ -576,15 +878,14 @@ def admin_manageuser():
# {'action': 'delete_user', 'data': 'username'}
#
try:
pdata = request.data
jdata = json.loads(pdata)
jdata = request.json
data = jdata['data']
if jdata['action'] == 'delete_user':
user = User(username=data)
result = user.delete()
if result:
history = History(msg='Delete username %s' % data, created_by=current_user.username)
history = History(msg='Delete username {0}'.format(data), created_by=current_user.username)
history.add()
return make_response(jsonify( { 'status': 'ok', 'msg': 'User has been removed.' } ), 200)
else:
@ -594,7 +895,7 @@ def admin_manageuser():
user = User(username=data)
result = user.revoke_privilege()
if result:
history = History(msg='Revoke %s user privielges' % data, created_by=current_user.username)
history = History(msg='Revoke {0} user privielges'.format(data), created_by=current_user.username)
history.add()
return make_response(jsonify( { 'status': 'ok', 'msg': 'Revoked user privielges.' } ), 200)
else:
@ -606,7 +907,7 @@ def admin_manageuser():
user = User(username=username)
result = user.set_admin(is_admin)
if result:
history = History(msg='Change user role of %s' % username, created_by=current_user.username)
history = History(msg='Change user role of {0}'.format(username), created_by=current_user.username)
history.add()
return make_response(jsonify( { 'status': 'ok', 'msg': 'Changed user role successfully.' } ), 200)
else:
@ -614,7 +915,7 @@ def admin_manageuser():
else:
return make_response(jsonify( { 'status': 'error', 'msg': 'Action not supported.' } ), 400)
except:
print traceback.format_exc()
logging.error(traceback.print_exc())
return make_response(jsonify( { 'status': 'error', 'msg': 'There is something wrong, please contact Administrator.' } ), 400)
@ -637,6 +938,7 @@ def admin_history():
histories = History.query.all()
return render_template('admin_history.html', histories=histories)
@app.route('/admin/settings', methods=['GET'])
@login_required
@admin_role_required
@ -645,7 +947,8 @@ def admin_settings():
settings = Setting.query.filter(Setting.name != 'maintenance')
return render_template('admin_settings.html', settings=settings)
@app.route('/admin/setting/<string:setting>/toggle', methods=['POST'])
@app.route('/admin/setting/<path:setting>/toggle', methods=['POST'])
@login_required
@admin_role_required
def admin_settings_toggle(setting):
@ -655,19 +958,21 @@ def admin_settings_toggle(setting):
else:
return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to toggle setting.' } ), 500)
@app.route('/admin/setting/<string:setting>/edit', methods=['POST'])
@app.route('/admin/setting/<path:setting>/edit', methods=['POST'])
@login_required
@admin_role_required
def admin_settings_edit(setting):
pdata = request.data
jdata = json.loads(pdata)
jdata = request.json
new_value = jdata['value']
result = Setting().set(setting, new_value)
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('/user/profile', methods=['GET', 'POST'])
@login_required
def user_profile():
@ -682,13 +987,13 @@ def user_profile():
# json data
if request.data:
jdata = json.loads(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: %s' % enable_otp } ), 200)
return make_response(jsonify( { 'status': 'ok', 'msg': 'Change OTP Authentication successfully. Status: {0}'.format(enable_otp) } ), 200)
# get new avatar
save_file_name = None
@ -702,7 +1007,6 @@ def user_profile():
save_file_name = current_user.username + '.' + file_extension
file.save(os.path.join(app.config['UPLOAD_DIR'], 'avatar', save_file_name))
# 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()
@ -710,7 +1014,7 @@ def user_profile():
return render_template('user_profile.html')
@app.route('/user/avatar/<string:filename>')
@app.route('/user/avatar/<path:filename>')
def user_avatar(filename):
return send_from_directory(os.path.join(app.config['UPLOAD_DIR'], 'avatar'), filename)
@ -737,6 +1041,7 @@ def dyndns_checkip():
# route covers the default ddclient 'web' setting for the checkip service
return render_template('dyndns.html', response=request.environ.get('HTTP_X_REAL_IP', request.remote_addr))
@app.route('/nic/update', methods=['GET', 'POST'])
@dyndns_login_required
def dyndns_update():
@ -768,39 +1073,39 @@ def dyndns_update():
break
if not domain:
history = History(msg="DynDNS update: attempted update of %s but it does not exist for this user" % hostname, created_by=current_user.username)
history = History(msg="DynDNS update: attempted update of {0} but it does not exist for this user".format(hostname), created_by=current_user.username)
history.add()
return render_template('dyndns.html', response='nohost'), 200
r = Record()
r.name = hostname
# check if the user requested record exists within this domain
if r.exists(domain.name) and r.is_allowed:
if r.exists(domain.name) and r.is_allowed_edit():
if r.data == myip:
# record content did not change, return 'nochg'
history = History(msg="DynDNS update: attempted update of %s but record did not change" % hostname, created_by=current_user.username)
history = History(msg="DynDNS update: attempted update of {0} but record did not change".format(hostname), created_by=current_user.username)
history.add()
return render_template('dyndns.html', response='nochg'), 200
else:
oldip = r.data
result = r.update(domain.name, myip)
if result['status'] == 'ok':
history = History(msg='DynDNS update: updated record %s in zone %s, it changed from %s to %s' % (hostname,domain.name,oldip,myip), detail=str(result), created_by=current_user.username)
history = History(msg='DynDNS update: updated record {0} in zone {1}, it changed from {2} to {3}'.format(hostname,domain.name,oldip,myip), detail=str(result), created_by=current_user.username)
history.add()
return render_template('dyndns.html', response='good'), 200
else:
return render_template('dyndns.html', response='911'), 200
elif r.is_allowed:
elif r.is_allowed_edit():
ondemand_creation = DomainSetting.query.filter(DomainSetting.domain == domain).filter(DomainSetting.setting == 'create_via_dyndns').first()
if (ondemand_creation != None) and (strtobool(ondemand_creation.value) == True):
record = Record(name=hostname,type='A',data=myip,status=False,ttl=3600)
result = record.add(domain.name)
if result['status'] == 'ok':
history = History(msg='DynDNS update: created record %s in zone %s, it now represents %s' % (hostname,domain.name,myip), detail=str(result), created_by=current_user.username)
history = History(msg='DynDNS update: created record {0} in zone {1}, it now represents {2}'.format(hostname,domain.name,myip), detail=str(result), created_by=current_user.username)
history.add()
return render_template('dyndns.html', response='good'), 200
history = History(msg="DynDNS update: attempted update of %s but it does not exist for this user" % hostname, created_by=current_user.username)
history = History(msg='DynDNS update: attempted update of {0} but it does not exist for this user'.format(hostname), created_by=current_user.username)
history.add()
return render_template('dyndns.html', response='nohost'), 200

View file

@ -5,7 +5,7 @@ basedir = os.path.abspath(os.path.dirname(__file__))
WTF_CSRF_ENABLED = True
SECRET_KEY = 'We are the world'
BIND_ADDRESS = '127.0.0.1'
PORT = 9393
PORT = 9191
LOGIN_TITLE = "PDNS"
# TIMEOUT - for large zones
@ -36,14 +36,22 @@ SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')
SQLALCHEMY_TRACK_MODIFICATIONS = True
# LDAP CONFIG
LDAP_ENABLE = False
LDAP_TYPE = 'ldap'
LDAP_URI = 'ldaps://your-ldap-server:636'
# with LDAP_BIND_TYPE you can specify 'direct' or 'search' to use user credentials
# for binding or a predefined LDAP_USERNAME and LDAP_PASSWORD, binding with non-DN only works with AD
LDAP_BIND_TYPE= 'direct' # direct or search
LDAP_USERNAME = 'cn=dnsuser,ou=users,ou=services,dc=duykhanh,dc=me'
LDAP_PASSWORD = 'dnsuser'
LDAP_SEARCH_BASE = 'ou=System Admins,ou=People,dc=duykhanh,dc=me'
# 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 = True # True or False
#LDAP_ADMIN_GROUP = 'CN=DnsAdmins,CN=Users,DC=example,DC=me'
#LDAP_USER_GROUP = 'CN=Domain Admins,CN=Users,DC=example,DC=me'
## AD CONFIG
#LDAP_TYPE = 'ad'
@ -58,12 +66,24 @@ LDAP_FILTER = '(objectClass=inetorgperson)'
# Github Oauth
GITHUB_OAUTH_ENABLE = False
GITHUB_OAUTH_KEY = 'G0j1Q15aRsn36B3aD6nwKLiYbeirrUPU8nDd1wOC'
GITHUB_OAUTH_SECRET = '0WYrKWePeBDkxlezzhFbDn1PBnCwEa0vCwVFvy6iLtgePlpT7WfUlAa9sZgm'
GITHUB_OAUTH_KEY = ''
GITHUB_OAUTH_SECRET = ''
GITHUB_OAUTH_SCOPE = 'email'
GITHUB_OAUTH_URL = 'http://127.0.0.1:5000/api/v3/'
GITHUB_OAUTH_TOKEN = 'http://127.0.0.1:5000/oauth/token'
GITHUB_OAUTH_AUTHORIZE = 'http://127.0.0.1:5000/oauth/authorize'
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/'
#Default Auth
BASIC_ENABLED = True
@ -72,10 +92,13 @@ SIGNUP_ENABLED = True
# POWERDNS CONFIG
PDNS_STATS_URL = 'http://172.16.214.131:8081/'
PDNS_API_KEY = 'you never know'
PDNS_VERSION = '3.4.7'
PDNS_VERSION = '4.1.1'
# RECORDS ALLOWED TO EDIT
RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CNAME', 'SPF', 'PTR', 'MX', 'TXT']
RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'PTR', 'SPF', 'SRV', 'TXT', 'NS']
# RECORDS ALLOWED TO EDIT FOR REVERSE DOMAINS
REVERSE_ALLOW_EDIT = ['PTR', 'NS']
# EXPERIMENTAL FEATURES
PRETTY_IPV6_PTR = False

97
configs/development.py Normal file
View file

@ -0,0 +1,97 @@
import os
basedir = os.path.abspath(os.path.dirname(__file__))
# BASIC APP CONFIG
WTF_CSRF_ENABLED = True
SECRET_KEY = 'changeme'
LOG_LEVEL = 'DEBUG'
LOG_FILE = 'log.txt'
# TIMEOUT - for large zones
TIMEOUT = 10
# UPLOAD DIR
UPLOAD_DIR = os.path.join(basedir, 'upload')
# DATABASE CONFIG FOR MYSQL
DB_USER = 'powerdnsadmin'
DB_PASSWORD = 'powerdnsadminpassword'
DB_HOST = 'docker.for.mac.localhost'
DB_NAME = 'powerdnsadmin'
#MySQL
SQLALCHEMY_DATABASE_URI = 'mysql://'+DB_USER+':'+DB_PASSWORD+'@'+DB_HOST+'/'+DB_NAME
SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')
SQLALCHEMY_TRACK_MODIFICATIONS = True
# AUTHENTICATION CONFIG
BASIC_ENABLED = True
SIGNUP_ENABLED = True
# LDAP CONFIG
LDAP_ENABLE = False
LDAP_TYPE = 'ldap'
LDAP_URI = 'ldaps://your-ldap-server:636'
# with LDAP_BIND_TYPE you can specify 'direct' or 'search' to use user credentials
# for binding or a predefined LDAP_USERNAME and LDAP_PASSWORD, binding with non-DN only works with AD
LDAP_BIND_TYPE= 'direct' # direct or search
LDAP_USERNAME = 'cn=dnsuser,ou=users,ou=services,dc=duykhanh,dc=me'
LDAP_PASSWORD = 'dnsuser'
LDAP_SEARCH_BASE = 'ou=System Admins,ou=People,dc=duykhanh,dc=me'
# 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 = True # True or False
#LDAP_ADMIN_GROUP = 'CN=DnsAdmins,CN=Users,DC=example,DC=me'
#LDAP_USER_GROUP = 'CN=Domain Admins,CN=Users,DC=example,DC=me'
## 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 AUTHENTICATION
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 AUTHENTICATION
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/'
# POWERDNS CONFIG
PDNS_STATS_URL = 'http://192.168.100.100:8081/'
PDNS_API_KEY = 'changeme'
PDNS_VERSION = '4.1.1'
# RECORDS ALLOWED TO EDIT
RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'PTR', 'SPF', 'SRV', 'TXT', 'NS']
# RECORDS ALLOWED TO EDIT FOR REVERSE DOMAINS
REVERSE_ALLOW_EDIT = ['PTR', 'NS']
# EXPERIMENTAL FEATURES
PRETTY_IPV6_PTR = False

View file

@ -1,13 +1,16 @@
#!/usr/bin/env python
#!/usr/bin/env python3
import sys
import time
import os.path
import traceback
from migrate.versioning import api
from config import SQLALCHEMY_DATABASE_URI
from config import SQLALCHEMY_MIGRATE_REPO
from app import db
from app.models import Role, Setting
import os.path
import time
import sys
from app.models import Role, Setting, DomainTemplate
def start():
wait_time = get_waittime_from_env()
@ -22,13 +25,14 @@ def get_waittime_from_env():
return int(os.environ.get('WAITFOR_DB', 1))
def connect_db(wait_time):
for i in xrange(0, wait_time):
for i in range(0, wait_time):
print("INFO: Wait for database server")
sys.stdout.flush()
try:
db.create_all()
return True
except:
traceback.print_exc()
time.sleep(1)
return False
@ -36,14 +40,14 @@ def connect_db(wait_time):
def init_roles(db, role_names):
# Get key name of data
name_of_roles = map(lambda r: r.name, role_names)
name_of_roles = [r.name for r in role_names]
# Query to get current data
rows = db.session.query(Role).filter(Role.name.in_(name_of_roles)).all()
name_of_rows = map(lambda r: r.name, rows)
name_of_rows = [r.name for r in rows]
# Check which data that need to insert
roles = filter(lambda r: r.name not in name_of_rows, role_names)
roles = [r for r in role_names if r.name not in name_of_rows]
# Insert data
for role in roles:
@ -52,19 +56,36 @@ def init_roles(db, role_names):
def init_settings(db, setting_names):
# Get key name of data
name_of_settings = map(lambda r: r.name, setting_names)
name_of_settings = [r.name for r in setting_names]
# Query to get current data
rows = db.session.query(Setting).filter(Setting.name.in_(name_of_settings)).all()
# Check which data that need to insert
name_of_rows = map(lambda r: r.name, rows)
settings = filter(lambda r: r.name not in name_of_rows, setting_names)
name_of_rows = [r.name for r in rows]
settings = [r for r in setting_names if r.name not in name_of_rows]
# Insert data
for setting in settings:
db.session.add(setting)
def init_domain_templates(db, domain_template_names):
# Get key name of data
name_of_domain_templates = map(lambda r: r.name, domain_template_names)
# Query to get current data
rows = db.session.query(DomainTemplate).filter(DomainTemplate.name.in_(name_of_domain_templates)).all()
# Check which data that need to insert
name_of_rows = map(lambda r: r.name, rows)
domain_templates = filter(lambda r: r.name not in name_of_rows, domain_template_names)
# Insert data
for domain_template in domain_templates:
db.session.add(domain_template)
def init_records():
# Create initial user roles and turn off maintenance mode
init_roles(db, [
@ -80,7 +101,12 @@ def init_records():
Setting('default_domain_table_size', '10'),
Setting('auto_ptr','False')
])
# TODO: add sample records to sample templates
init_domain_templates(db, [
DomainTemplate('basic_template_1', 'Basic Template #1'),
DomainTemplate('basic_template_2', 'Basic Template #2'),
DomainTemplate('basic_template_3', 'Basic Template #3')
])
db_commit = db.session.commit()
commit_version_control(db_commit)

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
from migrate.versioning import api
from config import SQLALCHEMY_DATABASE_URI
from config import SQLALCHEMY_MIGRATE_REPO

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
import imp
from migrate.versioning import api
from app import db

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
from migrate.versioning import api
from config import SQLALCHEMY_DATABASE_URI
from config import SQLALCHEMY_MIGRATE_REPO

22
docker-compose.dev.yml Normal file
View file

@ -0,0 +1,22 @@
version: "2.1"
services:
powerdns-admin:
build:
context: .
dockerfile: Dockerfile
image: powerdns-admin
container_name: powerdns-admin
mem_limit: 256M
memswap_limit: 256M
tty: true
command: /usr/bin/supervisord -c /etc/supervisord.conf
ports:
- "9191:9191"
volumes:
- .:/powerdns-admin/
- "./configs/development.py:/powerdns-admin/config.py"
logging:
driver: json-file
options:
max-size: 50m

View file

@ -1,50 +0,0 @@
version: '2'
services:
powerdns-authoritative:
image: winggundamth/powerdns-mysql:trusty
hostname: powerdns-authoritative
depends_on:
- powerdns-authoritative-mariadb
links:
- powerdns-authoritative-mariadb:mysqldb
ports:
- 172.17.0.1:53:53/udp
- 8081:8081
environment:
- PDNS_DB_HOST=mysqldb
- PDNS_DB_USERNAME=root
- PDNS_DB_NAME=powerdns
- PDNS_DB_PASSWORD=PowerDNSPassword
- PDNS_API_KEY=PowerDNSAPIKey
powerdns-authoritative-mariadb:
image: mariadb:10.1.15
hostname: powerdns-authoritative-mariadb
environment:
- MYSQL_DATABASE=powerdns
- MYSQL_ROOT_PASSWORD=PowerDNSPassword
powerdns-admin:
image: winggundamth/powerdns-admin:trusty
hostname: powerdns-admin
depends_on:
- powerdns-admin-mariadb
- powerdns-authoritative
links:
- powerdns-admin-mariadb:mysqldb
- powerdns-authoritative:powerdns-server
volumes:
- ./:/home/web/powerdns-admin
ports:
- 9393:9393
environment:
- WAITFOR_DB=60
powerdns-admin-mariadb:
image: mariadb:10.1.15
hostname: powerdns-admin-mariadb
environment:
- MYSQL_DATABASE=powerdns-admin
- MYSQL_ROOT_PASSWORD=PowerDNSAdminPassword

View file

@ -1,14 +1,16 @@
Flask>=0.10
Flask-WTF>=0.11
Flask-Login>=0.2.11
configobj==5.0.5
bcrypt==3.1.0
requests==2.7.0
python-ldap==2.4.21
Flask-SQLAlchemy==2.1
SQLAlchemy==1.0.9
Flask==0.12.2
Flask-WTF==0.14.2
Flask-Login==0.4.1
Flask-OAuthlib==0.9.4
Flask-SQLAlchemy==2.3.2
SQLAlchemy==1.2.5
sqlalchemy-migrate==0.10.0
pyotp==2.2.1
qrcode==5.3
Flask-OAuthlib==0.9.3
dnspython>=1.12.0
mysqlclient==1.3.12
configobj==5.0.6
bcrypt==3.1.4
requests==2.18.4
python-ldap==3.0.0
pyotp==2.2.6
qrcode==6.0
dnspython==1.15.0
gunicorn==19.7.1

2
run.py
View file

@ -3,7 +3,7 @@ from app import app
from config import PORT
try:
from config import BIND_ADDRESS
from config import BIND_ADDRESS
except:
BIND_ADDRESS = '127.0.0.1'

23
supervisord.conf Normal file
View file

@ -0,0 +1,23 @@
[unix_http_server]
file=/tmp/supervisor.sock ; (the path to the socket file)
chown=nobody:nogroup ; socket file uid:gid owner
[supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10 ; (num of main logfile rotation backups;default 10)
loglevel=info ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=true ; (start in foreground if true;default false)
minfds=1024 ; (min. avail startup file descriptors;default 1024)
minprocs=200 ; (min. avail process descriptors;default 200)
[program:powerdns-admin]
command=/usr/local/bin/gunicorn -t 120 --workers 4 --bind '0.0.0.0:9191' --log-level info app:app
directory=/powerdns-admin
autostart=true
priority=999
user=www-data
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0