Compare commits

..

No commits in common. "master" and "v0.1" have entirely different histories.
master ... v0.1

229 changed files with 7502 additions and 25178 deletions

View file

@ -1,112 +0,0 @@
### OSX ###
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
.pytest_cache/
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Flask stuff:
flask/
instance/settings.py
.webassets-cache
# Scrapy stuff:
.scrapy
# celery beat schedule file
celerybeat-schedule.*
# Node
node_modules
npm-debug.log
# Docker
Dockerfile*
docker-compose*
.dockerignore
# Git
.git
.gitattributes
.gitignore
# Vscode
.vscode
*.code-workspace
# Others
.lgtm.yml
.travis.yml

15
.env Normal file
View file

@ -0,0 +1,15 @@
ENVIRONMENT=development
PDA_DB_HOST=powerdns-admin-mysql
PDA_DB_NAME=powerdns_admin
PDA_DB_USER=powerdns_admin
PDA_DB_PASSWORD=changeme
PDNS_DB_HOST=pdns-mysql
PDNS_DB_NAME=pdns
PDNS_DB_USER=pdns
PDNS_DB_PASSWORD=changeme
PDNS_HOST=pdns-server
PDNS_API_KEY=changeme
PDNS_WEBSERVER_ALLOW_FROM=0.0.0.0

19
.github/stale.yml vendored
View file

@ -1,19 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- enhancement
- feature request
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: true

View file

@ -1,56 +0,0 @@
on:
push:
branches:
- 'master'
tags:
- 'v*.*.*'
jobs:
build-and-push-docker-image:
name: Build Docker image and push to repositories
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: |
ngoduykhanh/powerdns-admin
tags: |
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build latest image
uses: docker/build-push-action@v2
if: github.ref == 'refs/heads/master'
with:
context: ./
file: ./docker/Dockerfile
push: true
tags: ngoduykhanh/powerdns-admin:latest
- name: Build release image
uses: docker/build-push-action@v2
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
with:
context: ./
file: ./docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

18
.gitignore vendored
View file

@ -25,18 +25,20 @@ nosetests.xml
flask
config.py
configs/production.py
logfile.log
log.txt
pdns.db
settings.json
advanced_settings.json
idp.crt
*.bak
log.txt
db_repository/*
upload/avatar/*
tmp/*
.ropeproject
.sonarlint/*
pdns.db
node_modules
powerdnsadmin/static/generated
.webassets-cache
.venv*
.pytest_cache
.DS_Store
app/static/generated

24
.travis.yml Normal file
View file

@ -0,0 +1,24 @@
language: python
python:
- "3.5.2"
before_install:
- sudo apt-key adv --fetch-keys http://dl.yarnpkg.com/debian/pubkey.gpg
- echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
- travis_retry sudo apt-get update
- travis_retry sudo apt-get install python3-dev libxml2-dev libxmlsec1-dev yarn
- mysql -e 'CREATE DATABASE pda';
- mysql -e "GRANT ALL PRIVILEGES ON pda.* to pda@'%' IDENTIFIED BY 'changeme'";
install:
- pip install -r requirements.txt
before_script:
- mv config_template.py config.py
- export FLASK_APP=app/__init__.py
- flask db upgrade
- yarn install --pure-lockfile
- flask assets build
script:
- sh run_travis.sh
cache:
yarn: true
services:
- mysql

View file

@ -1,12 +0,0 @@
{
"scanSettings": {
"baseBranches": []
},
"checkRunSettings": {
"vulnerableCheckRunConclusionLevel": "failure",
"displayMode": "diff"
},
"issueSettings": {
"minSeverityLevel": "LOW"
}
}

View file

@ -1 +1 @@
--*.modules-folder "./powerdnsadmin/static/node_modules"
--*.modules-folder "./app/static/node_modules"

View file

@ -1,6 +1,7 @@
# PowerDNS-Admin
A PowerDNS web interface with advanced features.
[![Build Status](https://travis-ci.org/ngoduykhanh/PowerDNS-Admin.svg?branch=master)](https://travis-ci.org/ngoduykhanh/PowerDNS-Admin)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/ngoduykhanh/PowerDNS-Admin.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ngoduykhanh/PowerDNS-Admin/context:python)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/ngoduykhanh/PowerDNS-Admin.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ngoduykhanh/PowerDNS-Admin/context:javascript)
@ -10,51 +11,33 @@ A PowerDNS web interface with advanced features.
- User management
- User access management based on domain
- User activity logging
- Support Local DB / SAML / LDAP / Active Directory user authentication
- Support Google / Github / Azure / OpenID OAuth
- Local DB / SAML / LDAP / Active Directory user authentication
- Google oauth authentication
- Github oauth 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!)
- Limited API for manipulating zones and records
- Full IDN/Punycode support
## Running PowerDNS-Admin
There are several ways to run PowerDNS-Admin. The easiest way is to use Docker.
If you are looking to install and run PowerDNS-Admin directly onto your system check out the [Wiki](https://github.com/ngoduykhanh/PowerDNS-Admin/wiki#installation-guides) for ways to do that.
### 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 which has PowerDNS-Admin, PowerDNS server and MySQL Back-End Database.
### Docker
This are two options to run PowerDNS-Admin using Docker.
To get started as quickly as possible try option 1. If you want to make modifications to the configuration option 2 may be cleaner.
Step 1: Changing configuration
#### Option 1: From Docker Hub
The easiest is to just run the latest Docker image from Docker Hub:
```
$ docker run -d \
-e SECRET_KEY='a-very-secret-key' \
-v pda-data:/data \
-p 9191:80 \
ngoduykhanh/powerdns-admin:latest
```
This creates a volume called `pda-data` to persist the SQLite database with the configuration.
The configuration file for developement environment is located at `configs/development.py`, you can override some configs by editing `.env` file.
#### Option 2: Using docker-compose
1. Update the configuration
Edit the `docker-compose.yml` file to update the database connection string in `SQLALCHEMY_DATABASE_URI`.
Other environment variables are mentioned in the [legal_envvars](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/configs/docker_config.py#L5-L46).
To use the Docker secrets feature it is possible to append `_FILE` to the environment variables and point to a file with the values stored in it.
Make sure to set the environment variable `SECRET_KEY` to a long random string (https://flask.palletsprojects.com/en/1.1.x/config/#SECRET_KEY)
Step 2: Build docker images
2. Start docker container
```
$ docker-compose up
```
```$ docker-compose build```
You can then access PowerDNS-Admin by pointing your browser to http://localhost:9191.
Step 3: Start docker containers
## Screenshots
```$ docker-compose up```
You can now access PowerDNS-Admin at url http://localhost:9191
**NOTE:** For other methods to run PowerDNS-Admin, please take look at WIKI pages.
### Screenshots
![dashboard](https://user-images.githubusercontent.com/6447444/44068603-0d2d81f6-9fa5-11e8-83af-14e2ad79e370.png)
## LICENSE
MIT. See [LICENSE](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/LICENSE)

40
app/__init__.py Normal file
View file

@ -0,0 +1,40 @@
from werkzeug.contrib.fixers import ProxyFix
from flask import Flask, request, session, redirect, url_for
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy as SA
from flask_migrate import Migrate
from flask_oauthlib.client import OAuth
from sqlalchemy.exc import OperationalError
# subclass SQLAlchemy to enable pool_pre_ping
class SQLAlchemy(SA):
def apply_pool_defaults(self, app, options):
SA.apply_pool_defaults(self, app, options)
options["pool_pre_ping"] = True
from app.assets import assets
app = Flask(__name__)
app.config.from_object('config')
app.wsgi_app = ProxyFix(app.wsgi_app)
assets.init_app(app)
#### CONFIGURE LOGGER ####
from app.lib.log import logger
logging = logger('powerdns-admin', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config()
login_manager = LoginManager()
login_manager.init_app(app)
db = SQLAlchemy(app) # database
migrate = Migrate(app, db) # flask-migrate
oauth_client = OAuth(app) # oauth
if app.config.get('SAML_ENABLED') and app.config.get('SAML_ENCRYPT'):
from app.lib import certutil
if not certutil.check_certificate():
certutil.create_self_signed_cert()
from app import models
from app import views

73
app/assets.py Normal file
View file

@ -0,0 +1,73 @@
from flask_assets import Bundle, Environment, Filter
class ConcatFilter(Filter):
"""
Filter that merges files, placing a semicolon between them.
Fixes issues caused by missing semicolons at end of JS assets, for example
with last statement of jquery.pjax.js.
"""
def concat(self, out, hunks, **kw):
out.write(';'.join([h.data() for h, info in hunks]))
css_login = Bundle(
'node_modules/bootstrap/dist/css/bootstrap.css',
'node_modules/font-awesome/css/font-awesome.css',
'node_modules/ionicons/dist/css/ionicons.css',
'node_modules/icheck/skins/square/blue.css',
'node_modules/admin-lte/dist/css/AdminLTE.css',
filters=('cssmin','cssrewrite'),
output='generated/login.css'
)
js_login = Bundle(
'node_modules/jquery/dist/jquery.js',
'node_modules/bootstrap/dist/js/bootstrap.js',
'node_modules/icheck/icheck.js',
filters=(ConcatFilter, 'jsmin'),
output='generated/login.js'
)
js_validation = Bundle(
'node_modules/bootstrap-validator/dist/validator.js',
output='generated/validation.js'
)
css_main = Bundle(
'node_modules/bootstrap/dist/css/bootstrap.css',
'node_modules/font-awesome/css/font-awesome.css',
'node_modules/ionicons/dist/css/ionicons.css',
'node_modules/datatables.net-bs/css/dataTables.bootstrap.css',
'node_modules/icheck/skins/square/blue.css',
'node_modules/multiselect/css/multi-select.css',
'node_modules/admin-lte/dist/css/AdminLTE.css',
'node_modules/admin-lte/dist/css/skins/_all-skins.css',
'custom/css/custom.css',
filters=('cssmin','cssrewrite'),
output='generated/main.css'
)
js_main = Bundle(
'node_modules/jquery/dist/jquery.js',
'node_modules/jquery-ui-dist/jquery-ui.js',
'node_modules/bootstrap/dist/js/bootstrap.js',
'node_modules/datatables.net/js/jquery.dataTables.js',
'node_modules/datatables.net-bs/js/dataTables.bootstrap.js',
'node_modules/jquery-sparkline/jquery.sparkline.js',
'node_modules/jquery-slimscroll/jquery.slimscroll.js',
'node_modules/icheck/icheck.js',
'node_modules/fastclick/lib/fastclick.js',
'node_modules/moment/moment.js',
'node_modules/admin-lte/dist/js/adminlte.js',
'node_modules/multiselect/js/jquery.multi-select.js',
'custom/js/custom.js',
filters=(ConcatFilter, 'jsmin'),
output='generated/main.js'
)
assets = Environment()
assets.register('js_login', js_login)
assets.register('js_validation', js_validation)
assets.register('css_login', css_login)
assets.register('js_main', js_main)
assets.register('css_main', css_main)

78
app/decorators.py Normal file
View file

@ -0,0 +1,78 @@
from functools import wraps
from flask import g, redirect, url_for
from app.models import Setting
def admin_role_required(f):
"""
Grant access if user is in Administrator role
"""
@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 operator_role_required(f):
"""
Grant access if user is in Operator role or higher
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user.role.name not in ['Administrator', 'Operator']:
return redirect(url_for('error', code=401))
return f(*args, **kwargs)
return decorated_function
def can_access_domain(f):
"""
Grant access if:
- user is in Operator role or higher, or
- user is in granted Account, or
- user is in granted Domain
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user.role.name not in ['Administrator', 'Operator']:
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
def can_configure_dnssec(f):
"""
Grant access if:
- user is in Operator role or higher, or
- dnssec_admins_only is off
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user.role.name not in ['Administrator', 'Operator'] and Setting().get('dnssec_admins_only'):
return redirect(url_for('error', code=401))
return f(*args, **kwargs)
return decorated_function
def can_create_domain(f):
"""
Grant access if:
- user is in Operator role or higher, or
- allow_user_create_domain is on
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user.role.name not in ['Administrator', 'Operator'] and not Setting().get('allow_user_create_domain'):
return redirect(url_for('error', code=401))
return f(*args, **kwargs)
return decorated_function

View file

@ -42,7 +42,7 @@ def create_self_signed_cert():
cert.set_pubkey(k)
cert.sign(k, 'sha256')
open(CERT_FILE, "bw").write(
open(CERT_FILE, "wt").write(
crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
open(KEY_FILE, "bw").write(
open(KEY_FILE, "wt").write(
crypto.dump_privatekey(crypto.FILETYPE_PEM, k))

46
app/lib/log.py Normal file
View file

@ -0,0 +1,46 @@
import logging
class logger(object):
def __init__(self, name, level, logfile):
self.name = name
self.level = level
self.logfile = logfile
def config(self):
# define logger and set logging level
logger = logging.getLogger()
if self.level == 'CRITICAL':
level = logging.CRITICAL
elif self.level == 'ERROR':
level = logging.ERROR
elif self.level == 'WARNING':
level = logging.WARNING
elif self.level == 'DEBUG':
level = logging.DEBUG
else:
level = logging.INFO
logger.setLevel(level)
# set request requests module log level
logging.getLogger("requests").setLevel(logging.CRITICAL)
if self.logfile:
# define handler to log into file
file_log_handler = logging.FileHandler(self.logfile)
logger.addHandler(file_log_handler)
# define logging format for file
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_log_handler.setFormatter(file_formatter)
# define handler to log into console
stderr_log_handler = logging.StreamHandler()
logger.addHandler(stderr_log_handler)
# define logging format for console
console_formatter = logging.Formatter('[%(levelname)s] %(message)s')
stderr_log_handler.setFormatter(console_formatter)
return logging.getLogger(self.name)

293
app/lib/utils.py Normal file
View file

@ -0,0 +1,293 @@
import re
import json
import requests
import hashlib
from app import app
from distutils.version import StrictVersion
from urllib.parse import urlparse
from datetime import datetime, timedelta
from threading import Thread
from .certutil import KEY_FILE, CERT_FILE
if app.config['SAML_ENABLED']:
from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
idp_timestamp = datetime(1970, 1, 1)
idp_data = None
if 'SAML_IDP_ENTITY_ID' in app.config:
idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None), required_sso_binding=app.config['SAML_IDP_SSO_BINDING'])
else:
idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None))
if idp_data is None:
print('SAML: IDP Metadata initial load failed')
exit(-1)
idp_timestamp = datetime.now()
def get_idp_data():
global idp_data, idp_timestamp
lifetime = timedelta(minutes=app.config['SAML_METADATA_CACHE_LIFETIME'])
if idp_timestamp+lifetime < datetime.now():
background_thread = Thread(target=retreive_idp_data)
background_thread.start()
return idp_data
def retreive_idp_data():
global idp_data, idp_timestamp
if 'SAML_IDP_SSO_BINDING' in app.config:
new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None), required_sso_binding=app.config['SAML_IDP_SSO_BINDING'])
else:
new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None))
if new_idp_data is not None:
idp_data = new_idp_data
idp_timestamp = datetime.now()
print("SAML: IDP Metadata successfully retreived from: " + app.config['SAML_METADATA_URL'])
else:
print("SAML: IDP Metadata could not be retreived")
if 'TIMEOUT' in app.config.keys():
TIMEOUT = app.config['TIMEOUT']
else:
TIMEOUT = 10
def auth_from_url(url):
auth = None
parsed_url = urlparse(url).netloc
if '@' in parsed_url:
auth = parsed_url.split('@')[0].split(':')
auth = requests.auth.HTTPBasicAuth(auth[0], auth[1])
return auth
def fetch_remote(remote_url, method='GET', data=None, accept=None, params=None, timeout=None, headers=None):
if data is not None and type(data) != str:
data = json.dumps(data)
if timeout is None:
timeout = TIMEOUT
verify = False
our_headers = {
'user-agent': 'powerdnsadmin/0',
'pragma': 'no-cache',
'cache-control': 'no-cache'
}
if accept is not None:
our_headers['accept'] = accept
if headers is not None:
our_headers.update(headers)
r = requests.request(
method,
remote_url,
headers=headers,
verify=verify,
auth=auth_from_url(remote_url),
timeout=timeout,
data=data,
params=params
)
try:
if r.status_code not in (200, 400, 422):
r.raise_for_status()
except Exception as e:
raise RuntimeError('Error while fetching {0}'.format(remote_url)) from e
return r
def fetch_json(remote_url, method='GET', data=None, params=None, headers=None):
r = fetch_remote(remote_url, method=method, data=data, params=params, headers=headers,
accept='application/json; q=1')
if method == "DELETE":
return True
if r.status_code == 204:
return {}
try:
assert('json' in r.headers['content-type'])
except Exception as e:
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.decode('utf-8'))
except Exception as e:
raise RuntimeError('Error while loading JSON data from {0}'.format(remote_url)) from e
return data
def display_record_name(data):
record_name, domain_name = data
if record_name == domain_name:
return '@'
else:
return re.sub('\.{}$'.format(domain_name), '', record_name)
def display_master_name(data):
"""
input data: "[u'127.0.0.1', u'8.8.8.8']"
"""
matches = re.findall(r'\'(.+?)\'', data)
return ", ".join(matches)
def display_time(amount, units='s', remove_seconds=True):
"""
Convert timestamp to normal time format
"""
amount = int(amount)
INTERVALS = [(lambda mlsec:divmod(mlsec, 1000), 'ms'),
(lambda seconds:divmod(seconds, 60), 's'),
(lambda minutes:divmod(minutes, 60), 'm'),
(lambda hours:divmod(hours, 24), 'h'),
(lambda days:divmod(days, 7), 'D'),
(lambda weeks:divmod(weeks, 4), 'W'),
(lambda years:divmod(years, 12), 'M'),
(lambda decades:divmod(decades, 10), 'Y')]
for index_start, (interval, unit) in enumerate(INTERVALS):
if unit == units:
break
amount_abrev = []
last_index = 0
amount_temp = amount
for index, (formula, abrev) in enumerate(INTERVALS[index_start: len(INTERVALS)]):
divmod_result = formula(amount_temp)
amount_temp = divmod_result[0]
amount_abrev.append((divmod_result[1], abrev))
if divmod_result[1] > 0:
last_index = index
amount_abrev_partial = amount_abrev[0: last_index + 1]
amount_abrev_partial.reverse()
final_string = ''
for amount, abrev in amount_abrev_partial:
final_string += str(amount) + abrev + ' '
if remove_seconds and 'm' in final_string:
final_string = final_string[:final_string.rfind(' ')]
return final_string[:final_string.rfind(' ')]
return final_string
def pdns_api_extended_uri(version):
"""
Check the pdns version
"""
if StrictVersion(version) >= StrictVersion('4.0.0'):
return "/api/v1"
else:
return ""
def email_to_gravatar_url(email="", size=100):
"""
AD doesn't necessarily have email
"""
if email is None:
email = ""
hash_string = hashlib.md5(email.encode('utf-8')).hexdigest()
return "https://s.gravatar.com/avatar/{0}?s={1}".format(hash_string, size)
def prepare_flask_request(request):
# If server is behind proxys or balancers use the HTTP_X_FORWARDED fields
url_data = urlparse(request.url)
return {
'https': 'on' if request.scheme == 'https' else 'off',
'http_host': request.host,
'server_port': url_data.port,
'script_name': request.path,
'get_data': request.args.copy(),
'post_data': request.form.copy(),
# Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144
'lowercase_urlencoding': True,
'query_string': request.query_string
}
def init_saml_auth(req):
own_url = ''
if req['https'] == 'on':
own_url = 'https://'
else:
own_url = 'http://'
own_url += req['http_host']
metadata = get_idp_data()
settings = {}
settings['sp'] = {}
if 'SAML_NAMEID_FORMAT' in app.config:
settings['sp']['NameIDFormat'] = app.config['SAML_NAMEID_FORMAT']
else:
settings['sp']['NameIDFormat'] = idp_data.get('sp', {}).get('NameIDFormat', 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified')
settings['sp']['entityId'] = app.config['SAML_SP_ENTITY_ID']
cert = open(CERT_FILE, "r").readlines()
key = open(KEY_FILE, "r").readlines()
settings['sp']['privateKey'] = "".join(key)
settings['sp']['x509cert'] = "".join(cert)
settings['sp']['assertionConsumerService'] = {}
settings['sp']['assertionConsumerService']['binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
settings['sp']['assertionConsumerService']['url'] = own_url+'/saml/authorized'
settings['sp']['attributeConsumingService'] = {}
settings['sp']['singleLogoutService'] = {}
settings['sp']['singleLogoutService']['binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
settings['sp']['singleLogoutService']['url'] = own_url+'/saml/sls'
settings['idp'] = metadata['idp']
settings['strict'] = True
settings['debug'] = app.config['SAML_DEBUG']
settings['security'] = {}
settings['security']['digestAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
settings['security']['metadataCacheDuration'] = None
settings['security']['metadataValidUntil'] = None
settings['security']['requestedAuthnContext'] = True
settings['security']['signatureAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
settings['security']['wantAssertionsEncrypted'] = False
settings['security']['wantAttributeStatement'] = True
settings['security']['wantNameId'] = True
settings['security']['authnRequestsSigned'] = app.config['SAML_SIGN_REQUEST']
settings['security']['logoutRequestSigned'] = app.config['SAML_SIGN_REQUEST']
settings['security']['logoutResponseSigned'] = app.config['SAML_SIGN_REQUEST']
settings['security']['nameIdEncrypted'] = False
settings['security']['signMetadata'] = True
settings['security']['wantAssertionsSigned'] = True
settings['security']['wantMessagesSigned'] = True
settings['security']['wantNameIdEncrypted'] = False
settings['contactPerson'] = {}
settings['contactPerson']['support'] = {}
settings['contactPerson']['support']['emailAddress'] = app.config['SAML_SP_CONTACT_NAME']
settings['contactPerson']['support']['givenName'] = app.config['SAML_SP_CONTACT_MAIL']
settings['contactPerson']['technical'] = {}
settings['contactPerson']['technical']['emailAddress'] = app.config['SAML_SP_CONTACT_NAME']
settings['contactPerson']['technical']['givenName'] = app.config['SAML_SP_CONTACT_MAIL']
settings['organization'] = {}
settings['organization']['en-US'] = {}
settings['organization']['en-US']['displayname'] = 'PowerDNS-Admin'
settings['organization']['en-US']['name'] = 'PowerDNS-Admin'
settings['organization']['en-US']['url'] = own_url
auth = OneLogin_Saml2_Auth(req, settings)
return auth
def display_setting_state(value):
if value == 1:
return "ON"
elif value == 0:
return "OFF"
else:
return "UNKNOWN"

2027
app/models.py Normal file

File diff suppressed because it is too large Load diff

77
app/oauth.py Normal file
View file

@ -0,0 +1,77 @@
from ast import literal_eval
from flask import request, session, redirect, url_for
from app import app, oauth_client
from app.models import Setting
# TODO:
# - Replace Flask-OAuthlib by authlib
# - Fix github/google enabling (Currently need to reload the flask app)
def github_oauth():
if not Setting().get('github_oauth_enabled'):
return None
github = oauth_client.remote_app(
'github',
consumer_key = Setting().get('github_oauth_key'),
consumer_secret = Setting().get('github_oauth_secret'),
request_token_params = {'scope': Setting().get('github_oauth_scope')},
base_url = Setting().get('github_oauth_api_url'),
request_token_url = None,
access_token_method = 'POST',
access_token_url = Setting().get('github_oauth_token_url'),
authorize_url = Setting().get('github_oauth_authorize_url')
)
@app.route('/github/authorized')
def github_authorized():
session['github_oauthredir'] = url_for('.github_authorized', _external=True)
resp = github.authorized_response()
if resp is None:
return 'Access denied: reason=%s error=%s' % (
request.args['error'],
request.args['error_description']
)
session['github_token'] = (resp['access_token'], '')
return redirect(url_for('.login'))
@github.tokengetter
def get_github_oauth_token():
return session.get('github_token')
return github
def google_oauth():
if not Setting().get('google_oauth_enabled'):
return None
google = oauth_client.remote_app(
'google',
consumer_key=Setting().get('google_oauth_client_id'),
consumer_secret=Setting().get('google_oauth_client_secret'),
request_token_params=literal_eval(Setting().get('google_token_params')),
base_url=Setting().get('google_base_url'),
request_token_url=None,
access_token_method='POST',
access_token_url=Setting().get('google_token_url'),
authorize_url=Setting().get('google_authorize_url'),
)
@app.route('/google/authorized')
def google_authorized():
resp = google.authorized_response()
if resp is None:
return 'Access denied: reason=%s error=%s' % (
request.args['error_reason'],
request.args['error_description']
)
session['google_token'] = (resp['access_token'], '')
return redirect(url_for('.login'))
@google.tokengetter
def get_google_oauth_token():
return session.get('google_token')
return google

View file

@ -1,6 +1,5 @@
.length-break {
word-break: break-all !important;
width: 70% !important;
}
table td {
@ -35,22 +34,4 @@ table td {
.user-footer {
background-color: #222d32 !important;
}
.ms-container {
background-size: 20px 20px;
background-position: center;
}
.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img.img-circle.offline {
filter: brightness(0);
border-color: black;
}
.navbar-nav>.user-menu .user-image.offline {
filter: brightness(0);
}
.search-input {
width: 100%;
}

View file

@ -9,15 +9,10 @@ function applyChanges(data, url, showResult, refreshPage) {
crossDomain : true,
dataType : "json",
success : function(data, status, jqXHR) {
console.log("Applied changes successfully.");
console.log(data);
console.log("Applied changes successfully.")
if (showResult) {
var modal = $("#modal_success");
if (data['msg']) {
modal.find('.modal-body p').text(data['msg']);
} else {
modal.find('.modal-body p').text("Applied changes successfully");
}
modal.find('.modal-body p').text("Applied changes successfully");
modal.modal('show');
}
if (refreshPage) {
@ -26,6 +21,10 @@ function applyChanges(data, url, showResult, refreshPage) {
},
error : function(jqXHR, status) {
// console.log(jqXHR);
// var modal = $("#modal_error");
// modal.find('.modal-body p').text(jqXHR["responseText"]);
// modal.modal('show');
console.log(jqXHR);
var modal = $("#modal_error");
var responseJson = jQuery.parseJSON(jqXHR.responseText);
@ -53,7 +52,6 @@ function applyRecordChanges(data, domain) {
var modal = $("#modal_success");
modal.find('.modal-body p').text("Applied changes successfully");
modal.modal('show');
setTimeout(() => {window.location.reload()}, 2000);
},
error : function(jqXHR, status) {
@ -77,7 +75,6 @@ function getTableData(table) {
record["record_status"] = r[2].trim();
record["record_ttl"] = r[3].trim();
record["record_data"] = r[4].trim();
record["record_comment"] = r[5].trim();
records.push(record);
});
return records
@ -98,14 +95,13 @@ function saveRow(oTable, nRow) {
oTable.cell(nRow,2).data(status);
oTable.cell(nRow,3).data(jqSelect[2].value);
oTable.cell(nRow,4).data(jqInputs[1].value);
oTable.cell(nRow,5).data(jqInputs[2].value);
var record = jqInputs[0].value;
var button_edit = "<button type=\"button\" class=\"btn btn-flat btn-warning button_edit\" id=\"" + record + "\">Edit&nbsp;<i class=\"fa fa-edit\"></i></button>"
var button_delete = "<button type=\"button\" class=\"btn btn-flat btn-danger button_delete\" id=\"" + record + "\">Delete&nbsp;<i class=\"fa fa-trash\"></i></button>"
oTable.cell(nRow,6).data(button_edit);
oTable.cell(nRow,7).data(button_delete);
oTable.cell(nRow,5).data(button_edit);
oTable.cell(nRow,6).data(button_delete);
oTable.draw();
}
@ -116,44 +112,24 @@ function restoreRow(oTable, nRow) {
oTable.draw();
}
function sec2str(t){
var d = Math.floor(t/86400),
h = Math.floor(t/3600) % 24,
m = Math.floor(t/60) % 60,
s = t % 60;
return (d>0?d+' days ':'')+(h>0?h+' hours ':'')+(m>0?m+' minutes ':'')+(s>0?s+' seconds':'');
}
function editRow(oTable, nRow) {
var isDisabled = 'true';
var aData = oTable.row(nRow).data();
var jqTds = oTable.cells(nRow,'').nodes();
var record_types = "";
var ttl_opts = "";
var ttl_not_found = true;
for(var i = 0; i < records_allow_edit.length; i++) {
var record_type = records_allow_edit[i];
record_types += "<option value=\"" + record_type + "\">" + record_type + "</option>";
}
for(var i = 0; i < ttl_options.length; i++) {
ttl_opts += "<option value=\"" + ttl_options[i][0] + "\">" + ttl_options[i][1] + "</option>";
if (ttl_options[i][0] == aData[3]) {
ttl_not_found = false;
}
}
if (ttl_not_found) {
ttl_opts += "<option value=\"" + aData[3] + "\">" + sec2str(aData[3]) + "</option>";
}
jqTds[0].innerHTML = '<input type="text" id="edit-row-focus" class="form-control input-small" value="' + aData[0] + '">';
jqTds[1].innerHTML = '<select class="form-control" id="record_type" name="record_type" value="' + aData[1] + '">' + record_types + '</select>';
jqTds[2].innerHTML = '<select class="form-control" id="record_status" name="record_status" value="' + aData[2] + '"><option value="false">Active</option><option value="true">Disabled</option></select>';
jqTds[3].innerHTML = '<select class="form-control" id="record_ttl" name="record_ttl" value="' + aData[3] + '">' + ttl_opts + '</select>';
jqTds[1].innerHTML = '<select class="form-control" id="record_type" name="record_type" value="' + aData[1] + '"' + '>' + record_types + '</select>';
jqTds[2].innerHTML = '<select class="form-control" id="record_status" name="record_status" value="' + aData[2] + '"' + '><option value="false">Active</option><option value="true">Disabled</option></select>';
jqTds[3].innerHTML = '<select class="form-control" id="record_ttl" name="record_ttl" value="' + aData[3] + '"' + '><option value="60">1 minute</option><option value="300">5 minutes</option><option value="1800">30 minutes</option><option value="3600">60 minutes</option><option value="86400">24 hours</option></select>';
jqTds[4].innerHTML = '<input type="text" style="display:table-cell; width:100% !important" id="current_edit_record_data" name="current_edit_record_data" class="form-control input-small advance-data" value="' + aData[4].replace(/\"/g,"&quot;") + '">';
jqTds[5].innerHTML = '<input type="text" style="display:table-cell; width:100% !important" id="record_comment" name="record_comment" class="form-control input-small advance-data" value="' + aData[5].replace(/\"/g, "&quot;") + '">';
jqTds[6].innerHTML = '<button type="button" class="btn btn-flat btn-primary button_save">Save</button>';
jqTds[7].innerHTML = '<button type="button" class="btn btn-flat btn-primary button_cancel">Cancel</button>';
jqTds[5].innerHTML = '<button type="button" class="btn btn-flat btn-primary button_save">Save</button>';
jqTds[6].innerHTML = '<button type="button" class="btn btn-flat btn-primary button_cancel">Cancel</button>';
// set current value of dropdown column
// set current value of dropdows column
if (aData[2] == 'Active'){
isDisabled = 'false';
}
@ -169,8 +145,8 @@ function SelectElement(elementID, valueToSelect)
element.value = valueToSelect;
}
function enable_dns_sec(url, csrf_token) {
$.post(url, {'_csrf_token': csrf_token}, function(data) {
function enable_dns_sec(url) {
$.getJSON(url, function(data) {
var modal = $("#modal_dnssec_info");
if (data['status'] == 'error'){
@ -181,7 +157,7 @@ function enable_dns_sec(url, csrf_token) {
//location.reload();
window.location.reload(true);
}
}, 'json')
})
}
function getdnssec(url, domain){
@ -242,57 +218,26 @@ function reload_domains(url) {
// pretty JSON
json_library = {
replacer: function (match, pIndent, pKey, pVal, pEnd) {
replacer: function(match, pIndent, pKey, pVal, pEnd) {
var key = '<span class=json-key>';
var val = '<span class=json-value>';
var str = '<span class=json-string>';
var r = pIndent || '';
if (pKey) {
// r = r + key + pKey.replace(/[": ]/g, '') + '</span>: ';
// Keep the quote in the key
r = r + key + pKey.replace(/":/, '"') + '</span>: ';
if (pKey){
r = r + key + pKey.replace(/[": ]/g, '') + '</span>: ';
}
if (pVal) {
if (pVal){
r = r + (pVal[0] == '"' ? str : val) + pVal + '</span>';
}
return r + (pEnd || '');
},
prettyPrint: function (obj) {
prettyPrint: function(obj) {
obj = obj.replace(/u'/g, "\'").replace(/'/g, "\"").replace(/(False|None)/g, "\"$1\"");
var jsonData = JSON.parse(obj);
// var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
// The new regex to handle case value is an empty list [] or dict {}
var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?/mg;
return JSON.stringify(jsonData, null, 3)
var jsonLine = /^( *)("[\w]+": )?("[^"]*"|[\w.+-]*)?([,[{])?$/mg;
return JSON.stringify(jsonData, null, 3)
.replace(/&/g, '&amp;').replace(/\\"/g, '&quot;')
.replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(jsonLine, json_library.replacer);
}
};
// set count down in second on an element
function timer(elToUpdate, maxTime) {
elToUpdate.text(maxTime + "s");
var interval = setInterval(function () {
if (maxTime > 0) {
maxTime--;
elToUpdate.text(maxTime + "s");
}
else {
clearInterval(interval);
}
}, 1000);
return interval;
}
// copy otp secret code to clipboard
function copy_otp_secret_to_clipboard() {
var copyBox = document.getElementById("otp_secret");
copyBox.select();
copyBox.setSelectionRange(0, 99999); /* For mobile devices */
navigator.clipboard.writeText(copyBox.value);
$("#copy_tooltip").css("visibility", "visible");
setTimeout(function(){ $("#copy_tooltip").css("visibility", "collapse"); }, 2000);
}
};

View file

@ -6,10 +6,11 @@
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
PowerDNS server configuration & statistics
Admin Console
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li><a href="{{ url_for('dashboard') }}"><i
class="fa fa-dashboard"></i> Home</a></li>
<li class="active">Admin Console</li>
</ol>
</section>
@ -35,9 +36,10 @@
<tbody>
{% for statistic in statistics %}
<tr class="odd gradeX">
<td><a href="https://google.com/search?q=site:doc.powerdns.com+{{ statistic['name'] }}"
target="_blank" class="btn btn-flat btn-xs blue"><i
class="fa fa-search"></i></a></td>
<td><a
href="https://google.com/search?q=site:doc.powerdns.com+{{ statistic['name'] }}"
target="_blank" class="btn btn-flat btn-xs blue"><i
class="fa fa-search"></i></a></td>
<td>{{ statistic['name'] }}</td>
<td>{{ statistic['value'] }}</td>
</tr>
@ -70,14 +72,15 @@
<tbody>
{% for config in configs %}
<tr class="odd gradeX">
<td><a href="https://google.com/search?q=site:doc.powerdns.com+{{ config['name'] }}"
target="_blank" class="btn btn-flat btn-xs blue"><i
class="fa fa-search"></i></a></td>
<td><a
href="https://google.com/search?q=site:doc.powerdns.com+{{ config['name'] }}"
target="_blank" class="btn btn-flat btn-xs blue"><i
class="fa fa-search"></i></a></td>
<td>{{ config['name'] }}</td>
<td>
{{ config['value'] }}
</td>
</tr>
<td>
{{ config['value'] }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
@ -93,24 +96,24 @@
{% endblock %}
{% block extrascripts %}
<script>
// set up statistics data table
$("#tbl_statistics").DataTable({
"paging": true,
"lengthChange": false,
"searching": true,
"ordering": true,
"info": true,
"autoWidth": false
});
// set up statistics data table
$("#tbl_statistics").DataTable({
"paging" : true,
"lengthChange" : false,
"searching" : true,
"ordering" : true,
"info" : true,
"autoWidth" : false
});
// set up configuration data table
$("#tbl_configuration").DataTable({
"paging": true,
"lengthChange": false,
"searching": true,
"ordering": true,
"info": true,
"autoWidth": false
});
// set up configuration data table
$("#tbl_configuration").DataTable({
"paging" : true,
"lengthChange" : false,
"searching" : true,
"ordering" : true,
"info" : true,
"autoWidth" : false
});
</script>
{% endblock %}

View file

@ -0,0 +1,119 @@
{% extends "base.html" %}
{% set active_page = "admin_accounts" %}
{% block title %}<title>Edit Account - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
Account
<small>{% if create %}New account{% else %}{{ account.name }}{% endif %}</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li><a href="{{ url_for('admin_manageaccount') }}">Accounts</a></li>
<li class="active">{% if create %}Add{% else %}Edit{% endif %} account</li>
</ol>
</section>
{% endblock %}
{% block content %}
<section class="content">
<div class="row">
<div class="col-md-4">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">{% if create %}Add{% else %}Edit{% endif %} account</h3>
</div>
<!-- /.box-header -->
<!-- form start -->
<form role="form" method="post" action="{% if create %}{{ url_for('admin_editaccount') }}{% else %}{{ url_for('admin_editaccount', account_name=account.name) }}{% endif %}">
<input type="hidden" name="create" value="{{ create }}">
<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 {% if invalid_accountname or duplicate_accountname %}has-error{% endif %}">
<label class="control-label" for="accountname">Name</label>
<input type="text" class="form-control" placeholder="Account Name (required)"
name="accountname" {% if account %}value="{{ account.name }}"{% endif %} {% if not create %}disabled{% endif %}>
<span class="fa fa-cog form-control-feedback"></span>
{% if invalid_accountname %}
<span class="help-block">Cannot be blank and must only contain alphanumeric characters.</span>
{% elif duplicate_accountname %}
<span class="help-block">Account name already in use.</span>
{% endif %}
</div>
<div class="form-group has-feedback">
<label class="control-label" for="accountdescription">Description</label>
<input type="text" class="form-control" placeholder="Account Description (optional)"
name="accountdescription" {% if account %}value="{{ account.description }}"{% endif %}>
<span class="fa fa-industry form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<label class="control-label" for="accountcontact">Contact Person</label>
<input type="text" class="form-control" placeholder="Contact Person (optional)"
name="accountcontact" {% if account %}value="{{ account.contact }}"{% endif %}>
<span class="fa fa-user form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<label class="control-label" for="accountmail">Mail Address</label>
<input type="email" class="form-control" placeholder="Mail Address (optional)"
name="accountmail" {% if account %}value="{{ account.mail }}"{% endif %}>
<span class="fa fa-envelope form-control-feedback"></span>
</div>
</div>
<div class="box-header with-border">
<h3 class="box-title">Access Control</h3>
</div>
<div class="box-body">
<p>Users on the right have access to manage records in all domains
associated with the account.</p>
<p>Click on users to move between columns.</p>
<div class="form-group col-xs-2">
<select multiple="multiple" class="form-control" id="account_multi_user" name="account_multi_user">
{% for user in users %}
<option {% if user.id in account_user_ids %}selected{% endif %} value="{{ user.username }}">{{ user.username }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-flat btn-primary">{% if create %}Create{% else %}Update{% endif %} Account</button>
</div>
</form>
</div>
</div>
<div class="col-md-8">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Help with creating a new account</h3>
</div>
<div class="box-body">
<p>
An account allows grouping of domains belonging to a particular entity, such as a customer or department.<br/>
A domain can be assigned to an account upon domain creation or through the domain administration page.
</p>
<p>Fill in all the fields to the in the form to the left.</p>
<p>
<strong>Name</strong> is an account identifier. It will be stored as all lowercase letters (no spaces, special characters etc).<br/>
<strong>Description</strong> is a user friendly name for this account.<br/>
<strong>Contact person</strong> is the name of a contact person at the account.<br/>
<strong>Mail Address</strong> is an e-mail address for the contact person.
</p>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extrascripts %}
<script>
$("#account_multi_user").multiSelect();
</script>
{% endblock %}

View file

@ -0,0 +1,154 @@
{% extends "base.html" %}
{% set active_page = "admin_users" %}
{% block title %}<title>Edit Use - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
User
<small>{% if create %}New user{% else %}{{ user.username }}{% endif %}</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li><a href="{{ url_for('dashboard') }}">Admin</a></li>
<li class="active">{% if create %}Add{% else %}Edit{% endif %} user</li>
</ol>
</section>
{% endblock %}
{% block content %}
<section class="content">
<div class="row">
<div class="col-md-4">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">{% if create %}Add{% else %}Edit{% endif %} user</h3>
</div>
<!-- /.box-header -->
<!-- form start -->
<form role="form" method="post" action="{% if create %}{{ url_for('admin_edituser') }}{% else %}{{ url_for('admin_edituser', user_username=user.username) }}{% endif %}">
<input type="hidden" name="create" value="{{ create }}">
<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"
name="firstname" {% if user %}value={{ user.firstname }}{% endif %}> <span
class="glyphicon glyphicon-user form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<label class="control-label" for="lastname">Last Name</label>
<input type="text" class="form-control" placeholder="Last name"
name="lastname" {% if user %}value={{ user.lastname }}{% endif %}> <span
class="glyphicon glyphicon-user form-control-feedback"></span>
</div>
<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>
</div>
<p class="login-box-msg">Enter the account details below</p>
<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 %} {% if not create %}disabled{% endif %}> <span
class="glyphicon glyphicon-user form-control-feedback"></span>
</div>
<div class="form-group has-feedback {% if blank_password %}has-error{% endif %}">
<label class="control-label" for="username">Password</label>
<input type="password" class="form-control" placeholder="Password {% if create %}(Required){% else %}(Leave blank to keep unchanged){% endif %}"
name="password"> <span
class="glyphicon glyphicon-lock form-control-feedback"></span>
{% if blank_password %}
<span class="help-block">The password cannot be blank.</span>
{% endif %}
</div>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-flat btn-primary">{% if create %}Create{% else %}Update{% endif %} User</button>
</div>
</form>
</div>
{% if not create %}
<div class="box box-secondary">
<div class="box-header with-border">
<h3 class="box-title">Two Factor Authentication</h3>
</div>
<div class="box-body">
<p>If two factor authentication was configured and is causing problems due to a lost device or technical issue, it can be disabled here.</p>
<p>The user will need to reconfigure two factor authentication, to re-enable it.</p>
<p><strong>Beware: This could compromise security!</strong></p>
</div>
<div class="box-footer">
<button type="button" class="btn btn-flat btn-warning button_otp_disable" id="{{ user.username }}" {% if not user.otp_secret %}disabled{% endif %}>Disable Two Factor Authentication</button>
</div>
</div>
{% endif %}
</div>
<div class="col-md-8">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Help with {% if create %}creating a new{% else%}updating a{% endif %} user</h3>
</div>
<div class="box-body">
<p>Fill in all the fields to the in the form to the left.</p>
{% if create %}
<p><strong>Newly created users do not have access to any domains.</strong> You will need to grant access to the user once it is created via the domain management buttons on the dashboard.</p>
{% else %}
<p><strong>Password</strong> can be left empty to keep the current password.</p>
<p><strong>Username</strong> cannot be changed.</p>
{% endif %}
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extrascripts %}
<script>
// handle disabling two factor authentication
$(document.body).on('click', '.button_otp_disable', function() {
var modal = $("#modal_otp_disable");
var username = $(this).prop('id');
var info = "Are you sure you want to disable two factor authentication for user " + username + "?";
modal.find('.modal-body p').text(info);
modal.find('#button_otp_disable_confirm').click(function() {
var postdata = {'action': 'user_otp_disable', 'data': username}
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manageuser', false, true);
})
modal.modal('show');
});
</script>
{% endblock %}
{% block modals %}
<div class="modal fade modal-warning" id="modal_otp_disable">
<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_otp_disable_confirm">Disable Two Factor Authentication</button>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,141 @@
{% extends "base.html" %}
{% set active_page = "admin_history" %}
{% block title %}
<title>History - {{ SITE_NAME }}</title>
{% endblock %} {% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
History <small>Recent PowerDNS-Admin events</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i
class="fa fa-dashboard"></i> Home</a></li>
<li class="active">History</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">History Management</h3>
</div>
<div class="box-body clearfix">
<button type="button" class="btn btn-flat btn-danger pull-right" data-toggle="modal" data-target="#modal_clear_history" {% if current_user.role.name != 'Administrator' %}disabled{% endif %}>
Clear History&nbsp;<i class="fa fa-trash"></i>
</button>
</div>
<div class="box-body">
<table id="tbl_history" class="table table-bordered table-striped">
<thead>
<tr>
<th>Changed by</th>
<th>Content</th>
<th>Time</th>
<th>Detail</th>
</tr>
</thead>
<tbody>
{% for history in histories %}
<tr class="odd gradeX">
<td>{{ history.created_by }}</td>
<td>{{ history.msg }}</td>
<td>{{ history.created_on }}</td>
<td width="6%">
<button type="button" class="btn btn-flat btn-primary history-info-button" value='{{ history.detail }}'>Info&nbsp;<i class="fa fa-info"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</section>
{% endblock %}
{% block extrascripts %}
<script>
// set up history data table
$("#tbl_history").DataTable({
"paging" : true,
"lengthChange" : false,
"searching" : true,
"ordering" : true,
"info" : true,
"autoWidth" : false,
"order": [[ 2, "desc" ]],
"columnDefs": [
{
"type": "time",
"render": function ( data, type, row ) {
return moment.utc(data).local().format('YYYY-MM-DD HH:mm:ss');
},
"targets": 2
}
]
});
$(document.body).on('click', '.history-info-button', function() {
var modal = $("#modal_history_info");
var info = $(this).val();
$('#modal-code-content').html(json_library.prettyPrint(info));
modal.modal('show');
});
</script>
{% endblock %}
{% block modals %}
<!-- Clear History Confirmation Box -->
<div class="modal fade modal-warning" id="modal_clear_history">
<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>Are you sure you want to remove all history?</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" onclick="applyChanges('', $SCRIPT_ROOT + '/admin/history', false, true);">Clear History</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<div class="modal fade" id="modal_history_info">
<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">History Details</h4>
</div>
<div class="modal-body">
<pre><code id="modal-code-content"></code></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-right"
data-dismiss="modal">Close</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
{% endblock %}

View file

@ -8,7 +8,8 @@
Accounts <small>Manage accounts</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li><a href="{{ url_for('dashboard') }}"><i
class="fa fa-dashboard"></i> Home</a></li>
<li class="active">Accounts</li>
</ol>
</section>
@ -21,7 +22,7 @@
<h3 class="box-title">Account Management</h3>
</div>
<div class="box-body">
<a href="{{ url_for('admin.edit_account') }}">
<a href="{{ url_for('admin_editaccount') }}">
<button type="button" class="btn btn-flat btn-primary pull-left button_add_account">
Add Account&nbsp;<i class="fa fa-plus"></i>
</button>
@ -35,8 +36,6 @@
<th>Description</th>
<th>Contact</th>
<th>Mail</th>
<th>Member</th>
<th>Domain</th>
<th>Action</th>
</tr>
</thead>
@ -47,15 +46,11 @@
<td>{{ account.description }}</td>
<td>{{ account.contact }}</td>
<td>{{ account.mail }}</td>
<td>{{ account.user_num }}</td>
<td>{{ account.domains|length }}</td>
<td width="15%">
<button type="button" class="btn btn-flat btn-success"
onclick="window.location.href='{{ url_for('admin.edit_account', account_name=account.name) }}'">
<button type="button" class="btn btn-flat btn-success" onclick="window.location.href='{{ url_for('admin_editaccount', account_name=account.name) }}'">
Edit&nbsp;<i class="fa fa-cog"></i>
</button>
<button type="button" class="btn btn-flat btn-danger button_delete"
id="{{ account.name }}">
<button type="button" class="btn btn-flat btn-danger button_delete" id="{{ account.name }}">
Delete&nbsp;<i class="fa fa-trash"></i>
</button>
</td>
@ -72,45 +67,39 @@
</div>
<!-- /.row -->
</section>
{% endblock %}
{% endblock %}
{% block extrascripts %}
<script>
// set up accounts data table
$("#tbl_accounts").DataTable({
"paging": true,
"lengthChange": true,
"searching": true,
"ordering": true,
"columnDefs": [{
"orderable": false,
"targets": [-1]
}],
"info": false,
"autoWidth": false,
"lengthMenu": [
[10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"]
"paging" : true,
"lengthChange" : true,
"searching" : true,
"ordering" : true,
"columnDefs": [
{ "orderable": false, "targets": [-1] }
],
"info" : false,
"autoWidth" : false,
"lengthMenu": [ [10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"]],
"pageLength": 10
});
// handle deletion of account
$(document.body).on('click', '.button_delete', function () {
$(document.body).on('click', '.button_delete', function() {
var modal = $("#modal_delete");
var accountname = $(this).prop('id');
var info = "Are you sure you want to delete " + accountname + "?";
var info = "Are you sure you want to delete " + accountname + "?";
modal.find('.modal-body p').text(info);
modal.find('#button_delete_confirm').click(function () {
var postdata = {
'action': 'delete_account',
'data': accountname,
'_csrf_token': '{{ csrf_token() }}'
}
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manage-account', false, true);
modal.find('#button_delete_confirm').click(function() {
var postdata = {'action': 'delete_account', 'data': accountname}
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manageaccount', false, true);
modal.modal('hide');
})
modal.modal('show');
});
</script>
{% endblock %}
{% block modals %}
@ -118,7 +107,8 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Confirmation</h4>
@ -127,7 +117,8 @@
<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-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>

View file

@ -8,7 +8,8 @@
User <small>Manage user privileges</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li><a href="{{ url_for('dashboard') }}"><i
class="fa fa-dashboard"></i> Home</a></li>
<li class="active">User</li>
</ol>
</section>
@ -21,7 +22,7 @@
<h3 class="box-title">User Management</h3>
</div>
<div class="box-body">
<a href="{{ url_for('admin.edit_user') }}">
<a href="{{ url_for('admin_edituser') }}">
<button type="button" class="btn btn-flat btn-primary pull-left button_add_user">
Add User&nbsp;<i class="fa fa-plus"></i>
</button>
@ -48,30 +49,22 @@
<td>{{ user.lastname }}</td>
<td>{{ user.email }}</td>
<td>
<select id="{{ user.username }}" class="user_role"
{% if user.username==current_user.username or (current_user.role.name=='Operator' and user.role.name=='Administrator') %}disabled{% endif %}>
<select id="{{ user.username }}" class="user_role" {% if user.username==current_user.username or (current_user.role.name=='Operator' and user.role.name=='Administrator') %}disabled{% endif %}>
{% for role in roles %}
<option value="{{ role.name }}"
{% if role.id==user.role.id %}selected{% endif %}>{{ role.name }}</option>
<option value="{{ role.name }}" {% if role.id==user.role.id %}selected{% endif %}>{{ role.name }}</option>
{% endfor %}
</select>
</td>
<td width="6%">
<button type="button" class="btn btn-flat btn-warning button_revoke"
id="{{ user.username }}"
{% if current_user.role.name=='Operator' and user.role.name=='Administrator' %}disabled{% endif %}>
<button type="button" class="btn btn-flat btn-warning button_revoke" id="{{ user.username }}" {% if current_user.role.name=='Operator' and user.role.name=='Administrator' %}disabled{% endif %}>
Revoke&nbsp;<i class="fa fa-lock"></i>
</button>
</td>
<td width="15%">
<button type="button" class="btn btn-flat btn-success button_edit"
onclick="window.location.href='{{ url_for('admin.edit_user', user_username=user.username) }}'"
{% if current_user.role.name=='Operator' and user.role.name=='Administrator' %}disabled{% endif %}>
<button type="button" class="btn btn-flat btn-success button_edit" onclick="window.location.href='{{ url_for('admin_edituser', user_username=user.username) }}'" {% if current_user.role.name=='Operator' and user.role.name=='Administrator' %}disabled{% endif %}>
Edit&nbsp;<i class="fa fa-lock"></i>
</button>
<button type="button" class="btn btn-flat btn-danger button_delete"
id="{{ user.username }}"
{% if user.username==current_user.username or (current_user.role.name=='Operator' and user.role.name=='Administrator') %}disabled{% endif %}>
<button type="button" class="btn btn-flat btn-danger button_delete" id="{{ user.username }}" {% if user.username==current_user.username or (current_user.role.name=='Operator' and user.role.name=='Administrator') %}disabled{% endif %}>
Delete&nbsp;<i class="fa fa-trash"></i>
</button>
</td>
@ -88,74 +81,62 @@
</div>
<!-- /.row -->
</section>
{% endblock %}
{% endblock %}
{% block extrascripts %}
<script>
// set up user data table
$("#tbl_users").DataTable({
"paging": true,
"lengthChange": true,
"searching": true,
"ordering": true,
"info": false,
"autoWidth": false,
"lengthMenu": [
[10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"]
],
"paging" : true,
"lengthChange" : true,
"searching" : true,
"ordering" : true,
"info" : false,
"autoWidth" : false,
"lengthMenu": [ [10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"]],
"pageLength": 10
});
// handle revocation of privileges
$(document.body).on('click', '.button_revoke', function () {
$(document.body).on('click', '.button_revoke', function() {
var modal = $("#modal_revoke");
var username = $(this).prop('id');
var info = "Are you sure you want to revoke all privileges for " + username +
". They will not able to access any domain.";
var info = "Are you sure you want to revoke all privileges for " + username + ". They will not able to access any domain.";
modal.find('.modal-body p').text(info);
modal.find('#button_revoke_confirm').click(function () {
var postdata = {
'action': 'revoke_user_privileges',
'data': username,
'_csrf_token': '{{ csrf_token() }}'
}
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manage-user', true);
modal.find('#button_revoke_confirm').click(function() {
var postdata = {'action': 'revoke_user_privielges', 'data': username}
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manageuser');
modal.modal('hide');
})
modal.modal('show');
});
// handle deletion of user
$(document.body).on('click', '.button_delete', function () {
$(document.body).on('click', '.button_delete', function() {
var modal = $("#modal_delete");
var username = $(this).prop('id');
var info = "Are you sure you want to delete " + username + "?";
var info = "Are you sure you want to delete " + username + "?";
modal.find('.modal-body p').text(info);
modal.find('#button_delete_confirm').click(function () {
var postdata = {
'action': 'delete_user',
'data': username,
'_csrf_token': '{{ csrf_token() }}'
}
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manage-user', false, true);
modal.find('#button_delete_confirm').click(function() {
var postdata = {'action': 'delete_user', 'data': username}
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manageuser', false, true);
modal.modal('hide');
})
modal.modal('show');
});
// handle user role changing
$(document.body).on('change', '.user_role', function () {
$('.user_role').on('change', function() {
var role_name = this.value;
var username = $(this).prop('id');
var postdata = {
'action': 'update_user_role',
'data': {
'username': username,
'role_name': role_name
},
'_csrf_token': '{{ csrf_token() }}'
'action' : 'update_user_role',
'data' : {
'username' : username,
'role_name' : role_name
}
};
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manage-user', showResult = true);
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manageuser', showResult=true);
});
</script>
{% endblock %}
@ -164,7 +145,8 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Confirmation</h4>
@ -173,7 +155,8 @@
<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-default pull-left"
data-dismiss="modal">Close</button>
<button type="button" class="btn btn-flat btn-danger" id="button_revoke_confirm">Revoke</button>
</div>
</div>
@ -185,7 +168,8 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Confirmation</h4>
@ -194,7 +178,8 @@
<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-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>

View file

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

View file

@ -9,7 +9,7 @@
Settings <small>PowerDNS-Admin settings</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li><a href="#">Setting</a></li>
<li class="active">Basic</li>
</ol>
@ -38,16 +38,14 @@
{% if SETTING.get(setting) in [True, False] %}
<td>{{ SETTING.get(setting)|display_setting_state }}</td>
<td width="6%">
<button type="button" class="btn btn-flat btn-warning setting-toggle-button"
id="{{ setting }}">
<button type="button" class="btn btn-flat btn-warning setting-toggle-button" id="{{ setting }}">
Toggle&nbsp;<i class="fa fa-info"></i>
</button>
</td>
{% else %}
<td><input name="value" id="value" value="{{ SETTING.get(setting) }}"></td>
<td width="6%">
<button type="button" class="btn btn-flat btn-warning setting-save-button"
id="{{ setting }}">
<button type="button" class="btn btn-flat btn-warning setting-save-button" id="{{ setting }}">
Save&nbsp;<i class="fa fa-info"></i>
</button>
</td>
@ -65,33 +63,28 @@
</div>
<!-- /.row -->
</section>
{% endblock %}
{% endblock %}
{% block extrascripts %}
<script>
// set up history data table
$("#tbl_settings").DataTable({
"paging": false,
"lengthChange": false,
"searching": true,
"ordering": true,
"info": true,
"autoWidth": false
"paging" : false,
"lengthChange" : false,
"searching" : true,
"ordering" : true,
"info" : true,
"autoWidth" : false
});
$(document.body).on('click', '.setting-toggle-button', function () {
$(document.body).on('click', '.setting-toggle-button', function() {
var setting = $(this).prop('id');
applyChanges({
'_csrf_token': '{{ csrf_token() }}'
}, $SCRIPT_ROOT + '/admin/setting/basic/' + setting + '/toggle', false, true)
applyChanges('', $SCRIPT_ROOT + '/admin/setting/basic/' + setting + '/toggle', false, true)
});
$(document.body).on('click', '.setting-save-button', function () {
$(document.body).on('click', '.setting-save-button', function() {
var setting = $(this).prop('id');
var value = $(this).parents('tr').find('#value')[0].value;
var postdata = {
'value': value,
'_csrf_token': '{{ csrf_token() }}'
};
var postdata = {'value': value};
applyChanges(postdata, $SCRIPT_ROOT + '/admin/setting/basic/' + setting + '/edit', false, true)
});
</script>
{% endblock %}
{% endblock %}

View file

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

View file

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

241
app/templates/base.html Normal file
View file

@ -0,0 +1,241 @@
<!DOCTYPE html>
<html>
<head>
{% block head %}
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
{% block title %}<title>{{ SITE_NAME }}</title>{% endblock %}
<!-- Get Google Fonts we like -->
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto+Mono:400,300,700">
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
{% assets "css_main" -%}
<link rel="stylesheet" href="{{ ASSET_URL }}">
{%- endassets %}
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
{% endblock %}
</head>
<body class="hold-transition skin-blue sidebar-mini {% if not SETTING.get('fullscreen_layout') %}layout-boxed{% endif %}">
<div class="wrapper">
{% block pageheader %}
<header class="main-header">
<!-- Logo -->
<a href="{{ url_for('index') }}" class="logo">
<!-- mini logo for sidebar mini 50x50 pixels -->
<span class="logo-mini"><b>PD</b>A</span>
<!-- logo for regular state and mobile devices -->
<span class="logo-lg"><b>PowerDNS</b>-Admin</span>
</a>
<!-- Header Navbar: style can be found in header.less -->
<nav class="navbar navbar-static-top">
<!-- Sidebar toggle button-->
<a href="#" class="sidebar-toggle" data-toggle="push-menu" role="button">
<span class="sr-only">Toggle navigation</span>
</a>
<div class="navbar-custom-menu">
{% if current_user.id is defined %}
<ul class="nav navbar-nav">
<!-- User Account: style can be found in dropdown.less -->
<li class="dropdown user user-menu">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
{% if current_user.avatar %}
<img src="{{ url_for('user_avatar', filename=current_user.avatar) }}" class="user-image" alt="User Image"/>
{% else %}
<img src="{{ current_user.email|email_to_gravatar_url(size=80) }}" class="user-image" alt="User Image"/>
{% endif %}
<span class="hidden-xs">
{{ current_user.firstname }}
</span>
</a>
<ul class="dropdown-menu">
<li class="user-header">
{% if current_user.avatar %}
<img src="{{ url_for('user_avatar', filename=current_user.avatar) }}" class="img-circle" alt="User Image"/>
{% else %}
<img src="{{ current_user.email|email_to_gravatar_url(size=160) }}" class="img-circle" alt="User Image"/>
{% endif %}
<p>
{{ current_user.firstname }} {{ current_user.lastname }}
<small>{{ current_user.role.name }}</small>
</p>
</li>
<!-- Menu Footer-->
<li class="user-footer">
<div class="pull-left">
<a href="{{ url_for('user_profile') }}" class="btn btn-flat btn-primary">My Profile</a>
</div>
<div class="pull-right">
<a href="{{ url_for('logout') }}" class="btn btn-flat btn-warning">Log out</a>
</div>
</li>
</ul>
</li>
</ul>
{% endif %}
</div>
</nav>
</header>
{% endblock %}
<!-- Left side column. contains the logo and sidebar -->
<aside class="main-sidebar">
<!-- sidebar: style can be found in sidebar.less -->
<section class="sidebar">
{% if current_user.id is defined %}
<div class="user-panel">
<div class="pull-left image">
{% if current_user.avatar %}
<img src="{{ url_for('user_avatar', filename=current_user.avatar) }}" class="img-circle" alt="User Image"/>
{% else %}
<img src="{{ current_user.email|email_to_gravatar_url(size=100) }}" class="img-circle" alt="User Image"/>
{% endif %}
</div>
<div class="pull-left info">
<p>{{ current_user.firstname }} {{ current_user.lastname }}</p>
<a href="#"><i class="fa fa-circle text-success"></i> Logged In</a>
</div>
</div>
<!-- sidebar menu: : style can be found in sidebar.less -->
<ul class="sidebar-menu" data-widget="tree">
<li class="header">USER ACTIONS</li>
<li class="{{ 'active' if active_page == 'dashboard' else '' }}">
<a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i> Dashboard</a>
</li>
{% if SETTING.get('allow_user_create_domain') or current_user.role.name in ['Administrator', 'Operator'] %}
<li class="{{ 'active' if active_page == 'new_domain' else '' }}">
<a href="{{ url_for('domain_add') }}"><i class="fa fa-plus"></i> New Domain</a>
</li>
{% endif %}
{% if current_user.role.name in ['Administrator', 'Operator'] %}
<li class="header">ADMINISTRATION</li>
<li class="{{ 'active' if active_page == 'admin_console' else '' }}">
<a href="{{ url_for('admin_pdns') }}"><i class="fa fa-info-circle"></i> PDNS</a>
</li>
<li class="{{ 'active' if active_page == 'admin_history' else '' }}">
<a href="{{ url_for('admin_history') }}"><i class="fa fa-calendar"></i> History</a>
</li>
<li class="{{ 'active' if active_page == 'admin_domain_template' else '' }}">
<a href="{{ url_for('templates') }}"><i class="fa fa-clone"></i> Domain Templates</a>
</li>
<li class="{{ 'active' if active_page == 'admin_accounts' else '' }}">
<a href="{{ url_for('admin_manageaccount') }}"><i class="fa fa-industry"></i> Accounts</a>
</li>
<li class="{{ 'active' if active_page == 'admin_users' else '' }}">
<a href="{{ url_for('admin_manageuser') }}"><i class="fa fa-users"></i> Users</a>
</li>
<li class="{{ 'treeview active' if active_page == 'admin_settings' else 'treeview' }}">
<a href="#">
<i class="fa fa-cog"></i> Settings
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu" {% if active_page == 'admin_settings' %}style="display: block;"{% endif %}>
<li><a href="{{ url_for('admin_setting_basic') }}"><i class="fa fa-circle-o"></i></i> Basic</a></li>
<li><a href="{{ url_for('admin_setting_records') }}"><i class="fa fa-circle-o"></i> Records</a></li>
{% if current_user.role.name == 'Administrator' %}
<li><a href="{{ url_for('admin_setting_pdns') }}"><i class="fa fa-circle-o"></i> PDNS</a></li>
<li><a href="{{ url_for('admin_setting_authentication') }}"><i class="fa fa-circle-o"></i> Authentication</a></li>
{% endif %}
</ul>
</li>
{% endif %}
</ul>
{% endif %}
</section>
<!-- /.sidebar -->
</aside>
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
Dashboard
<small>Control panel</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li class="active">Dashboard</li>
</ol>
</section>
{% endblock %}
{% block content %}
{% endblock %}
</div>
<!-- /.content-wrapper -->
<footer class="main-footer">
<strong><a href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</a></strong> - A PowerDNS web interface with advanced features.
</footer>
</div>
<!-- ./wrapper -->
<script type="text/javascript">
$SCRIPT_ROOT = {{ request.script_root|tojson|safe }};
</script>
{% block scripts %}
{% assets "js_main" -%}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{%- endassets %}
{% endblock %}
{% block extrascripts %}
{% endblock %}
{% block defaultmodals %}
<div class="modal fade modal-danger" id="modal_error">
<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">Error</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>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
<div class="modal fade modal-success" id="modal_success">
<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">Success</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>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
{% endblock %}
{% block modals %}
{% endblock %}
</body>
</html>

View file

@ -2,7 +2,6 @@
{% set active_page = "dashboard" %}
{% block title %}<title>Dashboard - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
@ -11,18 +10,16 @@
<small>Info</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li class="active">Dashboard</li>
</ol>
</section>
{% endblock %}
{% import 'applied_change_macro.html' as applied_change_macro %}
{% block content %}
<!-- Main content -->
<section class="content">
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
{% if current_user.role.name in ['Administrator', 'Operator'] %}
<div class="row">
<div class="col-xs-3">
<div class="box">
@ -43,13 +40,12 @@
</div>
</div>
</div>
{% if current_user.role.name in ['Administrator', 'Operator'] %}
<div class="col-lg-6">
<a href="{{ url_for('admin.manage_user') }}">
<a href="{{ url_for('admin_manageuser') }}">
<div class="small-box bg-green">
<div class="inner">
<h3>{{ user_num }}</h3>
<p>{% if user_num > 1 %}Users{% else %}User{% endif %}</p>
<h3>{{ users|length }}</h3>
<p>{% if users|length > 1 %}Users{% else %}User{% endif %}</p>
</div>
<div class="icon">
<i class="fa fa-users"></i>
@ -57,11 +53,10 @@
</div>
</a>
</div>
{% endif %}
</div>
<div class="row">
<div class="col-lg-6">
<a href="{{ url_for('admin.history') }}">
<a href="{{ url_for('admin_history') }}">
<div class="small-box bg-green">
<div class="inner">
<h3>{{ history_number }}</h3>
@ -73,9 +68,8 @@
</div>
</a>
</div>
{% if current_user.role.name in ['Administrator', 'Operator'] %}
<div class="col-lg-6">
<a href="{{ url_for('admin.pdns_stats') }}">
<a href="{{ url_for('admin_pdns') }}">
<div class="small-box bg-green">
<div class="inner">
<h3><span style="font-size: 18px">{{ uptime|display_second_to_time }}</span></h3>
@ -87,7 +81,6 @@
</div>
</a>
</div>
{% endif %}
</div>
</div>
</div>
@ -110,25 +103,13 @@
<tbody>
{% for history in histories %}
<tr class="odd">
<td>{{ history.history.created_by }}</td>
<td>{{ history.history.msg }}</td>
<td>{{ history.history.created_on }}</td>
<td>{{ history.created_by }}</td>
<td>{{ history.msg }}</td>
<td>{{ history.created_on }}</td>
<td width="6%">
<div id="history-info-div-{{ loop.index0 }}" style="display: none;">
{{ history.detailed_msg | safe }}
{% if history.change_set %}
<div class="content">
<div id="change_index_definition"></div>
{% call applied_change_macro.applied_change_template(history.change_set) %}
{% endcall %}
</div>
{% endif %}
</div>
<button type="button" class="btn btn-flat btn-primary history-info-button"
{% if history.detailed_msg == "" and history.change_set is none %}
style="visibility: hidden;"
{% endif %} value="{{ loop.index0 }}">Info&nbsp;<i class="fa fa-info"></i>
</button>
<button type="button" class="btn btn-flat btn-primary history-info-button" value='{{ history.detail|replace("[]","None") }}'>
Info&nbsp;<i class="fa fa-info"></i>
</button>
</td>
</tr>
{% endfor %}
@ -139,90 +120,43 @@
</div>
</div>
{% endif %}
<!--SYBPATCH START-->
<div class="nav-tabs-custom">
<ul class="nav nav-tabs">
<li class="active"><a href="#tab_{{custom_boxes.order[0]}}" data-toggle="tab">Hosted Domains <b>{{custom_boxes.boxes[custom_boxes.order[0]][0]}}</b></a></li>
{% for boxId in custom_boxes.order[1:] %}
<li><a href="#tab_{{boxId}}" data-toggle="tab">Hosted Domains <b>{{custom_boxes.boxes[boxId][0]}}</b></a></li>
{% endfor %}
</ul>
<div class="tab-content">
{% for boxId in custom_boxes.order %}
<div class="tab-pane" id='tab_{{boxId}}'>
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Hosted Domains <b>{{custom_boxes.boxes[boxId][0]}}</b></h3>{% if show_bg_domain_button %}<button type="button" class="btn btn-flat btn-primary refresh-bg-button pull-right"><i class="fa fa-refresh"></i> Sync domains </button>{% endif %}
</div>
<div class="box-body">
<table id='tbl_domain_list_{{boxId}}' class="table table-bordered table-striped">
<thead>
<tr>
<th>Name</th>
<th>DNSSEC</th>
<th>Type</th>
<th>Serial</th>
<th>Master</th>
<th>Account</th>
<th {% if current_user.role.name not in ['Administrator','Operator'] %}width="6%"{% else %}width="25%"{% endif %}>Action</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div><!-- /.tab-content -->
</div><!-- custom tabs -->
<!--SYBPATCH END-->
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Hosted Domains</h3>{% if show_bg_domain_button %}<button type="button" class="btn btn-flat btn-primary refresh-bg-button pull-right"><i class="fa fa-refresh"></i> Sync domains </button>{% endif %}
</div>
<div class="box-body">
<table id="tbl_domain_list" class="table table-bordered table-striped">
<thead>
<tr>
<th>Name</th>
<th>DNSSEC</th>
<th>Type</th>
<th>Serial</th>
<th>Master</th>
<th>Account</th>
<th {% if current_user.role.name not in ['Administrator','Operator'] %}width="6%"{% else %}width="25%"{% endif %}>Action</th>
</tr>
</thead>
<tbody>
<!-- Content loaded via AJAX. -->
</tbody>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</section>
<!-- /.content -->
{% endblock %}
{% block extrascripts %}
<script>
//SYBPATCH START//
function setUpDomainList(id ,url){
$(id).DataTable({
"paging" : true,
"lengthChange" : true,
language: {
searchPlaceholder: "Use ^ and $ for start and end",
},
"searching" : true,
"ordering" : true,
"columnDefs": [
{ "orderable": false, "targets": [-1] }
{% if current_user.role.name not in ['Administrator', 'Operator'] %},{ "visible": false, "targets": [-2] }{% endif %}
],
"processing" : true,
"serverSide" : true,
"ajax" : url,
"info" : false,
"autoWidth" : false,
{% if SETTING.get('default_domain_table_size')|string in ['10','25','50','100'] %}
"lengthMenu": [ [10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"]],
{% else %}
"lengthMenu": [ [10, 25, 50, 100, {{ SETTING.get('default_domain_table_size') }}, -1],
[10, 25, 50, 100, {{ SETTING.get('default_domain_table_size') }}, "All"]],
{% endif %}
"pageLength": {{ SETTING.get('default_domain_table_size') }}
});
}
$('#tab_{{custom_boxes.order[0]}}').addClass( "active" );
{% for boxId in custom_boxes.order %}
setUpDomainList("#tbl_domain_list_{{boxId}}", "{{url_for('dashboard.domains_custom',boxId=boxId)}}");
{% endfor %}
//SYBPATCH END//
PDNS_VERSION = '{{ SETTING.get("pdns_version") }}';
// set up history data table
$("#tbl_history").DataTable({
"paging" : false,
@ -240,19 +174,41 @@
}
]
});
$(document.body).on('click', '.history-info-button', function () {
// set up domain list
$("#tbl_domain_list").DataTable({
"paging" : true,
"lengthChange" : true,
"searching" : true,
"ordering" : true,
"columnDefs": [
{ "orderable": false, "targets": [-1] }
{% if current_user.role.name not in ['Administrator', 'Operator'] %},{ "visible": false, "targets": [-2] }{% endif %}
],
"processing" : true,
"serverSide" : true,
"ajax" : "{{ url_for('dashboard_domains') }}",
"info" : false,
"autoWidth" : false,
{% if SETTING.get('default_domain_table_size') in ['10','25','50','100'] %}
"lengthMenu": [ [10, 25, 50, 100, -1],
[10, 25, 50, 100, "All"]],
{% else %}
"lengthMenu": [ [10, 25, 50, 100, {{ SETTING.get('default_domain_table_size') }}, -1],
[10, 25, 50, 100, {{ SETTING.get('default_domain_table_size') }}, "All"]],
{% endif %}
"pageLength": {{ SETTING.get('default_domain_table_size') }}
});
$(document.body).on('click', '.history-info-button', function() {
var modal = $("#modal_history_info");
var history_id = $(this).val();
var info = $("#history-info-div-" + history_id).html();
$('#modal-info-content').html(info);
var info = $(this).val();
$('#modal-code-content').html(json_library.prettyPrint(info));
modal.modal('show');
});
$(document.body).on('click', '.refresh-bg-button', function() {
var modal = $("#modal_bg_reload");
modal.modal('show');
reload_domains($SCRIPT_ROOT + '/dashboard/domains-updater');
reload_domains($SCRIPT_ROOT + '/dashboard-domains-updater');
});
$(document.body).on("click", ".button_template", function (e) {
@ -264,19 +220,19 @@
<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 = {'_csrf_token': '{{ csrf_token() }}'};
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, "{{ url_for('admin.create_template_from_zone') }}", true);
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');
});
@ -288,13 +244,13 @@
$(document.body).on("click", ".button_dnssec_enable", function() {
var domain = $(this).prop('id');
enable_dns_sec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec/enable', '{{ csrf_token() }}');
enable_dns_sec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec/enable');
});
$(document.body).on("click", ".button_dnssec_disable", function() {
var domain = $(this).prop('id');
enable_dns_sec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec/disable', '{{ csrf_token() }}');
enable_dns_sec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec/disable');
});
{% endif %}
@ -312,7 +268,7 @@
<h4 class="modal-title">History Details</h4>
</div>
<div class="modal-body">
<div id="modal-info-content"></div>
<pre><code id="modal-code-content"></code></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-right"

View file

@ -1,5 +1,5 @@
{% macro name(domain) %}
<a href="{{ url_for('domain.domain', domain_name=domain.name) }}"><strong>{{ domain.name | pretty_domain_name }}</strong></a>
<a href="{{ url_for('domain', domain_name=domain.name) }}"><strong>{{ domain.name }}</strong></a>
{% endmacro %}
{% macro dnssec(domain) %}
@ -15,16 +15,16 @@
{% endmacro %}
{% macro serial(domain) %}
{% if domain.serial == '0' %}{{ domain.notified_serial }}{% else %}{{ domain.serial }}{% endif %}
{% if domain.serial == 0 %}{{ domain.notified_serial }}{% else %}{{domain.serial}}{% endif %}
{% endmacro %}
{% macro master(domain) %}
{% if domain.master == '[]'%}-{% else %}{{ domain.master | display_master_name }}{% endif %}
{% if domain.master == '[]'%}N/A{% else %}{{ domain.master|display_master_name }}{% endif %}
{% endmacro %}
{% macro account(domain) %}
{% if current_user.role.name in ['Administrator', 'Operator'] %}
{{ domain.account.name if domain.account else '-' }}
{% if domain.account_description != "" %}{{ domain.account.description }} {% endif %}[{{ domain.account.name }}]
{% endif %}
{% endmacro %}
@ -34,26 +34,18 @@
<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', domain_name=domain.name) }}'">
<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.setting', domain_name=domain.name) }}'">
<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>
<button type="button" class="btn btn-flat btn-primary" onclick="window.location.href='{{ url_for('domain.changelog', domain_name=domain.name) }}'">
Changelog&nbsp;<i class="fa fa-history" aria-hidden="true"></i>
</button>
</td>
{% else %}
<td width="6%">
<button type="button" class="btn btn-flat btn-success" onclick="window.location.href='{{ url_for('domain.domain', domain_name=domain.name) }}'">
<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>
{% if allow_user_view_history %}
<button type="button" class="btn btn-flat btn-primary" onclick="window.location.href='{{ url_for('domain.changelog', domain_name=domain.name) }}'">
Changelog&nbsp;<i class="fa fa-history" aria-hidden="true"></i>
</button>
{% endif %}
</td>
{% endif %}
{% endif %}
{% endmacro %}

View file

@ -1,16 +1,16 @@
{% extends "base.html" %}
{% block title %}<title>{{ domain.name | pretty_domain_name }} - {{ SITE_NAME }}</title>{% endblock %}
{% block title %}<title>{{ domain.name }} - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<section class="content-header">
<h1>
Manage domain: <b>{{ domain.name | pretty_domain_name }}</b>
Manage domain: <b>{{ domain.name }}</b>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i
<li><a href="{{ url_for('dashboard') }}"><i
class="fa fa-dashboard"></i> Home</a></li>
<li>Domain</li>
<li class="active">{{ domain.name | pretty_domain_name }}</li>
<li class="active">{{ domain.name }}</li>
</ol>
</section>
{% endblock %}
@ -33,17 +33,6 @@
Update from Master&nbsp;<i class="fa fa-download"></i>
</button>
{% endif %}
{% if current_user.role.name in ['Administrator', 'Operator'] %}
<button type="button" style="position: relative; margin-left: 20px" class="btn btn-flat btn-primary pull-left btn-danger" onclick="window.location.href='{{ url_for('domain.setting', domain_name=domain.name) }}'">
Admin&nbsp;<i class="fa fa-cog"></i>
</button>
{% endif %}
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
<button type="button" style="position: relative; margin-left: 20px" class="btn btn-flat btn-primary button_changelog" id="{{ domain.name }}">
Changelog&nbsp;<i class="fa fa-history" aria-hidden="true"></i>
</i>
</button>
{% endif %}
</div>
<div class="box-body">
<table id="tbl_records" class="table table-bordered table-striped">
@ -54,19 +43,15 @@
<th>Status</th>
<th>TTL</th>
<th>Data</th>
<th>Comment</th>
<th>Edit</th>
<th>Delete</th>
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
<th >Changelog</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for record in records %}
<tr class="odd row_record" id="{{ domain.name }}">
<td>
{{ (record.name,domain.name) | display_record_name | pretty_domain_name }}
{{ (record.name,domain.name)|display_record_name }}
</td>
<td>
{{ record.type }}
@ -78,40 +63,29 @@
{{ record.ttl }}
</td>
<td>
{{ record.data | pretty_domain_name }}
</td>
<td>
{{ record.comment }}
{{ record.data }}
</td>
{% if domain.type != 'Slave' %}
<td width="6%">
{% if record.is_allowed_edit() %}
<button type="button" class="btn btn-flat btn-warning button_edit">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></button>
{% endif %}
{% 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_delete() %}
<button type="button" class="btn btn-flat btn-danger button_delete">Delete&nbsp;<i class="fa fa-trash"></i></button>
{% endif %}
</td>
{% 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>
{% 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>
</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>
<button type="button" class="btn btn-flat btn-warning"">&nbsp;&nbsp;<i class="fa fa-exclamation-circle"></i>&nbsp;&nbsp;</button>
</td>
{% endif %}
</td>
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
<td width="6%">
<button type="button" onclick="show_record_changelog('{{record.name}}','{{record.type}}',event)" class="btn btn-flat btn-primary">&nbsp;&nbsp;
<i class="fa fa-history" aria-hidden="true"></i>&nbsp;&nbsp;&nbsp;
</button>
</td>
{% endif %}
<!-- hidden column that we can sort on -->
<td>1</td>
</tr>
@ -131,8 +105,7 @@
{% block extrascripts %}
<script>
// superglobals
window.records_allow_edit = {{ editable_records | tojson }};
window.ttl_options = {{ ttl_options | tojson }};
window.records_allow_edit = {{ editable_records|tojson }};
window.nEditing = null;
window.nNew = false;
@ -144,7 +117,7 @@
"ordering" : true,
"info" : true,
"autoWidth" : false,
{% if SETTING.get('default_record_table_size') | string in ['5','15','20'] %}
{% if SETTING.get('default_record_table_size') in ['5','15','20'] %}
"lengthMenu": [ [5, 15, 20, -1],
[5, 15, 20, "All"]],
{% else %}
@ -163,35 +136,16 @@
},
{
// hidden column so that we can add new records on top
// regardless of whatever sorting is done. See orderFixed
// regardless of whatever sorting is done
visible: false,
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
targets: [ 9 ]
{% else %}
targets: [ 8 ]
{% endif %}
targets: [ 7 ]
},
{
className: "length-break",
targets: [ 4, 5 ]
targets: [ 4 ]
}
],
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
"orderFixed": [[9, 'asc']]
{% else %}
"orderFixed": [[8, 'asc']]
{% endif %}
});
function show_record_changelog(record_name, record_type, e) {
e.stopPropagation();
window.location.href = "/domain/{{domain.name}}/changelog/" + record_name + ".-" + record_type;
}
// handle changelog button
$(document.body).on("click", ".button_changelog", function(e) {
e.stopPropagation();
window.location.href = "/domain/{{domain.name}}/changelog";
"orderFixed": [[7, 'asc']]
});
// handle delete button
@ -246,23 +200,16 @@
// handle apply changes button
$(document.body).on("click",".button_apply_changes", function() {
if (nNew || nEditing) {
var modal = $("#modal_error");
modal.find('.modal-body p').text("Previous record not saved. Please save it before applying the changes.");
modal.modal('show');
return;
}
var modal = $("#modal_apply_changes");
var table = $("#tbl_records").DataTable();
var domain = $(this).prop('id');
var serial = $(".button_apply_changes").val();
var serial = $(".button_apply_changes").val();
var info = "Are you sure you want to apply your changes?";
modal.find('.modal-body p').text(info);
// following unbind("click") is to avoid multiple times execution
modal.find('#button_apply_confirm').unbind("click").click(function() {
var data = {'serial': serial, 'record': getTableData(table), '_csrf_token': '{{ csrf_token() }}'};
var data = {'serial': serial, 'record': getTableData(table)};
applyRecordChanges(data, domain);
modal.modal('hide');
})
@ -283,11 +230,7 @@
// add new row
var default_type = records_allow_edit[0]
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', default_type, 'Active', window.ttl_options[0][0], '', '', '', '', '', '0']);
{% else %}
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', default_type, 'Active', window.ttl_options[0][0], '', '', '', '', '0']);
{% endif %}
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', default_type, 'Active', 3600, '', '', '', '0']);
editRow($("#tbl_records").DataTable(), nRow);
document.getElementById("edit-row-focus").focus();
nEditing = nRow;
@ -320,7 +263,7 @@
//handle update_from_master button
$(document.body).on("click", ".button_update_from_master", function (e) {
var domain = $(this).prop('id');
applyChanges({'domain': domain, '_csrf_token': '{{ csrf_token() }}'}, $SCRIPT_ROOT + '/domain/' + domain + '/update', true);
applyChanges({'domain': domain}, $SCRIPT_ROOT + '/domain/' + domain + '/update');
});
{% if SETTING.get('record_helper') %}
@ -379,9 +322,6 @@
mx_server = modal.find('#mx_server').val();
mx_priority = modal.find('#mx_priority').val();
data = mx_priority + " " + mx_server;
if (data && !data.endsWith('.')) {
data = data + '.'
}
record_data.val(data);
modal.modal('hide');
})
@ -417,136 +357,60 @@
srv_port = modal.find('#srv_port').val();
srv_target = modal.find('#srv_target').val();
data = srv_priority + " " + srv_weight + " " + srv_port + " " + srv_target;
if (data && !data.endsWith('.')) {
data = data + '.'
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] + "\"> \
";
}
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=\"1209600\"> \
<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();
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');
} else if (record_type == "TLSA") {
var modal = $("#modal_custom_record");
if (record_data.val() == "") {
var form = " <label for=\"tlsa_certificate_usage\">TLSA Certificate Usage</label> \
<input type=\"text\" class=\"form-control\" name=\"tlsa_certificate_usage\" id=\"tlsa_certificate_usage\" placeholder=\"3\"> \
<label for=\"tlsa_selector\">TLSA-Selector</label> \
<input type=\"text\" class=\"form-control\" name=\"tlsa_selector\" id=\"tlsa_selector\" placeholder=\"1\"> \
<label for=\"tlsa_matching\"> TLSA Matching Type</label> \
<input type=\"text\" class=\"form-control\" name=\"tlsa_matching\" id=\"tlsa_matching\" placeholder=\"1\"> \
<label for=\"tlsa_hash\">Hash</label> \
<input type=\"text\" class=\"form-control\" name=\"tlsa_hash\" id=\"tlsa_hash\" placeholder=\"HASH\"> \
";
} else {
var parts = record_data.val().split(" ");
var form = " <label for=\"tlsa_certificate_usage\">TLSA Certificate Usage</label> \
<input type=\"text\" class=\"form-control\" name=\"tlsa_certificate_usage\" id=\"tlsa_certificate_usage\" value=\"" + parts[0] + "\"> \
<label for=\"tlsa_selector\">TLSA-Selector</label> \
<input type=\"text\" class=\"form-control\" name=\"tlsa_selector\" id=\"tlsa_selector\" value=\"" + parts[1] + "\"> \
<label for=\"tlsa_matching\"> TLSA Matching Type</label> \
<input type=\"text\" class=\"form-control\" name=\"tlsa_matching\" id=\"tlsa_matching\" value=\"" + parts[2] + "\"> \
<label for=\"tlsa_hash\">Hash</label> \
<input type=\"text\" class=\"form-control\" name=\"tlsa_hash\" id=\"tlsa_hash\" value=\"" + parts[3] + "\"> \
";
}
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
tlsa_certificate_usage = modal.find('#tlsa_certificate_usage').val();
tlsa_selector = modal.find('#tlsa_selector').val();
tlsa_matching = modal.find('#tlsa_matching').val();
tlsa_hash = modal.find('#tlsa_hash').val();
data = tlsa_certificate_usage + " " + tlsa_selector + " " + tlsa_matching + " " + tlsa_hash;
record_data.val(data);
modal.modal('hide');
})
modal.modal('show');
} else if (record_type == "TXT") {
var txt_data = record_data.val().replace(/"/g, '&quot;');
var modal = $("#modal_custom_record");
var form = " <label for=\"txt_record\">TXT Record Data</label> \
<textarea style=\"min-width: 100%;color: #333;\" placeholder=\"Your TXT record data\" rows=\"5\" id=\"txt_record\" name=\"txt_record\">" + txt_data + "</textarea> \
";
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
data = modal.find('#txt_record').val();
if (! /^".*"$/.test(data)) {
data = '"' + data + '"';
}
record_data.val(data);
modal.modal('hide');
});
modal.modal('show');
} else if (record_type == "LUA") {
var lua_type = record_data.val().split(" ")[0];
var lua_data = record_data.val().substr(record_data.val().indexOf(" ") + 1).replace(/"/g, '&quot;');
var modal = $("#modal_custom_record");
var form = " <label for=\"lua_type\">Lua Record Type</label> \
<input type=\"text\" class=\"form-control\" name=\"lua_type\" id=\"lua_type\" placeholder=\"Initial query type. Example: A, CNAME or PTR \" value=\"" + lua_type + "\"> \
<label for=\"lua_record\">Lua Record Data</label> \
<textarea style=\"min-width: 100%;color: #333;\" placeholder=\"Your LUA snippet\" rows=\"5\" id=\"lua_record\" name=\"lua_record\">" + lua_data + "</textarea> \
";
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
type = modal.find('#lua_type').val();
data = modal.find('#lua_record').val();
if (! /^".*"$/.test(data)) {
data = '"' + data + '"';
}
data = type + ' ' + data;
record_data.val(data);
modal.modal('hide');
});
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');
}
});

View file

@ -0,0 +1,162 @@
{% extends "base.html" %}
{% set active_page = "new_domain" %}
{% block title %}<title>Add Domain - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
Domain
<small>Create new</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li><a href="{{ url_for('dashboard') }}">Domain</a></li>
<li class="active">Add Domain</li>
</ol>
</section>
{% endblock %}
{% block content %}
<section class="content">
<div class="row">
<div class="col-md-4">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Create new domain</h3>
</div>
<!-- /.box-header -->
<!-- form start -->
<form role="form" method="post" action="{{ url_for('domain_add') }}">
<div class="box-body">
<div class="form-group">
<input type="text" class="form-control" name="domain_name" id="domain_name" placeholder="Enter a valid domain name (required)">
</div>
<select name="accountid" class="form-control" style="width:15em;">
<option value="0">- No Account -</option>
{% for account in accounts %}
<option value="{{ account.id }}">{{ account.name }}</option>
{% endfor %}
</select><br/>
<div class="form-group">
<label>Type</label>
<div class="radio">
<label>
<input type="radio" name="radio_type" id="radio_type_native" value="native" checked> Native
</label>
&nbsp;&nbsp;&nbsp;
<label>
<input type="radio" name="radio_type" id="radio_type_master" value="master"> Master
</label>
&nbsp;&nbsp;&nbsp;
<label>
<input type="radio" name="radio_type" id="radio_type_slave" value="slave">Slave
</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>
<div class="form-group">
<label>SOA-EDIT-API</label>
<div class="radio">
<label>
<input type="radio" name="radio_type_soa_edit_api" id="radio_default" value="DEFAULT" checked> DEFAULT
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="radio_type_soa_edit_api" id="radio_increase" value="INCREASE"> INCREASE
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="radio_type_soa_edit_api" id="radio_epoch" value="EPOCH"> EPOCH
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="radio_type_soa_edit_api" id="radio_off" value="OFF"> OFF
</label>
</div>
</div>
</div>
<!-- /.box-body -->
<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('dashboard') }}'">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 domain</h3>
</div>
<div class="box-body">
<dl class="dl-horizontal">
<dt>Domain name</dt>
<dd>Enter your domain name in the format of name.tld (eg. powerdns-admin.com). You can also enter sub-domains to create a sub-root zone (eg. sub.powerdns-admin.com) in case you want to delegate sub-domain management to specific users.</dd>
<dt>Type</dt>
<dd>The type decides how the domain will be replicated across multiple DNS servers.
<ul>
<li>
Native - PowerDNS will not perform any replication. Use this if you only have one PowerDNS server or you handle replication via your backend (MySQL).
</li>
<li>
Master - This PowerDNS server will serve as the master and will send zone transfers (AXFRs) to other servers configured as slaves.
</li>
<li>
Slave - This PowerDNS server will serve as the slave and will request and receive zone transfers (AXFRs) from other servers configured as masters.
</li>
</ul>
</dd>
<dt>SOA-EDIT-API</dt>
<dd>The SOA-EDIT-API setting defines how the SOA serial number will be updated after a change is made to the domain.
<ul>
<li>
DEFAULT - Generate a soa serial of YYYYMMDD01. If the current serial is lower than the generated serial, use the generated serial. If the current serial is higher or equal to the generated serial, increase the current serial by 1.
</li>
<li>
INCREASE - Increase the current serial by 1.
</li>
<li>
EPOCH - Change the serial to the number of seconds since the EPOCH, aka unixtime.
</li>
<li>
OFF - Disable automatic updates of the SOA serial.
</li>
</ul>
</dd>
</dl>
<p>Find more details at <a href="https://docs.powerdns.com/md/">https://docs.powerdns.com/md/</a></p>
</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,269 @@
{% extends "base.html" %}
{% block title %}<title>Domain Management - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
{% if status %}
{% if status.get('status') == 'ok' %}
<div class="alert alert-success">
<strong>Success!</strong> {{ status.get('msg') }}
</div>
{% elif status.get('status') == 'error' %}
<div class="alert alert-danger">
{% if status.get('msg') != None %}
<strong>Error!</strong> {{ status.get('msg') }}
{% else %}
<strong>Error!</strong> An undefined error occurred.
{% endif %}
</div>
{% endif %}
{% endif %}
<section class="content-header">
<h1>
Manage domain <small>{{ domain.name }}</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i
class="fa fa-dashboard"></i> Home</a></li>
<li class="active">Domain Management</li>
</ol>
</section>
{% endblock %}
{% block content %}
<section class="content">
<div class="row">
<div class="col-xs-12">
<div class="box">
<form method="post" action="{{ url_for('domain_management', domain_name=domain.name) }}">
<div class="box-header">
<h3 class="box-title">Domain Access Control</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-xs-2">
<p>Users on the right have access to manage the records in
the {{ domain.name }} domain.</p>
<p>Click on users to move from between columns.</p>
<p>
Users in <font style="color: red;">red</font> are Administrators
and already have access to <b>ALL</b> domains.
</p>
</div>
<div class="form-group col-xs-2">
<select multiple="multiple" class="form-control" id="domain_multi_user" name="domain_multi_user[]">
{% for user in users %}
<option {% if user.id in
domain_user_ids %}selected{% endif %} value="{{ user.username }}"
{% if user.role.name== 'Administrator' %}style="color: red"{% endif %}>{{
user.username}}</option> {% endfor %}
</select>
</div>
</div>
<div class="box-body">
<div class="col-xs-offset-2">
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary"><i class="fa fa-check"></i> Save</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Account</h3>
</div>
<div class="box-body">
<div class="col-xs-12">
<div class="form-group">
<form method="post" action="{{ url_for('domain_change_account', domain_name=domain.name) }}">
<select name="accountid" class="form-control" style="width:15em;">
<option value="0">- No Account -</option>
{% for account in accounts %}
<option value="{{ account.id }}" {% if domain_account.id == account.id %}selected{% endif %}>{{ account.name }}</option>
{% endfor %}
</select><br/>
<button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api">
<i class="fa fa-check"></i>&nbsp;Change account for {{ domain.name }}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Auto PTR creation</h3>
</div>
<div class="box-body">
<p><input type="checkbox" id="{{ domain.name }}" class="auto_ptr_toggle"
{% for setting in domain.settings %}{% if setting.setting=='auto_ptr' and setting.value=='True' %}checked{% endif %}{% endfor %} {% if SETTING.get('auto_ptr') %}disabled="True"{% endif %}>
&nbsp;Allow automatic reverse pointer creation on record updates?{% if
SETTING.get('auto_ptr') %}</br><code>Auto-ptr is enabled globally on the PDA system!</code>{% endif %}</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">DynDNS 2 Settings</h3>
</div>
<div class="box-body">
<p><input type="checkbox" id="{{ domain.name }}" class="dyndns_on_demand_toggle"
{% for setting in domain.settings %}{% if setting.setting=='create_via_dyndns' and setting.value=='True' %}checked{% endif %}{% endfor %}>
&nbsp;Allow on-demand creation of records via DynDNS updates?</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Change SOA-EDIT-API</h3>
</div>
<div class="box-body">
<p>The SOA-EDIT-API setting defines how the SOA serial number will be updated after a change is made to the domain.</p>
<ul>
<li>
DEFAULT - Generate a soa serial of YYYYMMDD01. If the current serial is lower than the generated serial, use the generated serial. If the current serial is higher or equal to the generated serial, increase the current serial by 1.
</li>
<li>
INCREASE - Increase the current serial by 1.
</li>
<li>
EPOCH - Change the serial to the number of seconds since the EPOCH, aka unixtime.
</li>
<li>
OFF - Disable automatic updates of the SOA serial.
</li>
</ul>
<b>New SOA-EDIT-API Setting:</b>
<form method="post" action="{{ url_for('domain_change_soa_edit_api', domain_name=domain.name) }}">
<select name="soa_edit_api" class="form-control" style="width:15em;">
<option selected value="0">- Unchanged -</option>
<option>DEFAULT</option>
<option>INCREASE</option>
<option>EPOCH</option>
<option>OFF</option>
</select><br/>
<button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api">
<i class="fa fa-check"></i>&nbsp;Change SOA-EDIT-API setting for {{ domain.name }}
</button>
</form>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Domain Deletion</h3>
</div>
<div class="box-body">
<p>This function is used to remove a domain from PowerDNS-Admin <b>AND</b> PowerDNS. All records and user privileges associated with this domain will also be removed. This change cannot be reverted.</p>
<button type="button" class="btn btn-flat btn-danger pull-left delete_domain" id="{{ domain.name }}">
<i class="fa fa-trash"></i>&nbsp;DELETE DOMAIN {{ domain.name }}
</button>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extrascripts %}
<script>
//initialize pretty checkboxes
$('.dyndns_on_demand_toggle').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%' // optional
});
$('.auto_ptr_toggle').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%' // optional
});
$("#domain_multi_user").multiSelect();
//handle checkbox toggling
$('.dyndns_on_demand_toggle').on('ifToggled', function(event) {
var is_checked = $(this).prop('checked');
var domain = $(this).prop('id');
postdata = {
'action' : 'set_setting',
'data' : {
'setting' : 'create_via_dyndns',
'value' : is_checked
}
};
applyChanges(postdata, $SCRIPT_ROOT + '/domain/' + domain + '/managesetting', true);
});
$('.auto_ptr_toggle').on('ifToggled', function(event) {
var is_checked = $(this).prop('checked');
var domain = $(this).prop('id');
postdata = {
'action' : 'set_setting',
'data' : {
'setting' : 'auto_ptr',
'value' : is_checked
}
};
applyChanges(postdata, $SCRIPT_ROOT + '/domain/' + domain + '/managesetting', true);
});
// handle deletion of domain
$(document.body).on('click', '.delete_domain', function() {
var modal = $("#modal_delete_domain");
var domain = $(this).prop('id');
var info = "Are you sure you want to delete " + domain + "?";
modal.find('.modal-body p').text(info);
modal.find('#button_delete_confirm').click(function() {
$.get($SCRIPT_ROOT + '/admin/domain/' + domain + '/delete', function() {
window.location.href = '{{ url_for('dashboard') }}';
});
modal.modal('hide');
})
modal.modal('show');
});
</script>
{% endblock %}
{% block modals %}
<div class="modal fade modal-warning" id="modal_delete_domain">
<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>
{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}<title>PowerDNS-Admin - 400 Error</title>{% endblock %}
{% block title %}<title>DNS Control Panel - 400 Error</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
@ -9,7 +9,7 @@
<small>Error</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li>400</li>
</ol>
</section>
@ -25,12 +25,8 @@
<i class="fa fa-warning text-yellow"></i> Oops! Bad request
</h3>
<p>
{% if msg %}
{{ msg }}
{% else %}
The server refused to process your request and return a 400 error.
{% endif %}
<br/>You may <a href="{{ url_for('dashboard.dashboard') }}">return to the dashboard</a>.
The server refused to process your request and return a 400 error.
You may <a href="{{ url_for('dashboard') }}">return to the dashboard</a>.
</p>
</div>
<!-- /.error-content -->

View file

@ -1,16 +1,16 @@
{% extends "base.html" %}
{% block title %}<title>PowerDNS-Admin - 403 Error</title>{% endblock %}
{% block title %}<title>DNS Control Panel - 401 Error</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
403
401
<small>Error</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li>403</li>
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li>401</li>
</ol>
</section>
{% endblock %}
@ -19,14 +19,14 @@
<!-- Main content -->
<section class="content">
<div class="error-page">
<h2 class="headline text-yellow">403</h2>
<h2 class="headline text-yellow">401</h2>
<div class="error-content">
<h3>
<i class="fa fa-warning text-yellow"></i> Oops! Access denied
</h3>
<p>
You don't have permission to access this page
You may <a href="{{ url_for('dashboard.dashboard') }}">return to the dashboard</a>.
You may <a href="{{ url_for('dashboard') }}">return to the dashboard</a>.
</p>
</div>
<!-- /.error-content -->

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}<title>PowerDNS-Admin - 404 Error</title>{% endblock %}
{% block title %}<title>DNS Control Panel - 404 Error</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
@ -9,7 +9,7 @@
<small>Error</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li>404</li>
</ol>
</section>
@ -26,7 +26,7 @@
</h3>
<p>
The page you requested could not be found.
You may <a href="{{ url_for('dashboard.dashboard') }}">return to the dashboard</a>.
You may <a href="{{ url_for('dashboard') }}">return to the dashboard</a>.
</p>
</div>
<!-- /.error-content -->

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}<title>PowerDNS-Admin - 500 Error</title>{% endblock %}
{% block title %}<title>DNS Control Panel - 500 Error</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
@ -9,7 +9,7 @@
<small>Error</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li>500</li>
</ol>
</section>
@ -26,7 +26,7 @@
</h3>
<p>
Try again later.
You may <a href="{{ url_for('dashboard.dashboard') }}">return to the dashboard</a>.
You may <a href="{{ url_for('dashboard') }}">return to the dashboard</a>.
</p>
</div>
<!-- /.error-content -->

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}<title>PowerDNS-Admin - SAML Authentication Error</title>{% endblock %}
{% block title %}<title>DNS Control Panel - SAML Authentication Error</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
@ -9,7 +9,7 @@
<small>Error</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li>SAML</li>
</ol>
</section>
@ -34,7 +34,7 @@
{% endfor %}
</ul>
You may <a href="{{ url_for('dashboard.dashboard') }}">return to the dashboard</a>.
You may <a href="{{ url_for('dashboard') }}">return to the dashboard</a>.
</p>
</div>
<!-- /.error-content -->

134
app/templates/login.html Normal file
View file

@ -0,0 +1,134 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Log In - {{ SITE_NAME }}</title>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
{% assets "css_login" -%}
<link rel="stylesheet" href="{{ ASSET_URL }}">
{%- endassets %}
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body class="hold-transition login-page">
<div class="login-box">
<div class="login-logo">
<a href="{{ url_for('index') }}"><b>PowerDNS</b>-Admin</a>
</div>
<!-- /.login-logo -->
<div class="login-box-body">
{% if error %}
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert"
aria-hidden="true">&times;</button>
{{ error }}
</div>
{% endif %}
<form action="" method="post" data-toggle="validator">
<div class="form-group">
<input type="text" class="form-control" placeholder="Username" name="username" data-error="Please input your username" required {% if username %}value="{{ username }}"{% endif %}>
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<input type="password" class="form-control" placeholder="Password" name="password" data-error="Please input your password" required {% if password %}value="{{ password }}"{% endif %}>
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<input type="otptoken" class="form-control" placeholder="OTP Token" name="otptoken">
</div>
{% if SETTING.get('ldap_enabled') and SETTING.get('local_db_enabled') %}
<div class="form-group">
<select class="form-control" name="auth_method">
<option value="LOCAL">LOCAL Authentication</option>
{% if SETTING.get('login_ldap_first') %}
<option value="LDAP" selected="selected">LDAP Authentication</option>
{% else %}
<option value="LDAP">LDAP Authentication</option>
{% endif %}
</select>
</div>
{% elif SETTING.get('ldap_enabled') and not SETTING.get('local_db_enabled') %}
<div class="form-group">
<input type="hidden" name="auth_method" value="LDAP">
</div>
{% elif SETTING.get('local_db_enabled') and not SETTING.get('ldap_enabled') %}
<div class="form-group">
<input type="hidden" name="auth_method" value="LOCAL">
</div>
{% else %}
<div class="form-group">
<input type="hidden" name="auth_method" value="LOCAL">
</div>
{% endif %}
<div class="row">
<div class="col-xs-8">
<div class="checkbox icheck">
<label>
<input type="checkbox"> Remember Me
</label>
</div>
</div>
<!-- /.col -->
<div class="col-xs-4">
<button type="submit" class="btn btn-flat btn-primary btn-block">Sign In</button>
</div>
<!-- /.col -->
</div>
</form>
{% if SETTING.get('google_oauth_enabled') or SETTING.get('github_oauth_enabled') %}
<div class="social-auth-links text-center">
<p>- OR -</p>
{% if SETTING.get('github_oauth_enabled') %}
<a href="{{ url_for('github_login') }}" class="btn btn-block btn-social btn-github btn-flat"><i class="fa fa-github"></i> Sign in using
Github</a>
{% endif %}
{% if SETTING.get('google_oauth_enabled') %}
<a href="{{ url_for('google_login') }}" class="btn btn-block btn-social btn-google btn-flat"><i class="fa fa-google-plus"></i> Sign in using
Google</a>
{% endif %}
</div>
{% endif %}
{% if saml_enabled %}
<a href="{{ url_for('saml_login') }}">SAML login</a>
{% endif %}
{% if SETTING.get('signup_enabled') %}
<br>
<a href="{{ url_for('register') }}" class="text-center">Create an account </a>
{% endif %}
</div>
<!-- /.login-box-body -->
<div class="login-box-footer">
<center><p>Powered by <a href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</a></p></center>
</div>
</div>
<!-- /.login-box -->
{% assets "js_login" -%}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{%- endassets %}
{% assets "js_validation" -%}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{%- endassets %}
<script>
$(function () {
$('input').iCheck({
checkboxClass: 'icheckbox_square-blue',
radioClass: 'iradio_square-blue',
increaseArea: '20%' // optional
});
});
</script>
</body>
</html>

View file

@ -0,0 +1,18 @@
<!doctype html>
<title>Site Maintenance</title>
<style>
body { text-align: center; padding: 150px; }
h1 { font-size: 50px; }
body { font: 20px Helvetica, sans-serif; color: #333; }
article { display: block; text-align: left; width: 650px; margin: 0 auto; }
a { color: #dc8100; text-decoration: none; }
a:hover { color: #333; text-decoration: none; }
</style>
<article>
<h1>We&rsquo;ll be back soon!</h1>
<div>
<p>Sorry for the inconvenience but we&rsquo;re performing some maintenance at the moment. Please contact the System Administrator if you need more information</a>, otherwise we&rsquo;ll be back online shortly!</p>
<p>&mdash; Team</p>
</div>
</article>

View file

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Register - {{ SITE_NAME }}</title>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
{% assets "css_login" -%}
<link rel="stylesheet" href="{{ ASSET_URL }}">
{%- endassets %}
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body class="hold-transition register-page">
<div class="register-box">
<div class="register-logo">
<a href="{{ url_for('index') }}"><b>PowerDNS</b>-Admin</a>
</div>
<div class="register-box-body">
{% if error %}
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert"
aria-hidden="true">&times;</button>
{{ error }}
</div>
{% endif %}
<p class="login-box-msg">Enter your personal details below</p>
<form action="{{ url_for('login') }}" method="post" data-toggle="validator">
<div class="form-group has-feedback">
<input type="text" class="form-control" placeholder="First Name" name="firstname" data-error="Please input your first name" required>
<span class="glyphicon glyphicon-user form-control-feedback"></span>
<span class="help-block with-errors"></span>
</div>
<div class="form-group has-feedback">
<input type="text" class="form-control" placeholder="Last name" name="lastname" data-error="Please input your last name" required>
<span class="glyphicon glyphicon-user form-control-feedback"></span>
<span class="help-block with-errors"></span>
</div>
<div class="form-group has-feedback">
<input type="email" class="form-control" placeholder="Email" name="email" data-error="Please input your valid email address"
pattern="^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$" required>
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
<span class="help-block with-errors"></span>
</div>
<p class="login-box-msg">Enter your account details below</p>
<div class="form-group has-feedback">
<input type="text" class="form-control" placeholder="Username" name="username" data-error="Please input your username" required>
<span class="glyphicon glyphicon-user form-control-feedback"></span>
<span class="help-block with-errors"></span>
</div>
<div class="form-group has-feedback">
<input type="password" class="form-control" placeholder="Password" id="password" name="password" data-error="Please input your password" required>
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<input type="password" class="form-control" placeholder="Retype password" name="rpassword" data-match="#password" data-match-error="Password confirmation does not match" required>
<span class="glyphicon glyphicon-log-in form-control-feedback"></span>
<span class="help-block with-errors"></span>
</div>
<div class="row">
<div class="col-xs-4 pull-left">
<button type="button" class="btn btn-flat btn-block" id="button_back">Back</button>
</div>
<div class="col-xs-4 pull-right">
<button type="submit" class="btn btn-flat btn-primary btn-block">Register</button>
</div>
<!-- /.col -->
</div>
</form>
</div>
<!-- /.form-box -->
<div class="login-box-footer">
<center><p>Powered by <a href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</a></p></center>
</div>
</div>
<!-- /.login-box -->
{% assets "js_login" -%}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{%- endassets %}
{% assets "js_validation" -%}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{%- endassets %}
<script>
$(function () {
$('#button_back').click(function(){
window.location.href='{{ url_for('login') }}';
})
});
</script>
</body>
</html>

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

@ -0,0 +1,117 @@
{% extends "base.html" %}
{% set active_page = "admin_domain_template" %}
{% block title %}<title>Templates - {{ SITE_NAME }}</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="btn_edit">
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="btn_delete">
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

@ -3,41 +3,42 @@
{% block title %}<title>Create Template - {{ SITE_NAME }}</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('admin.templates') }}"><i class="fa fa-dashboard"></i> Templates</a></li>
<li class="active">Create</li>
</ol>
</section>
<!-- 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"]) %} {%
{% 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 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>
{% endif %} {% endwith %}
</div>
{% endif %} {% endwith %}
<div class="row">
<div class="col-md-4">
@ -47,22 +48,23 @@ if errors %}
</div>
<!-- /.box-header -->
<!-- form start -->
<form role="form" method="post" action="{{ url_for('admin.create_template') }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<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"
<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('admin.templates') }}'">Cancel</button>
onclick="window.location.href='{{ url_for('templates') }}'">Cancel</button>
</div>
</form>
</div>
@ -91,13 +93,13 @@ if errors %}
{% 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();
}
});
$("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

@ -8,7 +8,7 @@
Edit template <small>{{ template }}</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i
<li><a href="{{ url_for('dashboard') }}"><i
class="fa fa-dashboard"></i> Home</a></li>
<li>Templates</li>
<li class="active">{{ template }}</li>
@ -41,7 +41,6 @@
<th>Status</th>
<th>TTL</th>
<th>Data</th>
<th>Comment</th>
<th>Edit</th>
<th>Delete</th>
<th>ID</th>
@ -49,7 +48,7 @@
</thead>
<tbody>
{% for record in records %}
<tr class="odd row_record">
<tr class="odd row_record" id="{{ record.name }}">
<td>
{{ record.name }}
</td>
@ -62,19 +61,16 @@
<td>
{{ record.ttl }}
</td>
<td>
<td class="length-break">
{{ record.data }}
</td>
<td>
{{ record.comment }}
</td>
<td width="6%">
<button type="button" class="btn btn-flat btn-warning button_edit">
<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">
<button type="button" class="btn btn-flat btn-danger button_delete" id="{{ record.name }}">
Delete&nbsp;<i class="fa fa-trash"></i>
</button>
</td>
@ -96,7 +92,6 @@
<script>
// superglobals
window.records_allow_edit = {{ editable_records|tojson }};
window.ttl_options = {{ ttl_options|tojson }};
window.nEditing = null;
window.nNew = false;
@ -108,7 +103,7 @@
"ordering" : true,
"info" : true,
"autoWidth" : false,
{% if SETTING.get('default_record_table_size')|string in ['5','15','20'] %}
{% if SETTING.get('default_record_table_size') in ['5','15','20'] %}
"lengthMenu": [ [5, 15, 20, -1],
[5, 15, 20, "All"]],
{% else %}
@ -127,16 +122,16 @@
},
{
// hidden column so that we can add new records on top
// regardless of whatever sorting is done. See orderFixed
// regardless of whatever sorting is done
visible: false,
targets: [ 8 ]
targets: [ 7 ]
},
{
className: "length-break",
targets: [ 4, 5 ]
targets: [ 4 ]
}
],
"orderFixed": [[8, 'asc']]
"orderFixed": [[7, 'asc']]
});
// handle delete button
@ -191,13 +186,6 @@
// handle apply changes button
$(document.body).on("click",".button_apply_changes", function() {
if (nNew || nEditing) {
var modal = $("#modal_error");
modal.find('.modal-body p').text("Previous record not saved. Please save it before applying the changes.");
modal.modal('show');
return;
}
var modal = $("#modal_apply_changes");
var table = $("#tbl_records").DataTable();
var template = $(this).prop('id');
@ -207,7 +195,7 @@
// following unbind("click") is to avoid multiple times execution
modal.find('#button_apply_confirm').unbind("click").click(function() {
var data = getTableData(table);
applyChanges( {'_csrf_token': '{{ csrf_token() }}', 'records': data}, $SCRIPT_ROOT + '/admin/template/' + template + '/apply', true);
applyChanges(data, '/template/' + template + '/apply', true);
modal.modal('hide');
})
modal.modal('show');
@ -227,7 +215,7 @@
// add new row
var default_type = records_allow_edit[0]
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', default_type, 'Active', 3600, '', '', '', '', '0']);
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', default_type, 'Active', 3600, '', '', '', '0']);
editRow($("#tbl_records").DataTable(), nRow);
document.getElementById("edit-row-focus").focus();
nEditing = nRow;
@ -313,9 +301,6 @@
mx_server = modal.find('#mx_server').val();
mx_priority = modal.find('#mx_priority').val();
data = mx_priority + " " + mx_server;
if (data && !data.endsWith('.')) {
data = data + '.'
}
record_data.val(data);
modal.modal('hide');
})
@ -351,9 +336,6 @@
srv_port = modal.find('#srv_port').val();
srv_target = modal.find('#srv_target').val();
data = srv_priority + " " + srv_weight + " " + srv_port + " " + srv_target;
if (data && !data.endsWith('.')) {
data = data + '.'
}
record_data.val(data);
modal.modal('hide');
})
@ -372,7 +354,7 @@
<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=\"1209600\"> \
<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\"> \
";

View file

@ -1,17 +1,17 @@
{% extends "base.html" %}
{% block title %}<title>My Profile - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
Profile
<small>Edit my profile</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li class="active">My Profile</li>
</ol>
</section>
</ol>
</section>
{% endblock %}
{% block content %}
<section class="content">
@ -19,8 +19,7 @@
<div class="col-lg-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Edit my profile{% if session['authentication_type'] != 'LOCAL' %} [Disabled -
Authenticated externally]{% endif %}</h3>
<h3 class="box-title">Edit my profile{% if session['authentication_type'] != 'LOCAL' %} [Disabled - Authenticated externally]{% endif %}</h3>
</div>
<div class="box-body">
<!-- Custom Tabs -->
@ -28,6 +27,8 @@
<ul class="nav nav-tabs">
<li class="active"><a href="#tabs-personal" data-toggle="tab">Personal
Info</a></li>
<li><a href="#tabs-avatar" data-toggle="tab">Change
Avatar</a></li>
{% if session['authentication_type'] == 'LOCAL' %}
<li><a href="#tabs-password" data-toggle="tab">Change Password</a></li>
{% endif %}
@ -38,22 +39,50 @@
<div class="tab-content">
<div class="tab-pane active" id="tabs-personal">
<form role="form" method="post" action="{{ user_profile }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="firstname">First Name</label> <input type="text"
class="form-control" name="firstname" id="firstname"
placeholder="{{ current_user.firstname }}"
{% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
placeholder="{{ current_user.firstname }}" {% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
</div>
<div class="form-group">
<label for="lastname">Last Name</label> <input type="text" class="form-control"
name="lastname" id="lastname" placeholder="{{ current_user.lastname }}"
{% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
<label for="lastname">Last Name</label> <input type="text"
class="form-control" name="lastname" id="lastname"
placeholder="{{ current_user.lastname }}" {% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
</div>
<div class="form-group">
<label for="email">E-mail</label> <input type="email" class="form-control"
name="email" id="email" placeholder="{{ current_user.email }}"
{% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
<label for="email">E-mail</label> <input type="text"
class="form-control" name="email" id="email"
placeholder="{{ current_user.email }}" {% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
</div>{% if session['authentication_type'] == 'LOCAL' %}
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Submit</button>
</div>{% endif %}
</form>
</div>
<div class="tab-pane" id="tabs-avatar">
<form action="{{ user_profile }}" method="post"
enctype="multipart/form-data">
<div class="form-group">
<div class="form-group">
<div class="thumbnail" style="width: 200px; height: 210px;">
{% if current_user.avatar %} <img
src="{{ url_for('user_avatar', filename=current_user.avatar) }}"
alt="" / style="width: 200px; height: 200px;"> {%
else %} <img
src="{{ current_user.email|email_to_gravatar_url(size=200) }}"
alt="" /> {% endif %}
</div>{% if session['authentication_type'] == 'LOCAL' %}
<div>
<label for="file">Select image</label> <input type="file"
id="file" name="file">
</div>{% endif %}
</div>{% if session['authentication_type'] == 'LOCAL' %}
<div>
<span class="label label-danger">NOTE! </span> <span>&nbsp;Only
supports <strong>.PNG, .JPG, .JPEG</strong>. The best size
to use is <strong>200x200</strong>.
</span>
</div>{% endif %}
</div>{% if session['authentication_type'] == 'LOCAL' %}
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Submit</button>
@ -61,19 +90,18 @@
</form>
</div>
{% if session['authentication_type'] == 'LOCAL' %}
<div class="tab-pane" id="tabs-password">
<div class="tab-pane" id="tabs-password">
{% if not current_user.password %}
Your account password is managed via LDAP which isn't supported to change here.
{% else %}
<form action="{{ user_profile }}" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<label for="password">New Password</label> <input type="password"
class="form-control" name="password" id="newpassword" />
<label for="password">New Password</label> <input
type="password" class="form-control" name="password" id="newpassword"/>
</div>
<div class="form-group">
<label for="rpassword">Re-type New Password</label> <input type="password"
class="form-control" name="rpassword" id="rpassword" />
<label for="rpassword">Re-type New Password</label> <input
type="password" class="form-control" name="rpassword" id="rpassword"/>
</div>
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Change Password</button>
@ -85,34 +113,15 @@
<!-- {% if session['authentication_type'] in ['LOCAL', 'LDAP'] %} -->
<div class="tab-pane" id="tabs-authentication">
<form action="{{ user_profile }}" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
<input type="checkbox" id="otp_toggle" class="otp_toggle"
{% if current_user.otp_secret %}checked{% endif %}>
<input type="checkbox" id="otp_toggle" class="otp_toggle" {% if current_user.otp_secret %}checked{% endif %}>
<label for="otp_toggle">Enable Two Factor Authentication</label>
{% if current_user.otp_secret %}
<div id="token_information">
<p><img id="qrcode" src="{{ url_for('user.qrcode') }}"></p>
<div style="position: relative; left: 15px">
Your secret key is: <br />
<form>
<input type=text id="otp_secret" value={{current_user.otp_secret}} readonly>
<button type=button style="position:relative; right:28px" onclick="copy_otp_secret_to_clipboard()"> <i class="fa fa-clipboard"></i> </button>
<br /><font color="red" id="copy_tooltip" style="visibility:collapse">Copied.</font>
</form>
</div>
You can use Google Authenticator (<a target="_blank"
href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Android</a>
- <a target="_blank"
href="https://apps.apple.com/us/app/google-authenticator/id388497605">iOS</a>)
or FreeOTP (<a target="_blank"
href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp&hl=en">Android</a>
- <a target="_blank"
href="https://itunes.apple.com/en/app/freeotp-authenticator/id872559395?mt=8">iOS</a>)
on your smartphone to scan the QR code.
<br />
<font color="red"><strong><i>Make sure only you can see this QR Code and secret key and
nobody can capture them.</i></strong></font>
<p><img id="qrcode" src="{{ url_for('qrcode') }}"></p>
Please start FreeOTP (<a target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp&hl=en">Android</a> - <a target="_blank" href="https://itunes.apple.com/en/app/freeotp-authenticator/id872559395?mt=8">iOS</a>) on your smartphone and scan the above QR Code with it.
<br/>
<font color="red"><strong><i>Make sure only you can see this QR Code and nobodoy can capture it.</i></strong></font>
</div>
{% endif %}
</div>
@ -128,35 +137,35 @@
</section>
{% endblock %}
{% block extrascripts %}
<!-- TODO: add password and password confirmation comparison check -->
<!-- TODO: add password and password confirmation comparisson check -->
<script>
$(function () {
$(function() {
$('#tabs').tabs({
// add url anchor tags
activate: function (event, ui) {
activate: function(event, ui) {
window.location.hash = ui.newPanel.attr('id');
}
});
// re-set active tab (ui)
var activeTabIdx = $('#tabs').tabs('option', 'active');
$('#tabs li:eq(' + activeTabIdx + ')').tab('show')
var activeTabIdx = $('#tabs').tabs('option','active');
$('#tabs li:eq('+activeTabIdx+')').tab('show')
});
// initialize pretty checkboxes
$('.otp_toggle').iCheck({
checkboxClass: 'icheckbox_square-blue',
increaseArea: '20%'
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
});
// handle checkbox toggling
$('.otp_toggle').on('ifToggled', function (event) {
$('.otp_toggle').on('ifToggled', function(event) {
var enable_otp = $(this).prop('checked');
var postdata = {
'action': 'enable_otp',
'data': {
'enable_otp': enable_otp
},
'_csrf_token': '{{ csrf_token() }}'
'action' : 'enable_otp',
'data' : {
'enable_otp' : enable_otp
}
};
applyChanges(postdata, $SCRIPT_ROOT + '/user/profile', false, true);
});

1662
app/views.py Normal file

File diff suppressed because it is too large Load diff

105
config_template.py Normal file
View file

@ -0,0 +1,105 @@
import os
basedir = os.path.abspath(os.path.dirname(__file__))
# BASIC APP CONFIG
WTF_CSRF_ENABLED = True
SECRET_KEY = 'We are the world'
BIND_ADDRESS = '127.0.0.1'
PORT = 9191
# TIMEOUT - for large zones
TIMEOUT = 10
# LOG CONFIG
# - For docker, LOG_FILE=''
LOG_LEVEL = 'DEBUG'
LOG_FILE = 'logfile.log'
# UPLOAD DIRECTORY
UPLOAD_DIR = os.path.join(basedir, 'upload')
# DATABASE CONFIG
SQLA_DB_USER = 'pda'
SQLA_DB_PASSWORD = 'changeme'
SQLA_DB_HOST = '127.0.0.1'
SQLA_DB_NAME = 'pda'
SQLALCHEMY_TRACK_MODIFICATIONS = True
# DATBASE - MySQL
SQLALCHEMY_DATABASE_URI = 'mysql://'+SQLA_DB_USER+':'+SQLA_DB_PASSWORD+'@'+SQLA_DB_HOST+'/'+SQLA_DB_NAME
# DATABSE - SQLite
# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
# SAML Authnetication
SAML_ENABLED = False
SAML_DEBUG = True
SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml')
##Example for ADFS Metadata-URL
SAML_METADATA_URL = 'https://<hostname>/FederationMetadata/2007-06/FederationMetadata.xml'
#Cache Lifetime in Seconds
SAML_METADATA_CACHE_LIFETIME = 1
# SAML SSO binding format to use
## Default: library default (urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect)
#SAML_IDP_SSO_BINDING = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
## EntityID of the IdP to use. Only needed if more than one IdP is
## in the SAML_METADATA_URL
### Default: First (only) IdP in the SAML_METADATA_URL
### Example: https://idp.example.edu/idp
#SAML_IDP_ENTITY_ID = 'https://idp.example.edu/idp'
## NameID format to request
### Default: The SAML NameID Format in the metadata if present,
### otherwise urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
### Example: urn:oid:0.9.2342.19200300.100.1.1
#SAML_NAMEID_FORMAT = 'urn:oid:0.9.2342.19200300.100.1.1'
## Attribute to use for Email address
### Default: email
### Example: urn:oid:0.9.2342.19200300.100.1.3
#SAML_ATTRIBUTE_EMAIL = 'urn:oid:0.9.2342.19200300.100.1.3'
## Attribute to use for Given name
### Default: givenname
### Example: urn:oid:2.5.4.42
#SAML_ATTRIBUTE_GIVENNAME = 'urn:oid:2.5.4.42'
## Attribute to use for Surname
### Default: surname
### Example: urn:oid:2.5.4.4
#SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4'
## Attribute to use for username
### Default: Use NameID instead
### Example: urn:oid:0.9.2342.19200300.100.1.1
#SAML_ATTRIBUTE_USERNAME = 'urn:oid:0.9.2342.19200300.100.1.1'
## Attribute to get admin status from
### Default: Don't control admin with SAML attribute
### Example: https://example.edu/pdns-admin
### If set, look for the value 'true' to set a user as an administrator
### If not included in assertion, or set to something other than 'true',
### the user is set as a non-administrator user.
#SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin'
## Attribute to get account names from
### Default: Don't control accounts with SAML attribute
### If set, the user will be added and removed from accounts to match
### what's in the login assertion. Accounts that don't exist will
### be created and the user added to them.
SAML_ATTRIBUTE_ACCOUNT = 'https://example.edu/pdns-account'
SAML_SP_ENTITY_ID = 'http://<SAML SP Entity ID>'
SAML_SP_CONTACT_NAME = '<contact name>'
SAML_SP_CONTACT_MAIL = '<contact mail>'
#Cofigures if SAML tokens should be encrypted.
#If enabled a new app certificate will be generated on restart
SAML_SIGN_REQUEST = False
#Use SAML standard logout mechanism retreived from idp metadata
#If configured false don't care about SAML session on logout.
#Logout from PowerDNS-Admin only and keep SAML session authenticated.
SAML_LOGOUT = False
#Configure to redirect to a different url then PowerDNS-Admin login after SAML logout
#for example redirect to google.com after successful saml logout
#SAML_LOGOUT_URL = 'https://google.com'

View file

@ -1,165 +1,98 @@
import os
#import urllib.parse
basedir = os.path.abspath(os.path.dirname(__file__))
### BASIC APP CONFIG
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
BIND_ADDRESS = '0.0.0.0'
PORT = 9191
OFFLINE_MODE = False
# BASIC APP CONFIG
WTF_CSRF_ENABLED = True
SECRET_KEY = 'changeme'
LOG_LEVEL = 'DEBUG'
LOG_FILE = 'logs/log.txt'
### DATABASE CONFIG
SQLA_DB_USER = 'pda'
SQLA_DB_PASSWORD = 'changeme'
SQLA_DB_HOST = '127.0.0.1'
SQLA_DB_NAME = 'pda'
# TIMEOUT - for large zones
TIMEOUT = 10
# UPLOAD DIR
UPLOAD_DIR = os.path.join(basedir, 'upload')
# DATABASE CONFIG FOR MYSQL
DB_HOST = os.environ.get('PDA_DB_HOST')
DB_NAME = os.environ.get('PDA_DB_NAME')
DB_USER = os.environ.get('PDA_DB_USER')
DB_PASSWORD = os.environ.get('PDA_DB_PASSWORD')
#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
### DATABASE - MySQL
#SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format(
# urllib.parse.quote_plus(SQLA_DB_USER),
# urllib.parse.quote_plus(SQLA_DB_PASSWORD),
# SQLA_DB_HOST,
# SQLA_DB_NAME
#)
### DATABASE - SQLite
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
### SMTP config
# MAIL_SERVER = 'localhost'
# MAIL_PORT = 25
# MAIL_DEBUG = False
# MAIL_USE_TLS = False
# MAIL_USE_SSL = False
# MAIL_USERNAME = None
# MAIL_PASSWORD = None
# MAIL_DEFAULT_SENDER = ('PowerDNS-Admin', 'noreply@domain.ltd')
# SAML Authnetication
SAML_ENABLED = False
# SAML_DEBUG = True
# SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml')
# ##Example for ADFS Metadata-URL
# SAML_METADATA_URL = 'https://<hostname>/FederationMetadata/2007-06/FederationMetadata.xml'
# #Cache Lifetime in Seconds
# SAML_METADATA_CACHE_LIFETIME = 1
SAML_DEBUG = True
SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml')
##Example for ADFS Metadata-URL
SAML_METADATA_URL = 'https://<hostname>/FederationMetadata/2007-06/FederationMetadata.xml'
#Cache Lifetime in Seconds
SAML_METADATA_CACHE_LIFETIME = 1
# # SAML SSO binding format to use
# ## Default: library default (urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect)
# #SAML_IDP_SSO_BINDING = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
# SAML SSO binding format to use
## Default: library default (urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect)
#SAML_IDP_SSO_BINDING = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
# ## EntityID of the IdP to use. Only needed if more than one IdP is
# ## in the SAML_METADATA_URL
# ### Default: First (only) IdP in the SAML_METADATA_URL
# ### Example: https://idp.example.edu/idp
# #SAML_IDP_ENTITY_ID = 'https://idp.example.edu/idp'
# ## NameID format to request
# ### Default: The SAML NameID Format in the metadata if present,
# ### otherwise urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
# ### Example: urn:oid:0.9.2342.19200300.100.1.1
# #SAML_NAMEID_FORMAT = 'urn:oid:0.9.2342.19200300.100.1.1'
## EntityID of the IdP to use. Only needed if more than one IdP is
## in the SAML_METADATA_URL
### Default: First (only) IdP in the SAML_METADATA_URL
### Example: https://idp.example.edu/idp
#SAML_IDP_ENTITY_ID = 'https://idp.example.edu/idp'
## NameID format to request
### Default: The SAML NameID Format in the metadata if present,
### otherwise urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
### Example: urn:oid:0.9.2342.19200300.100.1.1
#SAML_NAMEID_FORMAT = 'urn:oid:0.9.2342.19200300.100.1.1'
# Following parameter defines RequestedAttributes section in SAML metadata
# since certain iDPs require explicit attribute request. If not provided section
# will not be available in metadata.
#
# Possible attributes:
# name (mandatory), nameFormat, isRequired, friendlyName
#
# NOTE: This parameter requires to be entered in valid JSON format as displayed below
# and multiple attributes can given
#
# Following example:
#
# SAML_SP_REQUESTED_ATTRIBUTES = '[ \
# {"name": "urn:oid:0.9.2342.19200300.100.1.3", "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "isRequired": true, "friendlyName": "email"}, \
# {"name": "mail", "isRequired": false, "friendlyName": "test-field"} \
# ]'
#
# produces following metadata section:
# <md:AttributeConsumingService index="1">
# <md:RequestedAttribute Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="email" isRequired="true"/>
# <md:RequestedAttribute Name="mail" FriendlyName="test-field"/>
# </md:AttributeConsumingService>
## Attribute to use for Email address
### Default: email
### Example: urn:oid:0.9.2342.19200300.100.1.3
#SAML_ATTRIBUTE_EMAIL = 'urn:oid:0.9.2342.19200300.100.1.3'
## Attribute to use for Given name
### Default: givenname
### Example: urn:oid:2.5.4.42
#SAML_ATTRIBUTE_GIVENNAME = 'urn:oid:2.5.4.42'
# ## Attribute to use for Email address
# ### Default: email
# ### Example: urn:oid:0.9.2342.19200300.100.1.3
# #SAML_ATTRIBUTE_EMAIL = 'urn:oid:0.9.2342.19200300.100.1.3'
## Attribute to use for Surname
### Default: surname
### Example: urn:oid:2.5.4.4
#SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4'
# ## Attribute to use for Given name
# ### Default: givenname
# ### Example: urn:oid:2.5.4.42
# #SAML_ATTRIBUTE_GIVENNAME = 'urn:oid:2.5.4.42'
## Attribute to use for username
### Default: Use NameID instead
### Example: urn:oid:0.9.2342.19200300.100.1.1
#SAML_ATTRIBUTE_USERNAME = 'urn:oid:0.9.2342.19200300.100.1.1'
# ## Attribute to use for Surname
# ### Default: surname
# ### Example: urn:oid:2.5.4.4
# #SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4'
## Attribute to get admin status from
### Default: Don't control admin with SAML attribute
### Example: https://example.edu/pdns-admin
### If set, look for the value 'true' to set a user as an administrator
### If not included in assertion, or set to something other than 'true',
### the user is set as a non-administrator user.
#SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin'
# ## Attribute to use for username
# ### Default: Use NameID instead
# ### Example: urn:oid:0.9.2342.19200300.100.1.1
# #SAML_ATTRIBUTE_USERNAME = 'urn:oid:0.9.2342.19200300.100.1.1'
## Attribute to get account names from
### Default: Don't control accounts with SAML attribute
### If set, the user will be added and removed from accounts to match
### what's in the login assertion. Accounts that don't exist will
### be created and the user added to them.
SAML_ATTRIBUTE_ACCOUNT = 'https://example.edu/pdns-account'
# ## Attribute to get admin status from
# ### Default: Don't control admin with SAML attribute
# ### Example: https://example.edu/pdns-admin
# ### If set, look for the value 'true' to set a user as an administrator
# ### If not included in assertion, or set to something other than 'true',
# ### the user is set as a non-administrator user.
# #SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin'
# ## Attribute to get account names from
# ### Default: Don't control accounts with SAML attribute
# ### If set, the user will be added and removed from accounts to match
# ### what's in the login assertion. Accounts that don't exist will
# ### be created and the user added to them.
# SAML_ATTRIBUTE_ACCOUNT = 'https://example.edu/pdns-account'
# SAML_SP_ENTITY_ID = 'http://<SAML SP Entity ID>'
# SAML_SP_CONTACT_NAME = '<contact name>'
# SAML_SP_CONTACT_MAIL = '<contact mail>'
# Configures the path to certificate file and it's respective private key file
# This pair is used for signing metadata, encrypting tokens and all other signing/encryption
# tasks during communication between iDP and SP
# NOTE: if this two parameters aren't explicitly provided, self-signed certificate-key pair
# will be generated in "PowerDNS-Admin" root directory
# ###########################################################################################
# CAUTION: For production use, usage of self-signed certificates it's highly discouraged.
# Use certificates from trusted CA instead
# ###########################################################################################
# SAML_CERT_FILE = '/etc/pki/powerdns-admin/cert.crt'
# SAML_CERT_KEY = '/etc/pki/powerdns-admin/key.pem'
# Configures if SAML tokens should be encrypted.
# SAML_SIGN_REQUEST = False
# #Use SAML standard logout mechanism retreived from idp metadata
# #If configured false don't care about SAML session on logout.
# #Logout from PowerDNS-Admin only and keep SAML session authenticated.
# SAML_LOGOUT = False
# #Configure to redirect to a different url then PowerDNS-Admin login after SAML logout
# #for example redirect to google.com after successful saml logout
# #SAML_LOGOUT_URL = 'https://google.com'
# #SAML_ASSERTION_ENCRYPTED = True
# Remote authentication settings
# Whether to enable remote user authentication or not
# Defaults to False
# REMOTE_USER_ENABLED=True
# If set, users will be redirected to this location on logout
# Ignore or set to None to avoid redirecting altogether
# Warning: if REMOTE_USER environment variable is still set after logging out and not cleared by
# some external module, not defining a custom logout URL might trigger a loop
# that will just log the user back in right after logging out
# REMOTE_USER_LOGOUT_URL=https://my.sso.com/cas/logout
# An optional list of remote authentication tied cookies to be removed upon logout
# REMOTE_USER_COOKIES=['MOD_AUTH_CAS', 'MOD_AUTH_CAS_S']
SAML_SP_ENTITY_ID = 'http://<SAML SP Entity ID>'
SAML_SP_CONTACT_NAME = '<contact name>'
SAML_SP_CONTACT_MAIL = '<contact mail>'
#Cofigures if SAML tokens should be encrypted.
#If enabled a new app certificate will be generated on restart
SAML_SIGN_REQUEST = False
#Use SAML standard logout mechanism retreived from idp metadata
#If configured false don't care about SAML session on logout.
#Logout from PowerDNS-Admin only and keep SAML session authenticated.
SAML_LOGOUT = False
#Configure to redirect to a different url then PowerDNS-Admin login after SAML logout
#for example redirect to google.com after successful saml logout
#SAML_LOGOUT_URL = 'https://google.com'

View file

@ -1,115 +0,0 @@
# Defaults for Docker image
BIND_ADDRESS = '0.0.0.0'
PORT = 80
SQLALCHEMY_DATABASE_URI = 'sqlite:////data/powerdns-admin.db'
legal_envvars = (
'SECRET_KEY',
'OIDC_OAUTH_API_URL',
'OIDC_OAUTH_TOKEN_URL',
'OIDC_OAUTH_AUTHORIZE_URL',
'BIND_ADDRESS',
'PORT',
'LOG_LEVEL',
'SALT',
'SQLALCHEMY_TRACK_MODIFICATIONS',
'SQLALCHEMY_DATABASE_URI',
'MAIL_SERVER',
'MAIL_PORT',
'MAIL_DEBUG',
'MAIL_USE_TLS',
'MAIL_USE_SSL',
'MAIL_USERNAME',
'MAIL_PASSWORD',
'MAIL_DEFAULT_SENDER',
'SAML_ENABLED',
'SAML_DEBUG',
'SAML_PATH',
'SAML_METADATA_URL',
'SAML_METADATA_CACHE_LIFETIME',
'SAML_IDP_SSO_BINDING',
'SAML_IDP_ENTITY_ID',
'SAML_NAMEID_FORMAT',
'SAML_ATTRIBUTE_EMAIL',
'SAML_ATTRIBUTE_GIVENNAME',
'SAML_ATTRIBUTE_SURNAME',
'SAML_ATTRIBUTE_NAME',
'SAML_ATTRIBUTE_USERNAME',
'SAML_ATTRIBUTE_ADMIN',
'SAML_ATTRIBUTE_GROUP',
'SAML_GROUP_ADMIN_NAME',
'SAML_GROUP_TO_ACCOUNT_MAPPING',
'SAML_ATTRIBUTE_ACCOUNT',
'SAML_SP_ENTITY_ID',
'SAML_SP_CONTACT_NAME',
'SAML_SP_CONTACT_MAIL',
'SAML_SIGN_REQUEST',
'SAML_WANT_MESSAGE_SIGNED',
'SAML_LOGOUT',
'SAML_LOGOUT_URL',
'SAML_ASSERTION_ENCRYPTED',
'OFFLINE_MODE',
'REMOTE_USER_LOGOUT_URL',
'REMOTE_USER_COOKIES',
'SIGNUP_ENABLED',
'LOCAL_DB_ENABLED',
'LDAP_ENABLED',
'SAML_CERT',
'SAML_KEY',
'FILESYSTEM_SESSIONS_ENABLED'
)
legal_envvars_int = ('PORT', 'MAIL_PORT', 'SAML_METADATA_CACHE_LIFETIME')
legal_envvars_bool = (
'SQLALCHEMY_TRACK_MODIFICATIONS',
'HSTS_ENABLED',
'MAIL_DEBUG',
'MAIL_USE_TLS',
'MAIL_USE_SSL',
'SAML_ENABLED',
'SAML_DEBUG',
'SAML_SIGN_REQUEST',
'SAML_WANT_MESSAGE_SIGNED',
'SAML_LOGOUT',
'SAML_ASSERTION_ENCRYPTED',
'OFFLINE_MODE',
'REMOTE_USER_ENABLED',
'SIGNUP_ENABLED',
'LOCAL_DB_ENABLED',
'LDAP_ENABLED',
'FILESYSTEM_SESSIONS_ENABLED'
)
# import everything from environment variables
import os
import sys
def str2bool(v):
return v.lower() in ("true", "yes", "1")
for v in legal_envvars:
ret = None
# _FILE suffix will allow to read value from file, usefull for Docker's
# secrets feature
if v + '_FILE' in os.environ:
if v in os.environ:
raise AttributeError(
"Both {} and {} are set but are exclusive.".format(
v, v + '_FILE'))
with open(os.environ[v + '_FILE']) as f:
ret = f.read()
f.close()
elif v in os.environ:
ret = os.environ[v]
if ret is not None:
if v in legal_envvars_bool:
ret = str2bool(ret)
if v in legal_envvars_int:
ret = int(ret)
sys.modules[__name__].__dict__[v] = ret

View file

@ -1,25 +0,0 @@
import os
basedir = os.path.abspath(os.path.dirname(__file__))
### BASIC APP CONFIG
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
BIND_ADDRESS = '0.0.0.0'
PORT = 9191
HSTS_ENABLED = False
### DATABASE - SQLite
TEST_DB_LOCATION = '/tmp/testing.sqlite'
SQLALCHEMY_DATABASE_URI = 'sqlite:///{0}'.format(TEST_DB_LOCATION)
SQLALCHEMY_TRACK_MODIFICATIONS = False
# SAML Authnetication
SAML_ENABLED = False
# TEST SAMPLE DATA
TEST_USER = 'test'
TEST_USER_PASSWORD = 'test'
TEST_ADMIN_USER = 'admin'
TEST_ADMIN_PASSWORD = 'admin'
TEST_USER_APIKEY = 'wewdsfewrfsfsdf'
TEST_ADMIN_APIKEY = 'nghnbnhtghrtert'

View file

@ -1,34 +0,0 @@
version: "2.1"
services:
powerdns-admin:
build:
context: .
dockerfile: docker-test/Dockerfile
image: powerdns-admin-test
container_name: powerdns-admin-test
ports:
- "9191:80"
networks:
- default
env_file:
- ./docker-test/env
depends_on:
- pdns-server
pdns-server:
build:
context: .
dockerfile: docker-test/Dockerfile.pdns
image: pdns-server-test
ports:
- "5053:53"
- "5053:53/udp"
- "8081:8081"
networks:
- default
env_file:
- ./docker-test/env
networks:
default:

View file

@ -1,18 +1,113 @@
version: "3"
version: "2.1"
services:
app:
image: ngoduykhanh/powerdns-admin:latest
container_name: powerdns_admin
powerdns-admin:
build:
context: .
dockerfile: docker/PowerDNS-Admin/Dockerfile
args:
- ENVIRONMENT=${ENVIRONMENT}
image: powerdns-admin
container_name: powerdns-admin
mem_limit: 256M
memswap_limit: 256M
ports:
- "9191:80"
- "9191:9191"
volumes:
# Code
- .:/powerdns-admin/
- "./configs/${ENVIRONMENT}.py:/powerdns-admin/config.py"
# Assets dir volume
- powerdns-admin-assets:/powerdns-admin/app/static
- powerdns-admin-assets2:/powerdns-admin/node_modules
- powerdns-admin-assets3:/powerdns-admin/logs
- ./app/static/custom:/powerdns-admin/app/static/custom
logging:
driver: json-file
options:
max-size: 50m
networks:
- default
environment:
- SQLALCHEMY_DATABASE_URI=mysql://pda:changeme@host.docker.internal/pda
- GUNICORN_TIMEOUT=60
- GUNICORN_WORKERS=2
- GUNICORN_LOGLEVEL=DEBUG
- OFFLINE_MODE=False # True for offline, False for external resources
- ENVIRONMENT=${ENVIRONMENT}
- PDA_DB_HOST=${PDA_DB_HOST}
- PDA_DB_NAME=${PDA_DB_NAME}
- PDA_DB_USER=${PDA_DB_USER}
- PDA_DB_PASSWORD=${PDA_DB_PASSWORD}
- PDNS_HOST=${PDNS_HOST}
- PDNS_API_KEY=${PDNS_API_KEY}
- FLASK_APP=/powerdns-admin/app/__init__.py
depends_on:
powerdns-admin-mysql:
condition: service_healthy
powerdns-admin-mysql:
image: mysql/mysql-server:5.7
hostname: ${PDA_DB_HOST}
container_name: powerdns-admin-mysql
mem_limit: 256M
memswap_limit: 256M
expose:
- 3306
volumes:
- powerdns-admin-mysql-data:/var/lib/mysql
networks:
- default
environment:
- MYSQL_DATABASE=${PDA_DB_NAME}
- MYSQL_USER=${PDA_DB_USER}
- MYSQL_PASSWORD=${PDA_DB_PASSWORD}
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
timeout: 10s
retries: 5
pdns-server:
image: psitrax/powerdns
hostname: ${PDNS_HOST}
ports:
- "53:53"
- "53:53/udp"
networks:
- default
command: --api=yes --api-key=${PDNS_API_KEY} --webserver-address=0.0.0.0 --webserver-allow-from=0.0.0.0/0
environment:
- MYSQL_HOST=${PDNS_DB_HOST}
- MYSQL_USER=${PDNS_DB_USER}
- MYSQL_PASS=${PDNS_DB_PASSWORD}
- PDNS_API_KEY=${PDNS_API_KEY}
- PDNS_WEBSERVER_ALLOW_FROM=${PDNS_WEBSERVER_ALLOW_FROM}
depends_on:
pdns-mysql:
condition: service_healthy
pdns-mysql:
image: mysql/mysql-server:5.7
hostname: ${PDNS_DB_HOST}
container_name: ${PDNS_DB_HOST}
mem_limit: 256M
memswap_limit: 256M
expose:
- 3306
volumes:
- powerdns-mysql-data:/var/lib/mysql
networks:
- default
environment:
- MYSQL_DATABASE=${PDNS_DB_NAME}
- MYSQL_USER=${PDNS_DB_USER}
- MYSQL_PASSWORD=${PDNS_DB_PASSWORD}
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
timeout: 10s
retries: 5
networks:
default:
volumes:
powerdns-mysql-data:
powerdns-admin-mysql-data:
powerdns-admin-assets:
powerdns-admin-assets2:
powerdns-admin-assets3:

View file

@ -1,34 +0,0 @@
FROM debian:stretch-slim
LABEL maintainer="k@ndk.name"
ENV LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8
RUN apt-get update -y \
&& apt-get install -y --no-install-recommends apt-transport-https locales locales-all python3-pip python3-setuptools python3-dev curl libsasl2-dev libldap2-dev libssl-dev libxml2-dev libxslt1-dev libxmlsec1-dev libffi-dev build-essential libmariadb-dev-compat \
&& curl -sL https://deb.nodesource.com/setup_10.x | bash - \
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
&& apt-get update -y \
&& apt-get install -y nodejs yarn \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
# We copy just the requirements.txt first to leverage Docker cache
COPY ./requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip3 install --upgrade pip
RUN pip3 install -r requirements.txt
COPY . /app
COPY ./docker/entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
ENV FLASK_APP=powerdnsadmin/__init__.py
RUN yarn install --pure-lockfile --production \
&& yarn cache clean \
&& flask assets build
COPY ./docker-test/wait-for-pdns.sh /opt
RUN chmod u+x /opt/wait-for-pdns.sh
CMD ["/opt/wait-for-pdns.sh", "/usr/local/bin/pytest","--capture=no","-vv"]

View file

@ -1,13 +0,0 @@
FROM ubuntu:latest
RUN apt-get update && apt-get install -y pdns-backend-sqlite3 pdns-server sqlite3
COPY ./docker-test/pdns.sqlite.sql /data/pdns.sql
ADD ./docker-test/start.sh /data/
RUN rm -f /etc/powerdns/pdns.d/pdns.simplebind.conf
RUN rm -f /etc/powerdns/pdns.d/bind.conf
RUN chmod +x /data/start.sh && mkdir -p /var/empty/var/run
CMD /data/start.sh

View file

@ -1,5 +0,0 @@
PDNS_PROTO=http
PDNS_PORT=8081
PDNS_HOST=pdns-server
PDNS_API_KEY=changeme
PDNS_WEBSERVER_ALLOW_FROM=0.0.0.0/0

View file

@ -1,92 +0,0 @@
PRAGMA foreign_keys = 1;
CREATE TABLE domains (
id INTEGER PRIMARY KEY,
name VARCHAR(255) NOT NULL COLLATE NOCASE,
master VARCHAR(128) DEFAULT NULL,
last_check INTEGER DEFAULT NULL,
type VARCHAR(6) NOT NULL,
notified_serial INTEGER DEFAULT NULL,
account VARCHAR(40) DEFAULT NULL
);
CREATE UNIQUE INDEX name_index ON domains(name);
CREATE TABLE records (
id INTEGER PRIMARY KEY,
domain_id INTEGER DEFAULT NULL,
name VARCHAR(255) DEFAULT NULL,
type VARCHAR(10) DEFAULT NULL,
content VARCHAR(65535) DEFAULT NULL,
ttl INTEGER DEFAULT NULL,
prio INTEGER DEFAULT NULL,
change_date INTEGER DEFAULT NULL,
disabled BOOLEAN DEFAULT 0,
ordername VARCHAR(255),
auth BOOL DEFAULT 1,
FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX rec_name_index ON records(name);
CREATE INDEX nametype_index ON records(name,type);
CREATE INDEX domain_id ON records(domain_id);
CREATE INDEX orderindex ON records(ordername);
CREATE TABLE supermasters (
ip VARCHAR(64) NOT NULL,
nameserver VARCHAR(255) NOT NULL COLLATE NOCASE,
account VARCHAR(40) NOT NULL
);
CREATE UNIQUE INDEX ip_nameserver_pk ON supermasters(ip, nameserver);
CREATE TABLE comments (
id INTEGER PRIMARY KEY,
domain_id INTEGER NOT NULL,
name VARCHAR(255) NOT NULL,
type VARCHAR(10) NOT NULL,
modified_at INT NOT NULL,
account VARCHAR(40) DEFAULT NULL,
comment VARCHAR(65535) NOT NULL,
FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX comments_domain_id_index ON comments (domain_id);
CREATE INDEX comments_nametype_index ON comments (name, type);
CREATE INDEX comments_order_idx ON comments (domain_id, modified_at);
CREATE TABLE domainmetadata (
id INTEGER PRIMARY KEY,
domain_id INT NOT NULL,
kind VARCHAR(32) COLLATE NOCASE,
content TEXT,
FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX domainmetaidindex ON domainmetadata(domain_id);
CREATE TABLE cryptokeys (
id INTEGER PRIMARY KEY,
domain_id INT NOT NULL,
flags INT NOT NULL,
active BOOL,
content TEXT,
FOREIGN KEY(domain_id) REFERENCES domains(id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX domainidindex ON cryptokeys(domain_id);
CREATE TABLE tsigkeys (
id INTEGER PRIMARY KEY,
name VARCHAR(255) COLLATE NOCASE,
algorithm VARCHAR(50) COLLATE NOCASE,
secret VARCHAR(255)
);
CREATE UNIQUE INDEX namealgoindex ON tsigkeys(name, algorithm);

View file

@ -1,24 +0,0 @@
#!/usr/bin/env sh
if [ -z ${PDNS_API_KEY+x} ]; then
API_KEY=changeme
fi
if [ -z ${PDNS_PORT+x} ]; then
WEB_PORT=8081
fi
# Import schema structure
if [ -e "/data/pdns.sql" ]; then
rm /data/pdns.db
cat /data/pdns.sql | sqlite3 /data/pdns.db
rm /data/pdns.sql
echo "Imported schema structure"
fi
chown -R pdns:pdns /data/
/usr/sbin/pdns_server \
--launch=gsqlite3 --gsqlite3-database=/data/pdns.db \
--webserver=yes --webserver-address=0.0.0.0 --webserver-port=${PDNS_PORT} \
--api=yes --api-key=$PDNS_API_KEY --webserver-allow-from=${PDNS_WEBSERVER_ALLOW_FROM}

View file

@ -1,22 +0,0 @@
#!/bin/sh
set -e
CMD="$1"
shift
CMD_ARGS="$@"
LOOPS=10
until curl -H "X-API-Key: ${PDNS_API_KEY}" "${PDNS_PROTO}://${PDNS_HOST}:${PDNS_PORT}/api/v1/servers"; do
>&2 echo "PDNS is unavailable - sleeping"
sleep 1
if [ $LOOPS -eq 10 ]
then
break
fi
done
sleep 5
>&2 echo "PDNS is up - executing command"
exec $CMD $CMD_ARGS

View file

@ -1,95 +0,0 @@
FROM alpine:3.13 AS builder
LABEL maintainer="k@ndk.name"
ARG BUILD_DEPENDENCIES="build-base \
libffi-dev \
libxml2-dev \
mariadb-connector-c-dev \
openldap-dev \
python3-dev \
xmlsec-dev \
yarn \
cargo"
ENV LC_ALL=en_US.UTF-8 \
LANG=en_US.UTF-8 \
LANGUAGE=en_US.UTF-8 \
FLASK_APP=/build/powerdnsadmin/__init__.py
# Get dependencies
# py3-pip should not belong to BUILD_DEPENDENCIES. Otherwise, when we remove
# them with "apk del" at the end of build stage, the python requests module
# will be removed as well - (Tested with alpine:3.12 and python 3.8.5).
RUN apk add --no-cache ${BUILD_DEPENDENCIES} && \
apk add --no-cache py3-pip
WORKDIR /build
# We copy just the requirements.txt first to leverage Docker cache
COPY ./requirements.txt /build/requirements.txt
# Get application dependencies
RUN pip install --upgrade pip && \
pip install -r requirements.txt
# Add sources
COPY . /build
# Prepare assets
RUN yarn install --pure-lockfile --production && \
yarn cache clean && \
sed -i -r -e "s|'cssmin',\s?'cssrewrite'|'cssmin'|g" /build/powerdnsadmin/assets.py && \
flask assets build
RUN mv /build/powerdnsadmin/static /tmp/static && \
mkdir /build/powerdnsadmin/static && \
cp -r /tmp/static/generated /build/powerdnsadmin/static && \
cp -r /tmp/static/assets /build/powerdnsadmin/static && \
cp -r /tmp/static/img /build/powerdnsadmin/static && \
find /tmp/static/node_modules -name 'fonts' -exec cp -r {} /build/powerdnsadmin/static \; && \
find /tmp/static/node_modules/icheck/skins/square -name '*.png' -exec cp {} /build/powerdnsadmin/static/generated \;
RUN { \
echo "from flask_assets import Environment"; \
echo "assets = Environment()"; \
echo "assets.register('js_login', 'generated/login.js')"; \
echo "assets.register('js_validation', 'generated/validation.js')"; \
echo "assets.register('css_login', 'generated/login.css')"; \
echo "assets.register('js_main', 'generated/main.js')"; \
echo "assets.register('css_main', 'generated/main.css')"; \
} > /build/powerdnsadmin/assets.py
# Move application
RUN mkdir -p /app && \
cp -r /build/migrations/ /build/powerdnsadmin/ /build/run.py /app && \
mkdir -p /app/configs && \
cp -r /build/configs/docker_config.py /app/configs
# Build image
FROM alpine:3.13
ENV FLASK_APP=/app/powerdnsadmin/__init__.py \
USER=pda
RUN apk add --no-cache mariadb-connector-c postgresql-client py3-gunicorn py3-psycopg2 xmlsec tzdata libcap && \
addgroup -S ${USER} && \
adduser -S -D -G ${USER} ${USER} && \
mkdir /data && \
chown ${USER}:${USER} /data && \
setcap cap_net_bind_service=+ep $(readlink -f /usr/bin/python3) && \
apk del libcap
COPY --from=builder /usr/bin/flask /usr/bin/
COPY --from=builder /usr/lib/python3.8/site-packages /usr/lib/python3.8/site-packages/
COPY --from=builder --chown=root:${USER} /app /app/
COPY ./docker/entrypoint.sh /usr/bin/
WORKDIR /app
RUN chown ${USER}:${USER} ./configs /app && \
cat ./powerdnsadmin/default_config.py ./configs/docker_config.py > ./powerdnsadmin/docker_config.py
EXPOSE 80/tcp
USER ${USER}
HEALTHCHECK CMD ["wget","--output-document=-","--quiet","--tries=1","http://127.0.0.1/"]
ENTRYPOINT ["entrypoint.sh"]
CMD ["gunicorn","powerdnsadmin:create_app()"]

View file

@ -0,0 +1,45 @@
FROM ubuntu:16.04
MAINTAINER Khanh Ngo "k@ndk.name"
ARG ENVIRONMENT=development
ENV ENVIRONMENT=${ENVIRONMENT}
WORKDIR /powerdns-admin
RUN apt-get update -y
RUN apt-get install -y apt-transport-https
RUN apt-get install -y locales locales-all
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US.UTF-8
RUN apt-get install -y python3-pip python3-dev supervisor curl mysql-client
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
# Install yarn
RUN apt-get update -y
RUN apt-get install -y yarn
# Install Netcat for DB healthcheck
RUN apt-get install -y netcat
# lib for building mysql db driver
RUN apt-get install -y libmysqlclient-dev
# lib for buiding ldap and ssl-based application
RUN apt-get install -y libsasl2-dev libldap2-dev libssl-dev
# lib for building python3-saml
RUN apt-get install -y libxml2-dev libxslt1-dev libxmlsec1-dev libffi-dev pkg-config
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
COPY ./docker/PowerDNS-Admin/entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View file

@ -0,0 +1,64 @@
#!/bin/bash
set -o nounset
set -o errexit
set -o pipefail
# == Vars
#
DB_MIGRATION_DIR='/powerdns-admin/migrations'
# Wait for us to be able to connect to MySQL before proceeding
echo "===> Waiting for $PDA_DB_HOST MySQL service"
until nc -zv \
$PDA_DB_HOST \
3306;
do
echo "MySQL ($PDA_DB_HOST) is unavailable - sleeping"
sleep 1
done
echo "===> DB management"
# Go in Workdir
cd /powerdns-admin
if [ ! -d "${DB_MIGRATION_DIR}" ]; then
echo "---> Running DB Init"
flask db init --directory ${DB_MIGRATION_DIR}
flask db migrate -m "Init DB" --directory ${DB_MIGRATION_DIR}
flask db upgrade --directory ${DB_MIGRATION_DIR}
./init_data.py
else
echo "---> Running DB Migration"
set +e
flask db migrate -m "Upgrade BD Schema" --directory ${DB_MIGRATION_DIR}
flask db upgrade --directory ${DB_MIGRATION_DIR}
set -e
fi
echo "===> Update PDNS API connection info"
# initial setting if not available in the DB
mysql -h${PDA_DB_HOST} -u${PDA_DB_USER} -p${PDA_DB_PASSWORD} ${PDA_DB_NAME} -e "INSERT INTO setting (name, value) SELECT * FROM (SELECT 'pdns_api_url', 'http://${PDNS_HOST}:8081') AS tmp WHERE NOT EXISTS (SELECT name FROM setting WHERE name = 'pdns_api_url') LIMIT 1;"
mysql -h${PDA_DB_HOST} -u${PDA_DB_USER} -p${PDA_DB_PASSWORD} ${PDA_DB_NAME} -e "INSERT INTO setting (name, value) SELECT * FROM (SELECT 'pdns_api_key', '${PDNS_API_KEY}') AS tmp WHERE NOT EXISTS (SELECT name FROM setting WHERE name = 'pdns_api_key') LIMIT 1;"
# update pdns api setting if .env is changed.
mysql -h${PDA_DB_HOST} -u${PDA_DB_USER} -p${PDA_DB_PASSWORD} ${PDA_DB_NAME} -e "UPDATE setting SET value='http://${PDNS_HOST}:8081' WHERE name='pdns_api_url';"
mysql -h${PDA_DB_HOST} -u${PDA_DB_USER} -p${PDA_DB_PASSWORD} ${PDA_DB_NAME} -e "UPDATE setting SET value='${PDNS_API_KEY}' WHERE name='pdns_api_key';"
echo "===> Assets management"
echo "---> Running Yarn"
chown -R www-data:www-data /powerdns-admin/app/static
chown -R www-data:www-data /powerdns-admin/node_modules
su -s /bin/bash -c 'yarn install --pure-lockfile' www-data
echo "---> Running Flask assets"
chown -R www-data:www-data /powerdns-admin/logs
su -s /bin/bash -c 'flask assets build' www-data
echo "===> Start supervisor"
/usr/bin/supervisord -c /etc/supervisord.conf

View file

@ -1,17 +0,0 @@
#!/bin/sh
set -euo pipefail
cd /app
GUNICORN_TIMEOUT="${GUNICORN_TIMEOUT:-120}"
GUNICORN_WORKERS="${GUNICORN_WORKERS:-4}"
GUNICORN_LOGLEVEL="${GUNICORN_LOGLEVEL:-info}"
BIND_ADDRESS="${BIND_ADDRESS:-0.0.0.0:80}"
GUNICORN_ARGS="-t ${GUNICORN_TIMEOUT} --workers ${GUNICORN_WORKERS} --bind ${BIND_ADDRESS} --log-level ${GUNICORN_LOGLEVEL}"
if [ "$1" == gunicorn ]; then
/bin/sh -c "flask db upgrade"
exec "$@" $GUNICORN_ARGS
else
exec "$@"
fi

View file

@ -1,134 +0,0 @@
### API Usage
#### Getting started with docker
1. Run docker image docker-compose up, go to UI http://localhost:9191, at http://localhost:9191/swagger is swagger API specification
2. Click to register user, type e.g. user: admin and password: admin
3. Login to UI in settings enable allow domain creation for users, now you can create and manage domains with admin account and also ordinary users
4. Click on the API Keys menu then click on teh "Add Key" button to add a new Administrator Key
5. Keep the base64 encoded apikey somewhere safe as it won't be available in clear anymore
#### Accessing the API
The PDA API consists of two distinct parts:
- The /powerdnsadmin endpoints manages PDA content (accounts, users, apikeys) and also allow domain creation/deletion
- The /server endpoints are proxying queries to the backend PowerDNS instance's API. PDA acts as a proxy managing several API Keys and permissions to the PowerDNS content.
The requests to the API needs two headers:
- The classic 'Content-Type: application/json' is required to all POST and PUT requests, though it's harmless to use it on each call
- The authentication header to provide either the login:password basic authentication or the Api Key authentication.
When you access the `/powerdnsadmin` endpoint, you must use the Basic Auth:
```bash
# Encode your user and password to base64
$ echo -n 'admin:admin'|base64
YWRtaW46YWRtaW4=
# Use the ouput as your basic auth header
curl -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X <method> <url>
```
When you access the `/server` endpoint, you must use the ApiKey
```bash
# Use the already base64 encoded key in your header
curl -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' -X <method> <url>
```
Finally, the `/sync_domains` endpoint accepts both basic and apikey authentication
#### Examples
Creating domain via `/powerdnsadmin`:
```bash
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/zones --data '{"name": "yourdomain.com.", "kind": "NATIVE", "nameservers": ["ns1.mydomain.com."]}'
```
Creating an apikey which has the Administrator role:
```bash
# Create the key
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/apikeys --data '{"description": "masterkey","domains":[], "role": "Administrator"}'
```
Example response (don't forget to save the plain key from the output)
```json
[
{
"accounts": [],
"description": "masterkey",
"domains": [],
"role": {
"name": "Administrator",
"id": 1
},
"id": 2,
"plain_key": "aGCthP3KLAeyjZI"
}
]
```
We can use the apikey for all calls to PowerDNS (don't forget to specify Content-Type):
Getting powerdns configuration (Administrator Key is needed):
```bash
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/config
```
Creating and updating records:
```bash
curl -X PATCH -H 'Content-Type: application/json' --data '{"rrsets": [{"name": "test1.yourdomain.com.","type": "A","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "192.0.2.5", "disabled": false} ]},{"name": "test2.yourdomain.com.","type": "AAAA","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "2001:db8::6", "disabled": false} ]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://127.0.0.1:9191/api/v1/servers/localhost/zones/yourdomain.com.
```
Getting a domain:
```bash
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
```
List a zone's records:
```bash
curl -H 'Content-Type: application/json' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
```
Add a new record:
```bash
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.5.4", "disabled": false } ] } ] }' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
```
Update a record:
```bash
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.2.5", "disabled": false, "name": "test.yourdomain.com.", "ttl": 86400, "type": "A"}]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
```
Delete a record:
```bash
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "DELETE"}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq
```
### Generate ER diagram
With docker
```bash
# Install build packages
apt-get install python-dev graphviz libgraphviz-dev pkg-config
# Get the required python libraries
pip install graphviz mysqlclient ERAlchemy
# Start the docker container
docker-compose up -d
# Set environment variables
source .env
# Generate the diagrams
eralchemy -i 'mysql://${PDA_DB_USER}:${PDA_DB_PASSWORD}@'$(docker inspect powerdns-admin-mysql|jq -jr '.[0].NetworkSettings.Networks.powerdnsadmin_default.IPAddress')':3306/powerdns_admin' -o /tmp/output.pdf
```

View file

@ -1,99 +0,0 @@
### OAuth Authentication
#### Microsoft Azure
To link to Azure for authentication, you need to register PowerDNS-Admin in Azure. This requires your PowerDNS-Admin web interface to use an HTTPS URL.
* Under the Azure Active Directory, select App Registrations, and create a new one. Give it any name you want, and the Redirect URI shoule be type 'Web' and of the format https://powerdnsadmin/azure/authorized (replace the host name approriately).
* Select the newly-created registration
* On the Overview page, the Application ID is your new Client ID to use with PowerDNS-Admin
* On the Overview page, make a note of your Directory/Tenant ID - you need it for the API URLs later
* Ensure Access Tokens are enabled in the Authentication section
* Under Certificates and Secrets, create a new Client Secret. Note this secret as it is the new Client Secret to use with PowerDNS-Admin
* Under API Permissions, you need to add permissions. Add permissions for Graph API, Delegated. Add email, openid, profile, User.Read and possibly User.Read.All. You then need to grant admin approval for your organisation.
Now you can enable the OAuth in PowerDNS-Admin.
* For the Scope, use 'User.Read openid mail profile'
* Replace the [tenantID] in the default URLs for authorize and token with your Tenant ID.
* Restart PowerDNS-Admin
This should allow you to log in using OAuth.
#### Keycloak
To link to Keycloak for authentication, you need to create a new client in the Keycloak Administration Console.
* Log in to the Keycloak Administration Console
* Go to Clients > Create
* Enter a Client ID (for example 'powerdns-admin') and click 'Save'
* Scroll down to 'Access Type' and choose 'Confidential'.
* Scroll down to 'Valid Redirect URIs' and enter 'https://<pdnsa address>/oidc/authorized'
* Click 'Save'
* Go to the 'Credentials' tab and copy the Client Secret
* Log in to PowerDNS-Admin and go to 'Settings > Authentication > OpenID Connect OAuth'
* Enter the following details:
* Client key -> Client ID
* Client secret > Client secret copied from keycloak
* Scope: `profile`
* API URL: https://<keycloak url>/auth/realms/<realm>/protocol/openid-connect/
* Token URL: https://<keycloak url>/auth/realms/<realm>/protocol/openid-connect/token
* Authorize URL: https://<keycloak url>/auth/realms/<realm>/protocol/openid-connect/auth
* Logout URL: https://<keycloak url>/auth/realms/<realm>/protocol/openid-connect/logout
* Leave the rest default
* Save the changes and restart PowerDNS-Admin
* Use the new 'Sign in using OpenID Connect' button to log in.
#### OpenID Connect OAuth
To link to oidc service for authenticationregister your PowerDNS-Admin in the OIDC Provider. This requires your PowerDNS-Admin web interface to use an HTTPS URL.
Enable OpenID Connect OAuth option.
* Client key, The client ID
* Scope, The scope of the data.
* API URL, <oidc_provider_link>/auth (The ending can be different with each provider)
* Token URL, <oidc_provider_link>/token
* Authorize URL, <oidc_provider_link>/auth
* Logout URL, <oidc_provider_link>/logout
* Username, This will be the claim that will be used as the username. (Usually preferred_username)
* First Name, This will be the firstname of the user. (Usually given_name)
* Last Name, This will be the lastname of the user. (Usually family_name)
* Email, This will be the email of the user. (Usually email)
#### To create accounts on oidc login use the following properties:
* Autoprovision Account Name Property, This property will set the name of the created account.
This property can be a string or a list.
* Autoprovision Account Description Property, This property will set the description of the created account.
This property can be a string or a list.
If we get a variable named "groups" and "groups_description" from our IdP.
This variable contains groups that the user is a part of.
We will put the variable name "groups" in the "Name Property" and "groups_description" in the "Description Property".
This will result in the following account being created:
Input we get from the Idp:
```
{
"preferred_username": "example_username",
"given_name": "example_firstame",
"family_name": "example_lastname",
"email": "example_email",
"groups": ["github", "gitlab"]
"groups_description": ["github.com", "gitlab.com"]
}
```
The user properties will be:
```
Username: customer_username
First Name: customer_firstame
Last Name: customer_lastname
Email: customer_email
Role: User
```
The groups properties will be:
```
Name: github Description: github.com Members: example_username
Name: gitlab Description: gitlab.com Members: example_username
```
If the option "delete_sso_accounts" is turned on the user will only be apart of groups the IdP provided and removed from all other accoubnts.

View file

@ -1,24 +0,0 @@
### Running tests
**NOTE:** Tests will create `__pycache__` folders which will be owned by root, which might be issue during rebuild
thus (e.g. invalid tar headers message) when such situation occurs, you need to remove those folders as root
1. Build images
```
docker-compose -f docker-compose-test.yml build
```
2. Run tests
```
docker-compose -f docker-compose-test.yml up
```
3. To teardown the test environment
```
docker-compose -f docker-compose-test.yml down
docker-compose -f docker-compose-test.yml rm
```

20
init_data.py Executable file
View file

@ -0,0 +1,20 @@
#!/usr/bin/env python3
from app import db
from app.models import Role, DomainTemplate
admin_role = Role(name='Administrator', description='Administrator')
user_role = Role(name='User', description='User')
template_1 = DomainTemplate(name='basic_template_1', description='Basic Template #1')
template_2 = DomainTemplate(name='basic_template_2', description='Basic Template #2')
template_3 = DomainTemplate(name='basic_template_3', description='Basic Template #3')
db.session.add(admin_role)
db.session.add(user_role)
db.session.add(template_1)
db.session.add(template_2)
db.session.add(template_3)
db.session.commit()

View file

@ -73,7 +73,6 @@ def run_migrations_online():
context.configure(connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
render_as_batch=config.get_main_option('sqlalchemy.url').startswith('sqlite:'),
**current_app.extensions['migrate'].configure_args)
try:

View file

@ -1,41 +0,0 @@
"""add apikey account mapping table
Revision ID: 0967658d9c0d
Revises: 0d3d93f1c2e0
Create Date: 2021-11-13 22:28:46.133474
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0967658d9c0d'
down_revision = '0d3d93f1c2e0'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('apikey_account',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('apikey_id', sa.Integer(), nullable=False),
sa.Column('account_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['account_id'], ['account.id'], ),
sa.ForeignKeyConstraint(['apikey_id'], ['apikey.id'], ),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('history', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_history_created_on'), ['created_on'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('history', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_history_created_on'))
op.drop_table('apikey_account')
# ### end Alembic commands ###

View file

@ -1,34 +0,0 @@
"""Add domain_id to history table
Revision ID: 0d3d93f1c2e0
Revises: 3f76448bb6de
Create Date: 2021-02-15 17:23:05.688241
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0d3d93f1c2e0'
down_revision = '3f76448bb6de'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('history', schema=None) as batch_op:
batch_op.add_column(sa.Column('domain_id', sa.Integer(), nullable=True))
batch_op.create_foreign_key('fk_domain_id', 'domain', ['domain_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('history', schema=None) as batch_op:
batch_op.drop_constraint('fk_domain_id', type_='foreignkey')
batch_op.drop_column('domain_id')
# ### end Alembic commands ###

View file

@ -1,29 +0,0 @@
"""Remove user avatar
Revision ID: 0fb6d23a4863
Revises: 654298797277
Create Date: 2019-12-02 10:29:41.945044
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '0fb6d23a4863'
down_revision = '654298797277'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user') as batch_op:
batch_op.drop_column('avatar')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('avatar', mysql.VARCHAR(length=128), nullable=True))
# ### end Alembic commands ###

View file

@ -33,9 +33,8 @@ def update_data():
)
def upgrade():
with op.batch_alter_table('setting') as batch_op:
# change column data type
batch_op.alter_column('value', existing_type=sa.String(256), type_=sa.Text())
# change column data type
op.alter_column('setting', 'value', existing_type=sa.String(256), type_=sa.Text())
# update data for new schema
update_data()
@ -43,6 +42,5 @@ def upgrade():
def downgrade():
# delete added records in previous version
op.execute("DELETE FROM setting WHERE id > 41")
with op.batch_alter_table('setting') as batch_op:
# change column data type
batch_op.alter_column('value', existing_type=sa.Text(), type_=sa.String(256))
# change column data type
op.alter_column('setting', 'value', existing_type=sa.Text(), type_=sa.String(256))

View file

@ -23,10 +23,8 @@ def upgrade():
# written to the DB.
op.execute("DELETE FROM setting")
with op.batch_alter_table('setting') as batch_op:
# drop view column since we don't need it
batch_op.drop_column('view')
# drop view column since we don't need it
op.drop_column('setting', 'view')
def downgrade():
with op.batch_alter_table('setting') as batch_op:
batch_op.add_column(sa.Column('view', sa.String(length=64), nullable=True))
op.add_column('setting', sa.Column('view', sa.String(length=64), nullable=True))

View file

@ -1,27 +0,0 @@
"""Add user.confirmed column
Revision ID: 3f76448bb6de
Revises: b0fea72a3f20
Create Date: 2019-12-21 17:11:36.564632
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3f76448bb6de'
down_revision = 'b0fea72a3f20'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table('user') as batch_op:
batch_op.add_column(
sa.Column('confirmed', sa.Boolean(), nullable=False,
default=False))
def downgrade():
with op.batch_alter_table('user') as batch_op:
batch_op.drop_column('confirmed')

View file

@ -82,7 +82,7 @@ def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
## NOTE:
## - Drop action does not work on sqlite3
## - This action touches the `setting` table which loaded in views.py
## - This action touchs the `setting` table which loaded in views.py
## during app initlization, so the downgrade function won't work
## unless we temporary remove importing `views` from `app/__init__.py`
op.drop_column('setting', 'view')

View file

@ -1,43 +0,0 @@
"""Upgrade DB Schema
Revision ID: 654298797277
Revises: 31a4ed468b18
Create Date: 2018-12-23 22:18:01.904885
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '654298797277'
down_revision = '31a4ed468b18'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('apikey',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('key', sa.String(length=255), nullable=False),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['role.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('key')
)
op.create_table('domain_apikey',
sa.Column('domain_id', sa.Integer(), nullable=True),
sa.Column('apikey_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['apikey_id'], ['apikey.id'], ),
sa.ForeignKeyConstraint(['domain_id'], ['domain.id'], )
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('domain_apikey')
op.drop_table('apikey')
# ### end Alembic commands ###

View file

@ -1,30 +0,0 @@
"""Add comment column in domain template record table
Revision ID: 856bb94b7040
Revises: 0fb6d23a4863
Create Date: 2019-12-09 17:17:46.257906
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '856bb94b7040'
down_revision = '0fb6d23a4863'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('domain_template_record',
sa.Column('comment', sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('domain_template_record') as batch_op:
batch_op.drop_column('comment')
# ### end Alembic commands ###

View file

@ -1,35 +0,0 @@
"""Update domain serial columns type
Revision ID: b0fea72a3f20
Revises: 856bb94b7040
Create Date: 2019-12-20 09:18:51.541569
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b0fea72a3f20'
down_revision = '856bb94b7040'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table('domain') as batch_op:
batch_op.alter_column('serial',
existing_type=sa.Integer(),
type_=sa.BigInteger())
batch_op.alter_column('notified_serial',
existing_type=sa.Integer(),
type_=sa.BigInteger())
def downgrade():
with op.batch_alter_table('domain') as batch_op:
batch_op.alter_column('serial',
existing_type=sa.BigInteger(),
type_=sa.Integer())
batch_op.alter_column('notified_serial',
existing_type=sa.BigInteger(),
type_=sa.Integer())

View file

@ -1,15 +1,10 @@
{
"dependencies": {
"admin-lte": "2.4.9",
"bootstrap": "^3.4.1",
"bootstrap-datepicker": "^1.8.0",
"admin-lte": "2.4.3",
"bootstrap-validator": "^0.11.9",
"datatables.net-plugins": "^1.10.19",
"icheck": "^1.0.2",
"jquery-slimscroll": "^1.3.8",
"jquery-ui-dist": "^1.12.1",
"jquery.quicksearch": "^2.4.0",
"jtimeout": "^3.1.0",
"multiselect": "^0.9.12"
}
}

View file

@ -1,127 +0,0 @@
import os
import logging
from flask import Flask
from flask_seasurf import SeaSurf
from flask_mail import Mail
from werkzeug.middleware.proxy_fix import ProxyFix
from flask_session import Session
from .lib import utils
def create_app(config=None):
from . import models, routes, services
from .assets import assets
app = Flask(__name__)
# Read log level from environment variable
log_level_name = os.environ.get('PDNS_ADMIN_LOG_LEVEL', 'WARNING')
log_level = logging.getLevelName(log_level_name.upper())
# Setting logger
logging.basicConfig(
level=log_level,
format=
"[%(asctime)s] [%(filename)s:%(lineno)d] %(levelname)s - %(message)s")
# If we use Docker + Gunicorn, adjust the
# log handler
if "GUNICORN_LOGLEVEL" in os.environ:
gunicorn_logger = logging.getLogger("gunicorn.error")
app.logger.handlers = gunicorn_logger.handlers
app.logger.setLevel(gunicorn_logger.level)
# Proxy
app.wsgi_app = ProxyFix(app.wsgi_app)
# CSRF protection
csrf = SeaSurf(app)
csrf.exempt(routes.index.dyndns_checkip)
csrf.exempt(routes.index.dyndns_update)
csrf.exempt(routes.index.saml_authorized)
csrf.exempt(routes.api.api_login_create_zone)
csrf.exempt(routes.api.api_login_delete_zone)
csrf.exempt(routes.api.api_generate_apikey)
csrf.exempt(routes.api.api_delete_apikey)
csrf.exempt(routes.api.api_update_apikey)
csrf.exempt(routes.api.api_zone_subpath_forward)
csrf.exempt(routes.api.api_zone_forward)
csrf.exempt(routes.api.api_create_zone)
csrf.exempt(routes.api.api_create_account)
csrf.exempt(routes.api.api_delete_account)
csrf.exempt(routes.api.api_update_account)
csrf.exempt(routes.api.api_create_user)
csrf.exempt(routes.api.api_delete_user)
csrf.exempt(routes.api.api_update_user)
csrf.exempt(routes.api.api_list_account_users)
csrf.exempt(routes.api.api_add_account_user)
csrf.exempt(routes.api.api_remove_account_user)
csrf.exempt(routes.api.api_zone_cryptokeys)
csrf.exempt(routes.api.api_zone_cryptokey)
# Load config from env variables if using docker
if os.path.exists(os.path.join(app.root_path, 'docker_config.py')):
app.config.from_object('powerdnsadmin.docker_config')
else:
# Load default configuration
app.config.from_object('powerdnsadmin.default_config')
# Load config file from FLASK_CONF env variable
if 'FLASK_CONF' in os.environ:
app.config.from_envvar('FLASK_CONF')
# Load app sepecified configuration
if config is not None:
if isinstance(config, dict):
app.config.update(config)
elif config.endswith('.py'):
app.config.from_pyfile(config)
# HSTS
if app.config.get('HSTS_ENABLED'):
from flask_sslify import SSLify
_sslify = SSLify(app) # lgtm [py/unused-local-variable]
# Load Flask-Session
if app.config.get('FILESYSTEM_SESSIONS_ENABLED'):
app.config['SESSION_TYPE'] = 'filesystem'
sess = Session()
sess.init_app(app)
# SMTP
app.mail = Mail(app)
# Load app's components
assets.init_app(app)
models.init_app(app)
routes.init_app(app)
services.init_app(app)
# Register filters
app.jinja_env.filters['display_record_name'] = utils.display_record_name
app.jinja_env.filters['display_master_name'] = utils.display_master_name
app.jinja_env.filters['display_second_to_time'] = utils.display_time
app.jinja_env.filters[
'email_to_gravatar_url'] = utils.email_to_gravatar_url
app.jinja_env.filters[
'display_setting_state'] = utils.display_setting_state
app.jinja_env.filters['pretty_domain_name'] = utils.pretty_domain_name
# Register context proccessors
from .models.setting import Setting
@app.context_processor
def inject_sitename():
setting = Setting().get('site_name')
return dict(SITE_NAME=setting)
@app.context_processor
def inject_setting():
setting = Setting()
return dict(SETTING=setting)
@app.context_processor
def inject_mode():
setting = app.config.get('OFFLINE_MODE', False)
return dict(OFFLINE_MODE=setting)
return app

View file

@ -1,72 +0,0 @@
from flask_assets import Bundle, Environment, Filter
class ConcatFilter(Filter):
"""
Filter that merges files, placing a semicolon between them.
Fixes issues caused by missing semicolons at end of JS assets, for example
with last statement of jquery.pjax.js.
"""
def concat(self, out, hunks, **kw):
out.write(';'.join([h.data() for h, info in hunks]))
css_login = Bundle('node_modules/bootstrap/dist/css/bootstrap.css',
'node_modules/font-awesome/css/font-awesome.css',
'node_modules/ionicons/dist/css/ionicons.css',
'node_modules/icheck/skins/square/blue.css',
'node_modules/admin-lte/dist/css/AdminLTE.css',
filters=('cssmin', 'cssrewrite'),
output='generated/login.css')
js_login = Bundle('node_modules/jquery/dist/jquery.js',
'node_modules/bootstrap/dist/js/bootstrap.js',
'node_modules/icheck/icheck.js',
'custom/js/custom.js',
filters=(ConcatFilter, 'jsmin'),
output='generated/login.js')
js_validation = Bundle('node_modules/bootstrap-validator/dist/validator.js',
output='generated/validation.js')
css_main = Bundle(
'node_modules/bootstrap/dist/css/bootstrap.css',
'node_modules/font-awesome/css/font-awesome.css',
'node_modules/ionicons/dist/css/ionicons.css',
'node_modules/datatables.net-bs/css/dataTables.bootstrap.css',
'node_modules/icheck/skins/square/blue.css',
'node_modules/multiselect/css/multi-select.css',
'node_modules/admin-lte/dist/css/AdminLTE.css',
'node_modules/admin-lte/dist/css/skins/_all-skins.css',
'custom/css/custom.css',
'node_modules/bootstrap-datepicker/dist/css/bootstrap-datepicker.css',
filters=('cssmin', 'cssrewrite'),
output='generated/main.css')
js_main = Bundle('node_modules/jquery/dist/jquery.js',
'node_modules/jquery-ui-dist/jquery-ui.js',
'node_modules/bootstrap/dist/js/bootstrap.js',
'node_modules/datatables.net/js/jquery.dataTables.js',
'node_modules/datatables.net-bs/js/dataTables.bootstrap.js',
'node_modules/jquery-sparkline/jquery.sparkline.js',
'node_modules/jquery-slimscroll/jquery.slimscroll.js',
'node_modules/icheck/icheck.js',
'node_modules/fastclick/lib/fastclick.js',
'node_modules/moment/moment.js',
'node_modules/admin-lte/dist/js/adminlte.js',
'node_modules/multiselect/js/jquery.multi-select.js',
'node_modules/datatables.net-plugins/sorting/natural.js',
'node_modules/jtimeout/src/jTimeout.js',
'node_modules/jquery.quicksearch/src/jquery.quicksearch.js',
'custom/js/custom.js',
'node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.js',
filters=(ConcatFilter, 'jsmin'),
output='generated/main.js')
assets = Environment()
assets.register('js_login', js_login)
assets.register('js_validation', js_validation)
assets.register('css_login', css_login)
assets.register('js_main', js_main)
assets.register('css_main', css_main)

View file

@ -1,425 +0,0 @@
import base64
import binascii
from functools import wraps
from flask import g, request, abort, current_app, render_template
from flask_login import current_user
from .models import User, ApiKey, Setting, Domain, Setting
from .lib.errors import RequestIsNotJSON, NotEnoughPrivileges
from .lib.errors import DomainAccessForbidden
def admin_role_required(f):
"""
Grant access if user is in Administrator role
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if current_user.role.name != 'Administrator':
abort(403)
return f(*args, **kwargs)
return decorated_function
def operator_role_required(f):
"""
Grant access if user is in Operator role or higher
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if current_user.role.name not in ['Administrator', 'Operator']:
abort(403)
return f(*args, **kwargs)
return decorated_function
def history_access_required(f):
"""
Grant access if user is in Operator role or higher, or Users can view history
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if current_user.role.name not in [
'Administrator', 'Operator'
] and not Setting().get('allow_user_view_history'):
abort(403)
return f(*args, **kwargs)
return decorated_function
def can_access_domain(f):
"""
Grant access if:
- user is in Operator role or higher, or
- user is in granted Account, or
- user is in granted Domain
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if current_user.role.name not in ['Administrator', 'Operator']:
domain_name = kwargs.get('domain_name')
domain = Domain.query.filter(Domain.name == domain_name).first()
if not domain:
abort(404)
valid_access = Domain(id=domain.id).is_valid_access(
current_user.id)
if not valid_access:
abort(403)
return f(*args, **kwargs)
return decorated_function
def can_configure_dnssec(f):
"""
Grant access if:
- user is in Operator role or higher, or
- dnssec_admins_only is off
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if current_user.role.name not in [
'Administrator', 'Operator'
] and Setting().get('dnssec_admins_only'):
abort(403)
return f(*args, **kwargs)
return decorated_function
def can_remove_domain(f):
"""
Grant access if:
- user is in Operator role or higher, or
- allow_user_remove_domain is on
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if current_user.role.name not in [
'Administrator', 'Operator'
] and not Setting().get('allow_user_remove_domain'):
abort(403)
return f(*args, **kwargs)
return decorated_function
def can_create_domain(f):
"""
Grant access if:
- user is in Operator role or higher, or
- allow_user_create_domain is on
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if current_user.role.name not in [
'Administrator', 'Operator'
] and not Setting().get('allow_user_create_domain'):
abort(403)
return f(*args, **kwargs)
return decorated_function
def api_basic_auth(f):
@wraps(f)
def decorated_function(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if auth_header:
auth_header = auth_header.replace('Basic ', '', 1)
try:
auth_header = str(base64.b64decode(auth_header), 'utf-8')
username, password = auth_header.split(":")
except binascii.Error as e:
current_app.logger.error(
'Invalid base64-encoded of credential. Error {0}'.format(
e))
abort(401)
except TypeError as e:
current_app.logger.error('Error: {0}'.format(e))
abort(401)
user = User(username=username,
password=password,
plain_text_password=password)
try:
if Setting().get('verify_user_email') and user.email and not user.confirmed:
current_app.logger.warning(
'Basic authentication failed for user {} because of unverified email address'
.format(username))
abort(401)
auth_method = request.args.get('auth_method', 'LOCAL')
auth_method = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL'
auth = user.is_validate(method=auth_method,
src_ip=request.remote_addr)
if not auth:
current_app.logger.error('Checking user password failed')
abort(401)
else:
user = User.query.filter(User.username == username).first()
current_user = user # lgtm [py/unused-local-variable]
except Exception as e:
current_app.logger.error('Error: {0}'.format(e))
abort(401)
else:
current_app.logger.error('Error: Authorization header missing!')
abort(401)
return f(*args, **kwargs)
return decorated_function
def is_json(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if request.method in ['POST', 'PUT', 'PATCH']:
if not request.is_json:
raise RequestIsNotJSON()
return f(*args, **kwargs)
return decorated_function
def callback_if_request_body_contains_key(callback, http_methods=[], keys=[]):
"""
If request body contains one or more of specified keys, call
:param callback
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
check_current_http_method = not http_methods or request.method in http_methods
if (check_current_http_method and
set(request.get_json(force=True).keys()).intersection(set(keys))
):
callback(*args, **kwargs)
return f(*args, **kwargs)
return decorated_function
return decorator
def api_role_can(action, roles=None, allow_self=False):
"""
Grant access if:
- user is in the permitted roles
- allow_self and kwargs['user_id'] = current_user.id
- allow_self and kwargs['username'] = current_user.username
"""
if roles is None:
roles = ['Administrator', 'Operator']
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
try:
user_id = int(kwargs.get('user_id'))
except:
user_id = None
try:
username = kwargs.get('username')
except:
username = None
if (
(current_user.role.name in roles) or
(allow_self and user_id and current_user.id == user_id) or
(allow_self and username and current_user.username == username)
):
return f(*args, **kwargs)
msg = (
"User {} with role {} does not have enough privileges to {}"
).format(current_user.username, current_user.role.name, action)
raise NotEnoughPrivileges(message=msg)
return decorated_function
return decorator
def api_can_create_domain(f):
"""
Grant access if:
- user is in Operator role or higher, or
- allow_user_create_domain is on
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if current_user.role.name not in [
'Administrator', 'Operator'
] and not Setting().get('allow_user_create_domain'):
msg = "User {0} does not have enough privileges to create domain"
current_app.logger.error(msg.format(current_user.username))
raise NotEnoughPrivileges()
return f(*args, **kwargs)
return decorated_function
def apikey_can_create_domain(f):
"""
Grant access if:
- user is in Operator role or higher, or
- allow_user_create_domain is on
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if g.apikey.role.name not in [
'Administrator', 'Operator'
] and not Setting().get('allow_user_create_domain'):
msg = "ApiKey #{0} does not have enough privileges to create domain"
current_app.logger.error(msg.format(g.apikey.id))
raise NotEnoughPrivileges()
return f(*args, **kwargs)
return decorated_function
def apikey_can_remove_domain(http_methods=[]):
"""
Grant access if:
- user is in Operator role or higher, or
- allow_user_remove_domain is on
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
check_current_http_method = not http_methods or request.method in http_methods
if (check_current_http_method and
g.apikey.role.name not in ['Administrator', 'Operator'] and
not Setting().get('allow_user_remove_domain')
):
msg = "ApiKey #{0} does not have enough privileges to remove domain"
current_app.logger.error(msg.format(g.apikey.id))
raise NotEnoughPrivileges()
return f(*args, **kwargs)
return decorated_function
return decorator
def apikey_is_admin(f):
"""
Grant access if user is in Administrator role
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if g.apikey.role.name != 'Administrator':
msg = "Apikey {0} does not have enough privileges to create domain"
current_app.logger.error(msg.format(g.apikey.id))
raise NotEnoughPrivileges()
return f(*args, **kwargs)
return decorated_function
def apikey_can_access_domain(f):
"""
Grant access if:
- user has Operator role or higher, or
- user has explicitly been granted access to domain
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if g.apikey.role.name not in ['Administrator', 'Operator']:
zone_id = kwargs.get('zone_id').rstrip(".")
domain_names = [item.name for item in g.apikey.domains]
accounts = g.apikey.accounts
accounts_domains = [domain.name for a in accounts for domain in a.domains]
allowed_domains = set(domain_names + accounts_domains)
if zone_id not in allowed_domains:
raise DomainAccessForbidden()
return f(*args, **kwargs)
return decorated_function
def apikey_can_configure_dnssec(http_methods=[]):
"""
Grant access if:
- user is in Operator role or higher, or
- dnssec_admins_only is off
"""
def decorator(f=None):
@wraps(f)
def decorated_function(*args, **kwargs):
check_current_http_method = not http_methods or request.method in http_methods
if (check_current_http_method and
g.apikey.role.name not in ['Administrator', 'Operator'] and
Setting().get('dnssec_admins_only')
):
msg = "ApiKey #{0} does not have enough privileges to configure dnssec"
current_app.logger.error(msg.format(g.apikey.id))
raise DomainAccessForbidden(message=msg)
return f(*args, **kwargs) if f else None
return decorated_function
return decorator
def apikey_auth(f):
@wraps(f)
def decorated_function(*args, **kwargs):
auth_header = request.headers.get('X-API-KEY')
if auth_header:
try:
apikey_val = str(base64.b64decode(auth_header), 'utf-8')
except binascii.Error as e:
current_app.logger.error(
'Invalid base64-encoded of credential. Error {0}'.format(
e))
abort(401)
except TypeError as e:
current_app.logger.error('Error: {0}'.format(e))
abort(401)
apikey = ApiKey(key=apikey_val)
apikey.plain_text_password = apikey_val
try:
auth_method = 'LOCAL'
auth = apikey.is_validate(method=auth_method,
src_ip=request.remote_addr)
g.apikey = auth
except Exception as e:
current_app.logger.error('Error: {0}'.format(e))
abort(401)
else:
current_app.logger.error('Error: API key header missing!')
abort(401)
return f(*args, **kwargs)
return decorated_function
def dyndns_login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if current_user.is_authenticated is False:
return render_template('dyndns.html', response='badauth'), 200
return f(*args, **kwargs)
return decorated_function
def apikey_or_basic_auth(f):
@wraps(f)
def decorated_function(*args, **kwargs):
api_auth_header = request.headers.get('X-API-KEY')
if api_auth_header:
return apikey_auth(f)(*args, **kwargs)
else:
return api_basic_auth(f)(*args, **kwargs)
return decorated_function

View file

@ -1,34 +0,0 @@
import os
import urllib.parse
basedir = os.path.abspath(os.path.dirname(__file__))
### BASIC APP CONFIG
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
BIND_ADDRESS = '0.0.0.0'
PORT = 9191
HSTS_ENABLED = False
OFFLINE_MODE = False
FILESYSTEM_SESSIONS_ENABLED = False
### DATABASE CONFIG
SQLA_DB_USER = 'pda'
SQLA_DB_PASSWORD = 'changeme'
SQLA_DB_HOST = '127.0.0.1'
SQLA_DB_NAME = 'pda'
SQLALCHEMY_TRACK_MODIFICATIONS = True
### DATABASE - MySQL
SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format(
urllib.parse.quote_plus(SQLA_DB_USER),
urllib.parse.quote_plus(SQLA_DB_PASSWORD),
SQLA_DB_HOST,
SQLA_DB_NAME
)
### DATABASE - SQLite
# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
# SAML Authnetication
SAML_ENABLED = False
SAML_ASSERTION_ENCRYPTED = True

View file

@ -1,173 +0,0 @@
class StructuredException(Exception):
status_code = 0
def __init__(self, name=None, message="You want override this error!"):
Exception.__init__(self)
self.message = message
self.name = name
def to_dict(self):
rv = dict()
msg = ''
if self.name:
msg = '{0} {1}'.format(self.message, self.name)
else:
msg = self.message
rv['msg'] = msg
return rv
class DomainNotExists(StructuredException):
status_code = 404
def __init__(self, name=None, message="Domain does not exist"):
StructuredException.__init__(self)
self.message = message
self.name = name
class DomainAlreadyExists(StructuredException):
status_code = 409
def __init__(self, name=None, message="Domain already exists"):
StructuredException.__init__(self)
self.message = message
self.name = name
class DomainAccessForbidden(StructuredException):
status_code = 403
def __init__(self, name=None, message="Domain access not allowed"):
StructuredException.__init__(self)
self.message = message
self.name = name
class ApiKeyCreateFail(StructuredException):
status_code = 500
def __init__(self, name=None, message="Creation of api key failed"):
StructuredException.__init__(self)
self.message = message
self.name = name
class ApiKeyNotUsable(StructuredException):
status_code = 400
def __init__(
self,
name=None,
message=("Api key must have domains or accounts"
" or an administrative role")):
StructuredException.__init__(self)
self.message = message
self.name = name
class NotEnoughPrivileges(StructuredException):
status_code = 401
def __init__(self, name=None, message="Not enough privileges"):
StructuredException.__init__(self)
self.message = message
self.name = name
class RequestIsNotJSON(StructuredException):
status_code = 400
def __init__(self, name=None, message="Request is not json"):
StructuredException.__init__(self)
self.message = message
self.name = name
class AccountCreateFail(StructuredException):
status_code = 500
def __init__(self, name=None, message="Creation of account failed"):
StructuredException.__init__(self)
self.message = message
self.name = name
class AccountCreateDuplicate(StructuredException):
status_code = 409
def __init__(self, name=None, message="Creation of account failed"):
StructuredException.__init__(self)
self.message = message
self.name = name
class AccountUpdateFail(StructuredException):
status_code = 500
def __init__(self, name=None, message="Update of account failed"):
StructuredException.__init__(self)
self.message = message
self.name = name
class AccountDeleteFail(StructuredException):
status_code = 500
def __init__(self, name=None, message="Delete of account failed"):
StructuredException.__init__(self)
self.message = message
self.name = name
class AccountNotExists(StructuredException):
status_code = 404
def __init__(self, name=None, message="Account does not exist"):
StructuredException.__init__(self)
self.message = message
self.name = name
class UserCreateFail(StructuredException):
status_code = 500
def __init__(self, name=None, message="Creation of user failed"):
StructuredException.__init__(self)
self.message = message
self.name = name
class UserCreateDuplicate(StructuredException):
status_code = 409
def __init__(self, name=None, message="Creation of user failed"):
StructuredException.__init__(self)
self.message = message
self.name = name
class UserUpdateFail(StructuredException):
status_code = 500
def __init__(self, name=None, message="Update of user failed"):
StructuredException.__init__(self)
self.message = message
self.name = name
class UserUpdateFailEmail(StructuredException):
status_code = 409
def __init__(self, name=None, message="Update of user failed"):
StructuredException.__init__(self)
self.message = message
self.name = name
class UserDeleteFail(StructuredException):
status_code = 500
def __init__(self, name=None, message="Delete of user failed"):
StructuredException.__init__(self)
self.message = message
self.name = name

View file

@ -1,39 +0,0 @@
import requests
from urllib.parse import urljoin
from flask import request, current_app
from ..models import Setting
def forward_request():
pdns_api_url = Setting().get('pdns_api_url')
pdns_api_key = Setting().get('pdns_api_key')
headers = {}
data = None
msg_str = "Sending request to powerdns API {0}"
if request.method != 'GET' and request.method != 'DELETE':
msg = msg_str.format(request.get_json(force=True))
current_app.logger.debug(msg)
data = request.get_json(force=True)
verify = False
headers = {
'user-agent': 'powerdns-admin/api',
'pragma': 'no-cache',
'cache-control': 'no-cache',
'accept': 'application/json; q=1',
'X-API-KEY': pdns_api_key
}
url = urljoin(pdns_api_url, request.full_path)
resp = requests.request(request.method,
url,
headers=headers,
verify=verify,
json=data)
return resp

View file

@ -1,66 +0,0 @@
from lima import fields, Schema
class DomainSchema(Schema):
id = fields.Integer()
name = fields.String()
class RoleSchema(Schema):
id = fields.Integer()
name = fields.String()
class AccountSummarySchema(Schema):
id = fields.Integer()
name = fields.String()
domains = fields.Embed(schema=DomainSchema, many=True)
class ApiKeySummarySchema(Schema):
id = fields.Integer()
description = fields.String()
class ApiKeySchema(Schema):
id = fields.Integer()
role = fields.Embed(schema=RoleSchema)
domains = fields.Embed(schema=DomainSchema, many=True)
accounts = fields.Embed(schema=AccountSummarySchema, many=True)
description = fields.String()
key = fields.String()
class ApiPlainKeySchema(Schema):
id = fields.Integer()
role = fields.Embed(schema=RoleSchema)
domains = fields.Embed(schema=DomainSchema, many=True)
accounts = fields.Embed(schema=AccountSummarySchema, many=True)
description = fields.String()
plain_key = fields.String()
class UserSchema(Schema):
id = fields.Integer()
username = fields.String()
firstname = fields.String()
lastname = fields.String()
email = fields.String()
role = fields.Embed(schema=RoleSchema)
class UserDetailedSchema(Schema):
id = fields.Integer()
username = fields.String()
firstname = fields.String()
lastname = fields.String()
email = fields.String()
role = fields.Embed(schema=RoleSchema)
accounts = fields.Embed(schema=AccountSummarySchema, many=True)
class AccountSchema(Schema):
id = fields.Integer()
name = fields.String()
description = fields.String()
contact = fields.String()
mail = fields.String()
domains = fields.Embed(schema=DomainSchema, many=True)
apikeys = fields.Embed(schema=ApiKeySummarySchema, many=True)

View file

@ -1,257 +0,0 @@
import logging
import re
import json
import requests
import hashlib
import ipaddress
from collections.abc import Iterable
from distutils.version import StrictVersion
from urllib.parse import urlparse
from datetime import datetime, timedelta
def auth_from_url(url):
auth = None
parsed_url = urlparse(url).netloc
if '@' in parsed_url:
auth = parsed_url.split('@')[0].split(':')
auth = requests.auth.HTTPBasicAuth(auth[0], auth[1])
return auth
def fetch_remote(remote_url,
method='GET',
data=None,
accept=None,
params=None,
timeout=None,
headers=None,
verify=True):
if data is not None and type(data) != str:
data = json.dumps(data)
verify = bool(verify) # enforce type boolean
our_headers = {
'user-agent': 'powerdnsadmin/0',
'pragma': 'no-cache',
'cache-control': 'no-cache'
}
if accept is not None:
our_headers['accept'] = accept
if headers is not None:
our_headers.update(headers)
r = requests.request(method,
remote_url,
headers=headers,
verify=verify,
auth=auth_from_url(remote_url),
timeout=timeout,
data=data,
params=params)
logging.debug(
'Querying remote server "{0}" ({1}) finished with code {2} (took {3}s)'
.format(remote_url, method, r.status_code, r.elapsed.total_seconds()))
try:
if r.status_code not in (200, 201, 204, 400, 409, 422):
r.raise_for_status()
except Exception as e:
msg = "Returned status {0} and content {1}".format(r.status_code, r.text)
raise RuntimeError('Error while fetching {0}. {1}'.format(
remote_url, msg))
return r
def fetch_json(remote_url,
method='GET',
data=None,
params=None,
headers=None,
timeout=None,
verify=True):
r = fetch_remote(remote_url,
method=method,
data=data,
params=params,
headers=headers,
timeout=timeout,
verify=verify,
accept='application/json; q=1')
if method == "DELETE":
return True
if r.status_code == 204:
return {}
elif r.status_code == 409:
return {
'error': 'Resource already exists or conflict',
'http_code': r.status_code
}
try:
assert ('json' in r.headers['content-type'])
except Exception as e:
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.decode('utf-8'))
except UnicodeDecodeError:
# If the decoding fails, switch to slower but probably working .json()
try:
logging.warning("UTF-8 content.decode failed, switching to slower .json method")
data = r.json()
except Exception as e:
raise e
except Exception as e:
raise RuntimeError(
'Error while loading JSON data from {0}'.format(remote_url)) from e
return data
def display_record_name(data):
record_name, domain_name = data
if record_name == domain_name:
return '@'
else:
return re.sub('\.{}$'.format(domain_name), '', record_name)
def display_master_name(data):
"""
input data: "[u'127.0.0.1', u'8.8.8.8']"
"""
matches = re.findall(r'\'(.+?)\'', data)
return ", ".join(matches)
def display_time(amount, units='s', remove_seconds=True):
"""
Convert timestamp to normal time format
"""
amount = int(amount)
INTERVALS = [(lambda mlsec: divmod(mlsec, 1000), 'ms'),
(lambda seconds: divmod(seconds, 60), 's'),
(lambda minutes: divmod(minutes, 60), 'm'),
(lambda hours: divmod(hours, 24), 'h'),
(lambda days: divmod(days, 7), 'D'),
(lambda weeks: divmod(weeks, 4), 'W'),
(lambda years: divmod(years, 12), 'M'),
(lambda decades: divmod(decades, 10), 'Y')]
for index_start, (interval, unit) in enumerate(INTERVALS):
if unit == units:
break
amount_abrev = []
last_index = 0
amount_temp = amount
for index, (formula,
abrev) in enumerate(INTERVALS[index_start:len(INTERVALS)]):
divmod_result = formula(amount_temp)
amount_temp = divmod_result[0]
amount_abrev.append((divmod_result[1], abrev))
if divmod_result[1] > 0:
last_index = index
amount_abrev_partial = amount_abrev[0:last_index + 1]
amount_abrev_partial.reverse()
final_string = ''
for amount, abrev in amount_abrev_partial:
final_string += str(amount) + abrev + ' '
if remove_seconds and 'm' in final_string:
final_string = final_string[:final_string.rfind(' ')]
return final_string[:final_string.rfind(' ')]
return final_string
def pdns_api_extended_uri(version):
"""
Check the pdns version
"""
if StrictVersion(version) >= StrictVersion('4.0.0'):
return "/api/v1"
else:
return ""
def email_to_gravatar_url(email="", size=100):
"""
AD doesn't necessarily have email
"""
if email is None:
email = ""
hash_string = hashlib.md5(email.encode('utf-8')).hexdigest()
return "https://s.gravatar.com/avatar/{0}?s={1}".format(hash_string, size)
def display_setting_state(value):
if value == 1:
return "ON"
elif value == 0:
return "OFF"
else:
return "UNKNOWN"
def validate_ipaddress(address):
try:
ip = ipaddress.ip_address(address)
except ValueError:
pass
else:
if isinstance(ip, (ipaddress.IPv4Address, ipaddress.IPv6Address)):
return [ip]
return []
def pretty_json(data):
return json.dumps(data, sort_keys=True, indent=4)
def ensure_list(l):
if not l:
l = []
elif not isinstance(l, Iterable) or isinstance(l, str):
l = [l]
yield from l
class customBoxes:
boxes = {
"reverse": (" ", " "),
"ip6arpa": ("ip6", "%.ip6.arpa"),
"inaddrarpa": ("in-addr", "%.in-addr.arpa")
}
order = ["reverse", "ip6arpa", "inaddrarpa"]
def pretty_domain_name(value):
"""
Display domain name in original format.
If it is IDN domain (Punycode starts with xn--), do the
idna decoding.
Note that any part of the domain name can be individually punycoded
"""
if isinstance(value, str):
if value.startswith('xn--') \
or value.find('.xn--') != -1:
try:
return value.encode().decode('idna')
except:
raise Exception("Cannot decode IDN domain")
else:
return value
else:
raise Exception("Require the Punycode in string format")

View file

@ -1,32 +0,0 @@
import os
from bravado_core.spec import Spec
from bravado_core.validate import validate_object
from yaml import load, Loader
def validate_zone(zone):
validate_object(spec, zone_spec, zone)
def validate_apikey(apikey):
validate_object(spec, apikey_spec, apikey)
def get_swagger_spec(spec_path):
with open(spec_path, 'r') as spec:
return load(spec.read(), Loader)
bravado_config = {
'validate_swagger_spec': False,
'validate_requests': False,
'validate_responses': False,
'use_models': True,
}
dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
spec_path = os.path.join(dir_path, "swagger-spec.yaml")
spec_dict = get_swagger_spec(spec_path)
spec = Spec.from_dict(spec_dict, config=bravado_config)
zone_spec = spec_dict['definitions']['Zone']
apikey_spec = spec_dict['definitions']['ApiKey']

View file

@ -1,24 +0,0 @@
from flask_migrate import Migrate
from .base import db
from .user import User
from .role import Role
from .account import Account
from .account_user import AccountUser
from .server import Server
from .history import History
from .api_key import ApiKey
from .api_key_account import ApiKeyAccount
from .setting import Setting
from .domain import Domain
from .domain_setting import DomainSetting
from .domain_user import DomainUser
from .domain_template import DomainTemplate
from .domain_template_record import DomainTemplateRecord
from .record import Record
from .record_entry import RecordEntry
def init_app(app):
db.init_app(app)
_migrate = Migrate(app, db) # lgtm [py/unused-local-variable]

View file

@ -1,272 +0,0 @@
import traceback
from flask import current_app
from urllib.parse import urljoin
from ..lib import utils
from .base import db
from .setting import Setting
from .user import User
from .account_user import AccountUser
class Account(db.Model):
__tablename__ = 'account'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(40), index=True, unique=True, nullable=False)
description = db.Column(db.String(128))
contact = db.Column(db.String(128))
mail = db.Column(db.String(128))
domains = db.relationship("Domain", back_populates="account")
apikeys = db.relationship("ApiKey",
secondary="apikey_account",
back_populates="accounts")
def __init__(self, name=None, description=None, contact=None, mail=None):
self.name = name
self.description = description
self.contact = contact
self.mail = mail
# PDNS configs
self.PDNS_STATS_URL = Setting().get('pdns_api_url')
self.PDNS_API_KEY = Setting().get('pdns_api_key')
self.PDNS_VERSION = Setting().get('pdns_version')
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
if self.name is not None:
self.name = ''.join(c for c in self.name.lower()
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
def __repr__(self):
return '<Account {0}r>'.format(self.name)
def get_name_by_id(self, account_id):
"""
Convert account_id to account_name
"""
account = Account.query.filter(Account.id == account_id).first()
if account is None:
return ''
return account.name
def get_id_by_name(self, account_name):
"""
Convert account_name to account_id
"""
# Skip actual database lookup for empty queries
if account_name is None or account_name == "":
return None
account = Account.query.filter(Account.name == account_name).first()
if account is None:
return None
return account.id
def create_account(self):
"""
Create a new account
"""
# Sanity check - account name
if self.name == "":
return {'status': False, 'msg': 'No account name specified'}
# check that account name is not already used
account = Account.query.filter(Account.name == self.name).first()
if account:
return {'status': False, 'msg': 'Account already exists'}
db.session.add(self)
db.session.commit()
return {'status': True, 'msg': 'Account created successfully'}
def update_account(self):
"""
Update an existing account
"""
# Sanity check - account name
if self.name == "":
return {'status': False, 'msg': 'No account name specified'}
# read account and check that it exists
account = Account.query.filter(Account.name == self.name).first()
if not account:
return {'status': False, 'msg': 'Account does not exist'}
account.description = self.description
account.contact = self.contact
account.mail = self.mail
db.session.commit()
return {'status': True, 'msg': 'Account updated successfully'}
def delete_account(self, commit=True):
"""
Delete an account
"""
# unassociate all users first
self.grant_privileges([])
try:
Account.query.filter(Account.name == self.name).delete()
if commit:
db.session.commit()
return True
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot delete account {0} from DB. DETAIL: {1}'.format(
self.name, e))
return False
def get_user(self):
"""
Get users (id) associated with this account
"""
user_ids = []
query = db.session.query(
AccountUser,
Account).filter(User.id == AccountUser.user_id).filter(
Account.id == AccountUser.account_id).filter(
Account.name == self.name).all()
for q in query:
user_ids.append(q[0].user_id)
return user_ids
def grant_privileges(self, new_user_list):
"""
Reconfigure account_user table
"""
account_id = self.get_id_by_name(self.name)
account_user_ids = self.get_user()
new_user_ids = [
u.id
for u in User.query.filter(User.username.in_(new_user_list)).all()
] if new_user_list else []
removed_ids = list(set(account_user_ids).difference(new_user_ids))
added_ids = list(set(new_user_ids).difference(account_user_ids))
try:
for uid in removed_ids:
AccountUser.query.filter(AccountUser.user_id == uid).filter(
AccountUser.account_id == account_id).delete()
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot revoke user privileges on account {0}. DETAIL: {1}'.
format(self.name, e))
try:
for uid in added_ids:
au = AccountUser(account_id, uid)
db.session.add(au)
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot grant user privileges to account {0}. DETAIL: {1}'.
format(self.name, e))
def revoke_privileges_by_id(self, user_id):
"""
Remove a single user from privilege list based on user_id
"""
new_uids = [u for u in self.get_user() if u != user_id]
users = []
for uid in new_uids:
users.append(User(id=uid).get_user_info_by_id().username)
self.grant_privileges(users)
def add_user(self, user):
"""
Add a single user to Account by User
"""
try:
au = AccountUser(self.id, user.id)
db.session.add(au)
db.session.commit()
return True
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot add user privileges on account {0}. DETAIL: {1}'.
format(self.name, e))
return False
def remove_user(self, user):
"""
Remove a single user from Account by User
"""
# TODO: This func is currently used by SAML feature in a wrong way. Fix it
try:
AccountUser.query.filter(AccountUser.user_id == user.id).filter(
AccountUser.account_id == self.id).delete()
db.session.commit()
return True
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot revoke user privileges on account {0}. DETAIL: {1}'.
format(self.name, e))
return False
def update(self):
"""
Fetch accounts from PowerDNS and syncs them into DB
"""
db_accounts = Account.query.all()
list_db_accounts = [d.name for d in db_accounts]
current_app.logger.info("Found {} accounts in PowerDNS-Admin".format(
len(list_db_accounts)))
headers = {'X-API-Key': self.PDNS_API_KEY}
try:
jdata = utils.fetch_json(
urljoin(self.PDNS_STATS_URL,
self.API_EXTENDED_URL + '/servers/localhost/zones'),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
verify=Setting().get('verify_ssl_connections'))
list_jaccount = set(d['account'] for d in jdata if d['account'])
current_app.logger.info("Found {} accounts in PowerDNS".format(
len(list_jaccount)))
try:
# Remove accounts that don't exist any more
should_removed_db_account = list(
set(list_db_accounts).difference(list_jaccount))
for account_name in should_removed_db_account:
account_id = self.get_id_by_name(account_name)
if not account_id:
continue
current_app.logger.info("Deleting account for {0}".format(account_name))
account = Account.query.get(account_id)
account.delete_account(commit=False)
except Exception as e:
current_app.logger.error(
'Can not delete account from DB. DETAIL: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
for account_name in list_jaccount:
account_id = self.get_id_by_name(account_name)
if account_id:
continue
current_app.logger.info("Creating account for {0}".format(account_name))
account = Account(name=account_name)
db.session.add(account)
db.session.commit()
current_app.logger.info('Update accounts finished')
return {
'status': 'ok',
'msg': 'Account table has been updated successfully'
}
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot update account table. Error: {0}'.format(e))
return {'status': 'error', 'msg': 'Cannot update account table'}

View file

@ -1,17 +0,0 @@
from .base import db
class AccountUser(db.Model):
__tablename__ = 'account_user'
id = db.Column(db.Integer, primary_key=True)
account_id = db.Column(db.Integer,
db.ForeignKey('account.id'),
nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
def __init__(self, account_id, user_id):
self.account_id = account_id
self.user_id = user_id
def __repr__(self):
return '<Account_User {0} {1}>'.format(self.account_id, self.user_id)

View file

@ -1,142 +0,0 @@
import secrets
import string
import bcrypt
from flask import current_app
from .base import db
from ..models.role import Role
from ..models.domain import Domain
from ..models.account import Account
class ApiKey(db.Model):
__tablename__ = "apikey"
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(255), unique=True, nullable=False)
description = db.Column(db.String(255))
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
role = db.relationship('Role', back_populates="apikeys", lazy=True)
domains = db.relationship("Domain",
secondary="domain_apikey",
back_populates="apikeys")
accounts = db.relationship("Account",
secondary="apikey_account",
back_populates="apikeys")
def __init__(self, key=None, desc=None, role_name=None, domains=[], accounts=[]):
self.id = None
self.description = desc
self.role_name = role_name
self.domains[:] = domains
self.accounts[:] = accounts
if not key:
rand_key = ''.join(
secrets.choice(string.ascii_letters + string.digits)
for _ in range(15))
self.plain_key = rand_key
self.key = self.get_hashed_password(rand_key).decode('utf-8')
current_app.logger.debug("Hashed key: {0}".format(self.key))
else:
self.key = key
def create(self):
try:
self.role = Role.query.filter(Role.name == self.role_name).first()
db.session.add(self)
db.session.commit()
except Exception as e:
current_app.logger.error('Can not update api key table. Error: {0}'.format(e))
db.session.rollback()
raise e
def delete(self):
try:
db.session.delete(self)
db.session.commit()
except Exception as e:
msg_str = 'Can not delete api key template. Error: {0}'
current_app.logger.error(msg_str.format(e))
db.session.rollback()
raise e
def update(self, role_name=None, description=None, domains=None, accounts=None):
try:
if role_name:
role = Role.query.filter(Role.name == role_name).first()
self.role_id = role.id
if description:
self.description = description
if domains is not None:
domain_object_list = Domain.query \
.filter(Domain.name.in_(domains)) \
.all()
self.domains[:] = domain_object_list
if accounts is not None:
account_object_list = Account.query \
.filter(Account.name.in_(accounts)) \
.all()
self.accounts[:] = account_object_list
db.session.commit()
except Exception as e:
msg_str = 'Update of apikey failed. Error: {0}'
current_app.logger.error(msg_str.format(e))
db.session.rollback
raise e
def get_hashed_password(self, plain_text_password=None):
# Hash a password for the first time
# (Using bcrypt, the salt is saved into the hash itself)
if plain_text_password is None:
return plain_text_password
if plain_text_password:
pw = plain_text_password
else:
pw = self.plain_text_password
# The salt value is currently re-used here intentionally because
# the implementation relies on just the API key's value itself
# for database lookup: ApiKey.is_validate() would have no way of
# discerning whether any given key is valid if bcrypt.gensalt()
# was used. As far as is known, this is fine as long as the
# value of new API keys is randomly generated in a
# cryptographically secure fashion, as this then makes
# expendable as an exception the otherwise vital protection of
# proper salting as provided by bcrypt.gensalt().
return bcrypt.hashpw(pw.encode('utf-8'),
current_app.config.get('SALT').encode('utf-8'))
def check_password(self, hashed_password):
# Check hashed password. Using bcrypt,
# the salt is saved into the hash itself
if self.plain_text_password:
return bcrypt.checkpw(self.plain_text_password.encode('utf-8'),
hashed_password.encode('utf-8'))
return False
def is_validate(self, method, src_ip=''):
"""
Validate user credential
"""
if method == 'LOCAL':
passw_hash = self.get_hashed_password(self.plain_text_password)
apikey = ApiKey.query \
.filter(ApiKey.key == passw_hash.decode('utf-8')) \
.first()
if not apikey:
raise Exception("Unauthorized")
return apikey
def associate_account(self, account):
return True
def dissociate_account(self, account):
return True
def get_accounts(self):
return True

View file

@ -1,20 +0,0 @@
from .base import db
class ApiKeyAccount(db.Model):
__tablename__ = 'apikey_account'
id = db.Column(db.Integer, primary_key=True)
apikey_id = db.Column(db.Integer,
db.ForeignKey('apikey.id'),
nullable=False)
account_id = db.Column(db.Integer,
db.ForeignKey('account.id'),
nullable=False)
db.UniqueConstraint('apikey_id', 'account_id', name='uniq_apikey_account')
def __init__(self, apikey_id, account_id):
self.apikey_id = apikey_id
self.account_id = account_id
def __repr__(self):
return '<ApiKey_Account {0} {1}>'.format(self.apikey_id, self.account_id)

View file

@ -1,7 +0,0 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
domain_apikey = db.Table(
'domain_apikey',
db.Column('domain_id', db.Integer, db.ForeignKey('domain.id')),
db.Column('apikey_id', db.Integer, db.ForeignKey('apikey.id')))

View file

@ -1,881 +0,0 @@
import re
import traceback
from flask import current_app
from urllib.parse import urljoin
from distutils.util import strtobool
from ..lib import utils
from .base import db, domain_apikey
from .setting import Setting
from .user import User
from .account import Account
from .account import AccountUser
from .domain_user import DomainUser
from .domain_setting import DomainSetting
from .history import History
class Domain(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), index=True, unique=True)
master = db.Column(db.String(128))
type = db.Column(db.String(6), nullable=False)
serial = db.Column(db.BigInteger)
notified_serial = db.Column(db.BigInteger)
last_check = db.Column(db.Integer)
dnssec = db.Column(db.Integer)
account_id = db.Column(db.Integer, db.ForeignKey('account.id'))
account = db.relationship("Account", back_populates="domains")
settings = db.relationship('DomainSetting', back_populates='domain')
apikeys = db.relationship("ApiKey",
secondary=domain_apikey,
back_populates="domains")
def __init__(self,
id=None,
name=None,
master=None,
type='NATIVE',
serial=None,
notified_serial=None,
last_check=None,
dnssec=None,
account_id=None):
self.id = id
self.name = name
self.master = master
self.type = type
self.serial = serial
self.notified_serial = notified_serial
self.last_check = last_check
self.dnssec = dnssec
self.account_id = account_id
# PDNS configs
self.PDNS_STATS_URL = Setting().get('pdns_api_url')
self.PDNS_API_KEY = Setting().get('pdns_api_key')
self.PDNS_VERSION = Setting().get('pdns_version')
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
def __repr__(self):
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 as e:
current_app.logger.error(
'Can not create setting {0} for domain {1}. {2}'.format(
setting, self.name, e))
return False
def get_domain_info(self, domain_name):
"""
Get all domains which has in PowerDNS
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
jdata = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain_name)),
headers=headers,
timeout=int(
Setting().get('pdns_api_timeout')),
verify=Setting().get('verify_ssl_connections'))
return jdata
def get_domains(self):
"""
Get all domains which has in PowerDNS
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
jdata = utils.fetch_json(
urljoin(self.PDNS_STATS_URL,
self.API_EXTENDED_URL + '/servers/localhost/zones'),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
verify=Setting().get('verify_ssl_connections'))
return jdata
def get_id_by_name(self, name):
"""
Return domain id
"""
try:
domain = Domain.query.filter(Domain.name == name).first()
return domain.id
except Exception as e:
current_app.logger.error(
'Domain does not exist. ERROR: {0}'.format(e))
return None
def update(self):
"""
Fetch zones (domains) from PowerDNS and update into DB
"""
db_domain = Domain.query.all()
list_db_domain = [d.name for d in db_domain]
dict_db_domain = dict((x.name, x) for x in db_domain)
current_app.logger.info("Found {} domains in PowerDNS-Admin".format(
len(list_db_domain)))
headers = {'X-API-Key': self.PDNS_API_KEY}
try:
jdata = utils.fetch_json(
urljoin(self.PDNS_STATS_URL,
self.API_EXTENDED_URL + '/servers/localhost/zones'),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
verify=Setting().get('verify_ssl_connections'))
list_jdomain = [d['name'].rstrip('.') for d in jdata]
current_app.logger.info(
"Found {} zones in PowerDNS server".format(len(list_jdomain)))
try:
# domains should remove from db since it doesn't exist in powerdns anymore
should_removed_db_domain = list(
set(list_db_domain).difference(list_jdomain))
for domain_name in should_removed_db_domain:
self.delete_domain_from_pdnsadmin(domain_name, do_commit=False)
except Exception as e:
current_app.logger.error(
'Can not delete domain from DB. DETAIL: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
# update/add new domain
for data in jdata:
if 'account' in data:
account_id = Account().get_id_by_name(data['account'])
else:
current_app.logger.debug(
"No 'account' data found in API result - Unsupported PowerDNS version?"
)
account_id = None
domain = dict_db_domain.get(data['name'].rstrip('.'), None)
if domain:
self.update_pdns_admin_domain(domain, account_id, data, do_commit=False)
else:
# add new domain
self.add_domain_to_powerdns_admin(domain=data, do_commit=False)
db.session.commit()
current_app.logger.info('Update domain finished')
return {
'status': 'ok',
'msg': 'Domain table has been updated successfully'
}
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot update domain table. Error: {0}'.format(e))
return {'status': 'error', 'msg': 'Cannot update domain table'}
def update_pdns_admin_domain(self, domain, account_id, data, do_commit=True):
# existing domain, only update if something actually has changed
if (domain.master != str(data['masters'])
or domain.type != data['kind']
or domain.serial != data['serial']
or domain.notified_serial != data['notified_serial']
or domain.last_check != (1 if data['last_check'] else 0)
or domain.dnssec != data['dnssec']
or domain.account_id != account_id):
domain.master = str(data['masters'])
domain.type = data['kind']
domain.serial = data['serial']
domain.notified_serial = data['notified_serial']
domain.last_check = 1 if data['last_check'] else 0
domain.dnssec = 1 if data['dnssec'] else 0
domain.account_id = account_id
try:
if do_commit:
db.session.commit()
current_app.logger.info("Updated PDNS-Admin domain {0}".format(
domain.name))
except Exception as e:
db.session.rollback()
current_app.logger.info("Rolled back Domain {0} {1}".format(
domain.name, e))
raise
def add(self,
domain_name,
domain_type,
soa_edit_api,
domain_ns=[],
domain_master_ips=[],
account_name=None):
"""
Add a domain to power dns
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
domain_name = domain_name + '.'
domain_ns = [ns + '.' for ns in domain_ns]
if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]:
soa_edit_api = 'DEFAULT'
elif soa_edit_api == 'OFF':
soa_edit_api = ''
post_data = {
"name": domain_name,
"kind": domain_type,
"masters": domain_master_ips,
"nameservers": domain_ns,
"soa_edit_api": soa_edit_api,
"account": account_name
}
try:
jdata = utils.fetch_json(
urljoin(self.PDNS_STATS_URL,
self.API_EXTENDED_URL + '/servers/localhost/zones'),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='POST',
verify=Setting().get('verify_ssl_connections'),
data=post_data)
if 'error' in jdata.keys():
current_app.logger.error(jdata['error'])
if jdata.get('http_code') == 409:
return {'status': 'error', 'msg': 'Domain already exists'}
return {'status': 'error', 'msg': jdata['error']}
else:
current_app.logger.info(
'Added domain successfully to PowerDNS: {0}'.format(
domain_name))
self.add_domain_to_powerdns_admin(domain_dict=post_data)
return {'status': 'ok', 'msg': 'Added domain successfully'}
except Exception as e:
current_app.logger.error('Cannot add domain {0} {1}'.format(
domain_name, e))
current_app.logger.debug(traceback.format_exc())
return {'status': 'error', 'msg': 'Cannot add this domain.'}
def add_domain_to_powerdns_admin(self, domain=None, domain_dict=None, do_commit=True):
"""
Read Domain from PowerDNS and add into PDNS-Admin
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
if not domain:
try:
domain = utils.fetch_json(
urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(
domain_dict['name'])),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
verify=Setting().get('verify_ssl_connections'))
except Exception as e:
current_app.logger.error('Can not read domain from PDNS')
current_app.logger.error(e)
current_app.logger.debug(traceback.format_exc())
if 'account' in domain:
account_id = Account().get_id_by_name(domain['account'])
else:
current_app.logger.debug(
"No 'account' data found in API result - Unsupported PowerDNS version?"
)
account_id = None
# add new domain
d = Domain()
d.name = domain['name'].rstrip('.') # lgtm [py/modification-of-default-value]
d.master = str(domain['masters'])
d.type = domain['kind']
d.serial = domain['serial']
d.notified_serial = domain['notified_serial']
d.last_check = domain['last_check']
d.dnssec = 1 if domain['dnssec'] else 0
d.account_id = account_id
db.session.add(d)
try:
if do_commit:
db.session.commit()
current_app.logger.info(
"Synced PowerDNS Domain to PDNS-Admin: {0}".format(d.name))
return {
'status': 'ok',
'msg': 'Added Domain successfully to PowerDNS-Admin'
}
except Exception as e:
db.session.rollback()
current_app.logger.info("Rolled back Domain {0}".format(d.name))
raise
def update_soa_setting(self, domain_name, soa_edit_api):
domain = Domain.query.filter(Domain.name == domain_name).first()
if not domain:
return {'status': 'error', 'msg': 'Domain does not exist.'}
headers = {'X-API-Key': self.PDNS_API_KEY}
if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]:
soa_edit_api = 'DEFAULT'
elif soa_edit_api == 'OFF':
soa_edit_api = ''
post_data = {"soa_edit_api": soa_edit_api, "kind": domain.type}
try:
jdata = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain.name)),
headers=headers,
timeout=int(
Setting().get('pdns_api_timeout')),
method='PUT',
verify=Setting().get('verify_ssl_connections'),
data=post_data)
if 'error' in jdata.keys():
current_app.logger.error(jdata['error'])
return {'status': 'error', 'msg': jdata['error']}
else:
current_app.logger.info(
'soa-edit-api changed for domain {0} successfully'.format(
domain_name))
return {
'status': 'ok',
'msg': 'soa-edit-api changed successfully'
}
except Exception as e:
current_app.logger.debug(e)
current_app.logger.debug(traceback.format_exc())
current_app.logger.error(
'Cannot change soa-edit-api for domain {0}'.format(
domain_name))
return {
'status': 'error',
'msg': 'Cannot change soa-edit-api for this domain.'
}
def update_kind(self, domain_name, kind, masters=[]):
"""
Update zone kind: Native / Master / Slave
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if not domain:
return {'status': 'error', 'msg': 'Domain does not exist.'}
headers = {'X-API-Key': self.PDNS_API_KEY}
post_data = {"kind": kind, "masters": masters}
try:
jdata = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain.name)),
headers=headers,
timeout=int(
Setting().get('pdns_api_timeout')),
method='PUT',
verify=Setting().get('verify_ssl_connections'),
data=post_data)
if 'error' in jdata.keys():
current_app.logger.error(jdata['error'])
return {'status': 'error', 'msg': jdata['error']}
else:
current_app.logger.info(
'Update domain kind for {0} successfully'.format(
domain_name))
return {
'status': 'ok',
'msg': 'Domain kind changed successfully'
}
except Exception as e:
current_app.logger.error(
'Cannot update kind for domain {0}. Error: {1}'.format(
domain_name, e))
current_app.logger.debug(traceback.format_exc())
return {
'status': 'error',
'msg': 'Cannot update kind for this domain.'
}
def create_reverse_domain(self, domain_name, domain_reverse_name):
"""
Check the existing reverse lookup domain,
if not exists create a new one automatically
"""
domain_obj = Domain.query.filter(Domain.name == domain_name).first()
domain_auto_ptr = DomainSetting.query.filter(
DomainSetting.domain == domain_obj).filter(
DomainSetting.setting == 'auto_ptr').first()
domain_auto_ptr = strtobool(
domain_auto_ptr.value) if domain_auto_ptr else False
system_auto_ptr = Setting().get('auto_ptr')
self.name = domain_name
domain_id = self.get_id_by_name(domain_reverse_name)
if domain_id is None and \
(
system_auto_ptr or
domain_auto_ptr
):
result = self.add(domain_reverse_name, 'Master', 'DEFAULT', [], [])
self.update()
if result['status'] == 'ok':
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'
}
domain_user_ids = self.get_user()
if len(domain_user_ids) > 0:
self.name = domain_reverse_name
self.grant_privileges(domain_user_ids)
return {
'status':
'ok',
'msg':
'New reverse lookup domain created with granted privileges'
}
return {
'status': 'ok',
'msg': 'New reverse lookup domain created without users'
}
return {'status': 'ok', 'msg': 'Reverse lookup domain already exists'}
def get_reverse_domain_name(self, reverse_host_address):
c = 1
if re.search('ip6.arpa', reverse_host_address):
for i in range(1, 32, 1):
address = re.search(
'((([a-f0-9]\.){' + str(i) + '})(?P<ipname>.+6.arpa)\.?)',
reverse_host_address)
if None != self.get_id_by_name(address.group('ipname')):
c = i
break
return re.search(
'((([a-f0-9]\.){' + str(c) + '})(?P<ipname>.+6.arpa)\.?)',
reverse_host_address).group('ipname')
else:
for i in range(1, 4, 1):
address = re.search(
'((([0-9]+\.){' + str(i) + '})(?P<ipname>.+r.arpa)\.?)',
reverse_host_address)
if None != self.get_id_by_name(address.group('ipname')):
c = i
break
return re.search(
'((([0-9]+\.){' + str(c) + '})(?P<ipname>.+r.arpa)\.?)',
reverse_host_address).group('ipname')
def delete(self, domain_name):
"""
Delete a single domain name from powerdns
"""
try:
self.delete_domain_from_powerdns(domain_name)
self.delete_domain_from_pdnsadmin(domain_name)
return {'status': 'ok', 'msg': 'Delete domain successfully'}
except Exception as e:
current_app.logger.error(
'Cannot delete domain {0}'.format(domain_name))
current_app.logger.error(e)
current_app.logger.debug(traceback.format_exc())
return {'status': 'error', 'msg': 'Cannot delete domain'}
def delete_domain_from_powerdns(self, domain_name):
"""
Delete a single domain name from powerdns
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain_name)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='DELETE',
verify=Setting().get('verify_ssl_connections'))
current_app.logger.info(
'Deleted domain successfully from PowerDNS: {0}'.format(
domain_name))
return {'status': 'ok', 'msg': 'Delete domain successfully'}
def delete_domain_from_pdnsadmin(self, domain_name, do_commit=True):
# Revoke permission before deleting domain
domain = Domain.query.filter(Domain.name == domain_name).first()
domain_user = DomainUser.query.filter(
DomainUser.domain_id == domain.id)
if domain_user:
domain_user.delete()
domain_setting = DomainSetting.query.filter(
DomainSetting.domain_id == domain.id)
if domain_setting:
domain_setting.delete()
domain.apikeys[:] = []
# Remove history for domain
domain_history = History.query.filter(
History.domain_id == domain.id
)
if domain_history:
domain_history.delete()
# then remove domain
Domain.query.filter(Domain.name == domain_name).delete()
if do_commit:
db.session.commit()
current_app.logger.info(
"Deleted domain successfully from pdnsADMIN: {}".format(
domain_name))
def get_user(self):
"""
Get users (id) who have access to this domain name
"""
user_ids = []
query = db.session.query(
DomainUser, Domain).filter(User.id == DomainUser.user_id).filter(
Domain.id == DomainUser.domain_id).filter(
Domain.name == self.name).all()
for q in query:
user_ids.append(q[0].user_id)
return user_ids
def grant_privileges(self, new_user_ids):
"""
Reconfigure domain_user table
"""
domain_id = self.get_id_by_name(self.name)
domain_user_ids = self.get_user()
removed_ids = list(set(domain_user_ids).difference(new_user_ids))
added_ids = list(set(new_user_ids).difference(domain_user_ids))
try:
for uid in removed_ids:
DomainUser.query.filter(DomainUser.user_id == uid).filter(
DomainUser.domain_id == domain_id).delete()
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot revoke user privileges on domain {0}. DETAIL: {1}'.
format(self.name, e))
current_app.logger.debug(print(traceback.format_exc()))
try:
for uid in added_ids:
du = DomainUser(domain_id, uid)
db.session.add(du)
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot grant user privileges to domain {0}. DETAIL: {1}'.
format(self.name, e))
current_app.logger.debug(print(traceback.format_exc()))
def revoke_privileges_by_id(self, user_id):
"""
Remove a single user from privilege list based on user_id
"""
new_uids = [u for u in self.get_user() if u != user_id]
users = []
for uid in new_uids:
users.append(User(id=uid).get_user_info_by_id().username)
self.grant_privileges(users)
def add_user(self, user):
"""
Add a single user to Domain by User
"""
try:
du = DomainUser(self.id, user.id)
db.session.add(du)
db.session.commit()
return True
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot add user privileges on domain {0}. DETAIL: {1}'.
format(self.name, e))
return False
def update_from_master(self, domain_name):
"""
Update records from Master DNS server
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
headers = {'X-API-Key': self.PDNS_API_KEY}
try:
r = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}/axfr-retrieve'.format(
domain.name)),
headers=headers,
timeout=int(
Setting().get('pdns_api_timeout')),
method='PUT',
verify=Setting().get('verify_ssl_connections'))
return {'status': 'ok', 'msg': r.get('result')}
except Exception as e:
current_app.logger.error(
'Cannot update from master. DETAIL: {0}'.format(e))
return {
'status':
'error',
'msg':
'There was something wrong, please contact administrator'
}
else:
return {'status': 'error', 'msg': 'This domain does not exist'}
def get_domain_dnssec(self, domain_name):
"""
Get domain DNSSEC information
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
headers = {'X-API-Key': self.PDNS_API_KEY}
try:
jdata = utils.fetch_json(
urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}/cryptokeys'.format(
domain.name)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='GET',
verify=Setting().get('verify_ssl_connections'))
if 'error' in jdata:
return {
'status': 'error',
'msg': 'DNSSEC is not enabled for this domain'
}
else:
return {'status': 'ok', 'dnssec': jdata}
except Exception as e:
current_app.logger.error(
'Cannot get domain dnssec. DETAIL: {0}'.format(e))
return {
'status':
'error',
'msg':
'There was something wrong, please contact administrator'
}
else:
return {'status': 'error', 'msg': 'This domain does not exist'}
def enable_domain_dnssec(self, domain_name):
"""
Enable domain DNSSEC
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
headers = {'X-API-Key': self.PDNS_API_KEY}
try:
# Enable API-RECTIFY for domain, BEFORE activating DNSSEC
post_data = {"api_rectify": True}
jdata = utils.fetch_json(
urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain.name)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='PUT',
verify=Setting().get('verify_ssl_connections'),
data=post_data)
if 'error' in jdata:
return {
'status': 'error',
'msg':
'API-RECTIFY could not be enabled for this domain',
'jdata': jdata
}
# Activate DNSSEC
post_data = {"keytype": "ksk", "active": True}
jdata = utils.fetch_json(
urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}/cryptokeys'.format(
domain.name)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='POST',
verify=Setting().get('verify_ssl_connections'),
data=post_data)
if 'error' in jdata:
return {
'status':
'error',
'msg':
'Cannot enable DNSSEC for this domain. Error: {0}'.
format(jdata['error']),
'jdata':
jdata
}
return {'status': 'ok'}
except Exception as e:
current_app.logger.error(
'Cannot enable dns sec. DETAIL: {}'.format(e))
current_app.logger.debug(traceback.format_exc())
return {
'status':
'error',
'msg':
'There was something wrong, please contact administrator'
}
else:
return {'status': 'error', 'msg': 'This domain does not exist'}
def delete_dnssec_key(self, domain_name, key_id):
"""
Remove keys DNSSEC
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
headers = {'X-API-Key': self.PDNS_API_KEY}
try:
# Deactivate DNSSEC
jdata = utils.fetch_json(
urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}/cryptokeys/{1}'.format(
domain.name, key_id)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='DELETE',
verify=Setting().get('verify_ssl_connections'))
if jdata != True:
return {
'status':
'error',
'msg':
'Cannot disable DNSSEC for this domain. Error: {0}'.
format(jdata['error']),
'jdata':
jdata
}
# Disable API-RECTIFY for domain, AFTER deactivating DNSSEC
post_data = {"api_rectify": False}
jdata = utils.fetch_json(
urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain.name)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='PUT',
verify=Setting().get('verify_ssl_connections'),
data=post_data)
if 'error' in jdata:
return {
'status': 'error',
'msg':
'API-RECTIFY could not be disabled for this domain',
'jdata': jdata
}
return {'status': 'ok'}
except Exception as e:
current_app.logger.error(
'Cannot delete dnssec key. DETAIL: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
return {
'status': 'error',
'msg':
'There was something wrong, please contact administrator',
'domain': domain.name,
'id': key_id
}
else:
return {'status': 'error', 'msg': 'This domain does not exist'}
def assoc_account(self, account_id):
"""
Associate domain with a domain, specified by account id
"""
domain_name = self.name
# Sanity check - domain name
if domain_name == "":
return {'status': False, 'msg': 'No domain name specified'}
# read domain and check that it exists
domain = Domain.query.filter(Domain.name == domain_name).first()
if not domain:
return {'status': False, 'msg': 'Domain does not exist'}
headers = {'X-API-Key': self.PDNS_API_KEY}
account_name = Account().get_name_by_id(account_id)
post_data = {"account": account_name}
try:
jdata = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain_name)),
headers=headers,
timeout=int(
Setting().get('pdns_api_timeout')),
method='PUT',
verify=Setting().get('verify_ssl_connections'),
data=post_data)
if 'error' in jdata.keys():
current_app.logger.error(jdata['error'])
return {'status': 'error', 'msg': jdata['error']}
else:
self.update()
msg_str = 'Account changed for domain {0} successfully'
current_app.logger.info(msg_str.format(domain_name))
return {'status': 'ok', 'msg': 'account changed successfully'}
except Exception as e:
current_app.logger.debug(e)
current_app.logger.debug(traceback.format_exc())
msg_str = 'Cannot change account for domain {0}'
current_app.logger.error(msg_str.format(domain_name))
return {
'status': 'error',
'msg': 'Cannot change account for this domain.'
}
def get_account(self):
"""
Get current account associated with this domain
"""
domain = Domain.query.filter(Domain.name == self.name).first()
return domain.account
def is_valid_access(self, user_id):
"""
Check if the user is allowed to access this
domain name
"""
return db.session.query(Domain) \
.outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \
.outerjoin(Account, Domain.account_id == Account.id) \
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
.filter(
db.or_(
DomainUser.user_id == user_id,
AccountUser.user_id == user_id
)).filter(Domain.id == self.id).first()

View file

@ -1,37 +0,0 @@
import traceback
from flask import current_app
from .base import db
class DomainSetting(db.Model):
__tablename__ = 'domain_setting'
id = db.Column(db.Integer, primary_key=True)
domain_id = db.Column(db.Integer, db.ForeignKey('domain.id'))
domain = db.relationship('Domain', back_populates='settings')
setting = db.Column(db.String(255), nullable=False)
value = db.Column(db.String(255))
def __init__(self, id=None, setting=None, value=None):
self.id = id
self.setting = setting
self.value = value
def __repr__(self):
return '<DomainSetting {0} for {1}>'.format(setting, self.domain.name)
def __eq__(self, other):
return type(self) == type(other) and self.setting == other.setting
def set(self, value):
try:
self.value = value
db.session.commit()
return True
except Exception as e:
current_app.logger.error(
'Unable to set DomainSetting value. DETAIL: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
db.session.rollback()
return False

View file

@ -1,65 +0,0 @@
from flask import current_app
from .base import db
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:
current_app.logger.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:
current_app.logger.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:
current_app.logger.error(
'Can not delete domain template. Error: {0}'.format(e))
db.session.rollback()
return {'status': 'error', 'msg': 'Can not delete domain template'}

Some files were not shown because too many files have changed in this diff Show more