Refactoring the code

- Use Flask blueprint
- Split model and views into smaller parts
- Bug fixes
- API adjustment
This commit is contained in:
Khanh Ngo
2019-12-02 10:32:03 +07:00
parent 0b2eb0fbf8
commit 8ea00b9484
99 changed files with 15183 additions and 6386 deletions

125
powerdnsadmin/__init__.py Executable file
View File

@@ -0,0 +1,125 @@
import os
from werkzeug.contrib.fixers import ProxyFix
from flask import Flask
from flask_seasurf import SeaSurf
from flask_sslify import SSLify
from .lib import utils
# from flask_login import LoginManager
# from flask_sqlalchemy import SQLAlchemy as SA
# from flask_migrate import Migrate
# from authlib.flask.client import OAuth as AuthlibOAuth
# from sqlalchemy.exc import OperationalError
# from app.assets import assets
# ### SYBPATCH ###
# from app.customboxes import customBoxes
### SYBPATCH ###
# 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
# app = Flask(__name__)
# app.config.from_object('config')
# app.wsgi_app = ProxyFix(app.wsgi_app)
# csrf = SeaSurf(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
# authlib_oauth_client = AuthlibOAuth(app) # authlib 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.blueprints.api import api_blueprint
# app.register_blueprint(api_blueprint, url_prefix='/api/v1')
# from app import views
def create_app(config=None):
from . import models, routes, services
from .assets import assets
app = Flask(__name__)
# Proxy
app.wsgi_app = ProxyFix(app.wsgi_app)
# HSTS enabled
_sslify = SSLify(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)
# Load default configuration
app.config.from_object('powerdnsadmin.default_config')
# Load environment configuration
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)
# 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
# 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)
return app

74
powerdnsadmin/assets.py Normal file
View File

@@ -0,0 +1,74 @@
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',
'node_modules/datatables.net-plugins/sorting/natural.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)

252
powerdnsadmin/decorators.py Normal file
View File

@@ -0,0 +1,252 @@
import base64
import binascii
from functools import wraps
from flask import g, redirect, url_for, request, abort, current_app, render_template
from flask_login import current_user
from .models import User, ApiKey, Setting, Domain
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 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_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:
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
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 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_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):
@wraps(f)
def decorated_function(*args, **kwargs):
apikey = g.apikey
if g.apikey.role.name not in ['Administrator', 'Operator']:
domains = apikey.domains
zone_id = kwargs.get('zone_id')
domain_names = [item.name for item in domains]
if zone_id not in domain_names:
raise DomainAccessForbidden()
return f(*args, **kwargs)
return decorated_function
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

View File

@@ -0,0 +1,102 @@
import os
basedir = os.path.abspath(os.path.dirname(__file__))
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
# BASIC APP CONFIG
SECRET_KEY = 'We are the world'
BIND_ADDRESS = '0.0.0.0'
PORT = 9191
# TIMEOUT - for large zones
TIMEOUT = 10
# LOG CONFIG
# - For docker, LOG_FILE=''
LOG_LEVEL = 'DEBUG'
# 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

View File

@@ -0,0 +1,48 @@
from OpenSSL import crypto
from datetime import datetime
import pytz
import os
CRYPT_PATH = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/../../")
CERT_FILE = CRYPT_PATH + "/saml_cert.crt"
KEY_FILE = CRYPT_PATH + "/saml_cert.key"
def check_certificate():
if not os.path.isfile(CERT_FILE):
return False
st_cert = open(CERT_FILE, 'rt').read()
cert = crypto.load_certificate(crypto.FILETYPE_PEM, st_cert)
now = datetime.now(pytz.utc)
begin = datetime.strptime(cert.get_notBefore(), "%Y%m%d%H%M%SZ").replace(tzinfo=pytz.UTC)
begin_ok = begin < now
end = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ").replace(tzinfo=pytz.UTC)
end_ok = end > now
if begin_ok and end_ok:
return True
return False
def create_self_signed_cert():
# create a key pair
k = crypto.PKey()
k.generate_key(crypto.TYPE_RSA, 2048)
# create a self-signed cert
cert = crypto.X509()
cert.get_subject().C = "DE"
cert.get_subject().ST = "NRW"
cert.get_subject().L = "Dortmund"
cert.get_subject().O = "Dummy Company Ltd"
cert.get_subject().OU = "Dummy Company Ltd"
cert.get_subject().CN = "PowerDNS-Admin"
cert.set_serial_number(1000)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(10*365*24*60*60)
cert.set_issuer(cert.get_subject())
cert.set_pubkey(k)
cert.sign(k, 'sha256')
open(CERT_FILE, "wt").write(
crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
open(KEY_FILE, "wt").write(
crypto.dump_privatekey(crypto.FILETYPE_PEM, k))

View File

@@ -0,0 +1,82 @@
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 have \
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

View File

@@ -0,0 +1,39 @@
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.path)
resp = requests.request(request.method,
url,
headers=headers,
verify=verify,
json=data)
return resp

View File

@@ -0,0 +1,27 @@
from lima import fields, Schema
class DomainSchema(Schema):
id = fields.Integer()
name = fields.String()
class RoleSchema(Schema):
id = fields.Integer()
name = fields.String()
class ApiKeySchema(Schema):
id = fields.Integer()
role = fields.Embed(schema=RoleSchema)
domains = fields.Embed(schema=DomainSchema, 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)
description = fields.String()
plain_key = fields.String()

337
powerdnsadmin/lib/utils.py Normal file
View File

@@ -0,0 +1,337 @@
import re
import json
import requests
import hashlib
import ipaddress
import os
# 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
# import logging as logger
# logging = logger.getLogger(__name__)
# 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=retrieve_idp_data)
# background_thread.start()
# return idp_data
# def retrieve_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 retrieved from: " + app.config['SAML_METADATA_URL'])
# else:
# print("SAML: IDP Metadata could not be retrieved")
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)
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, 201, 204, 400, 409, 422):
r.raise_for_status()
except Exception as e:
msg = "Returned status {0} and content {1}"
raise RuntimeError('Error while fetching {0}'.format(remote_url))
return r
def fetch_json(remote_url, method='GET', data=None, params=None, headers=None, timeout=None):
r = fetch_remote(remote_url,
method=method,
data=data,
params=params,
headers=headers,
timeout=timeout,
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']
if os.path.isfile(CERT_FILE):
cert = open(CERT_FILE, "r").readlines()
settings['sp']['x509cert'] = "".join(cert)
if os.path.isfile(KEY_FILE):
key = open(KEY_FILE, "r").readlines()
settings['sp']['privateKey'] = "".join(key)
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'] = app.config.get(
'SAML_WANT_MESSAGE_SIGNED', 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"
def validate_ipaddress(address):
try:
ip = ipaddress.ip_address(address)
except ValueError:
pass
else:
if isinstance(ip, (ipaddress.IPv4Address, ipaddress.IPv6Address)):
return [ip]
return []
class customBoxes:
boxes = {
"reverse": (" ", " "),
"ip6arpa": ("ip6", "%.ip6.arpa"),
"inaddrarpa": ("in-addr", "%.in-addr.arpa")
}
order = ["reverse", "ip6arpa", "inaddrarpa"]

26
powerdnsadmin/models.py Normal file
View File

@@ -0,0 +1,26 @@
import sys
import os
import re
import ldap
import ldap.filter
import base64
import bcrypt
import itertools
import traceback
import pyotp
import dns.reversename
import dns.inet
import dns.name
import pytimeparse
import random
import string
from ast import literal_eval
from datetime import datetime
from urllib.parse import urljoin
from distutils.util import strtobool
from distutils.version import StrictVersion
from flask_login import AnonymousUserMixin
from app import db, app
from app.lib import utils
from app.lib.log import logging

View File

@@ -0,0 +1,23 @@
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 .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)

View File

@@ -0,0 +1,200 @@
from .base import db
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")
def __init__(self, name=None, description=None, contact=None, mail=None):
self.name = name
self.description = description
self.contact = contact
self.mail = mail
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):
"""
Delete an account
"""
# unassociate all users first
self.grant_privileges([])
try:
Account.query.filter(Account.name == self.name).delete()
db.session.commit()
return True
except Exception as e:
db.session.rollback()
logging.error(
'Cannot delete account {0} from DB. DETAIL: {1}'.format(
self.username, 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()
logging.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()
logging.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()
logging.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()
logging.error(
'Cannot revoke user privileges on account {0}. DETAIL: {1}'.
format(self.name, e))
return False

View File

@@ -0,0 +1,17 @@
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

@@ -0,0 +1,114 @@
import random
import string
import bcrypt
from flask import current_app
from .base import db, domain_apikey
from ..models.role import Role
from ..models.domain import Domain
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")
def __init__(self, key=None, desc=None, role_name=None, domains=[]):
self.id = None
self.description = desc
self.role_name = role_name
self.domains[:] = domains
if not key:
rand_key = ''.join(
random.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):
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:
domain_object_list = Domain.query \
.filter(Domain.name.in_(domains)) \
.all()
self.domains[:] = domain_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
return bcrypt.hashpw(pw.encode('utf-8'),
current_app.config.get('SALT').encode('utf-8'))
def check_password(self, hashed_password):
# Check hased 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

View File

@@ -0,0 +1,7 @@
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

@@ -0,0 +1,805 @@
import re
import traceback
from flask import current_app
from urllib.parse import urljoin
from distutils.util import strtobool
from distutils.version import StrictVersion
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.Integer)
notified_serial = db.Column(db.Integer)
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)
if StrictVersion(self.PDNS_VERSION) >= StrictVersion('4.0.0'):
self.NEW_SCHEMA = True
else:
self.NEW_SCHEMA = False
def __repr__(self):
return '<Domain {0}>'.format(self.name)
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 = {}
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')))
return jdata
def get_domains(self):
"""
Get all domains which has in PowerDNS
"""
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
jdata = utils.fetch_json(
urljoin(self.PDNS_STATS_URL,
self.API_EXTENDED_URL + '/servers/localhost/zones'),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')))
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 {} entrys in PowerDNS-Admin".format(
len(list_db_domain)))
headers = {}
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')))
list_jdomain = [d['name'].rstrip('.') for d in jdata]
current_app.logger.info(
"Found {} entrys 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)
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)
else:
# add new domain
self.add_domain_to_powerdns_admin(domain=data)
current_app.logger.info('Update domain finished')
return {
'status': 'ok',
'msg': 'Domain table has been updated successfully'
}
except Exception as e:
current_app.logger.error(
'Can not update domain table. Error: {0}'.format(e))
return {'status': 'error', 'msg': 'Can not update domain table'}
def update_pdns_admin_domain(self, domain, account_id, data):
# 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:
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("Rolledback 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 = {}
headers['X-API-Key'] = self.PDNS_API_KEY
if self.NEW_SCHEMA:
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',
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(
'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):
"""
Read Domain from PowerDNS and add into PDNS-Admin
"""
headers = {}
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')))
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('.')
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:
db.session.commit()
current_app.logger.info(
"Synched 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("Rolledback 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 doesnt exist.'}
headers = {}
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',
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 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 None == domain_id 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 = {}
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')
current_app.logger.info(
'Deleted domain successfully from PowerDNS-Entity: {0}'.format(
domain_name))
return {'status': 'ok', 'msg': 'Delete domain successfully'}
def delete_domain_from_pdnsadmin(self, domain_name):
# 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()
db.session.commit()
domain_setting = DomainSetting.query.filter(
DomainSetting.domain_id == domain.id)
if domain_setting:
domain_setting.delete()
db.session.commit()
domain.apikeys[:] = []
db.session.commit()
# then remove domain
Domain.query.filter(Domain.name == domain_name).delete()
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))
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()
print(traceback.format_exc())
current_app.logger.error(
'Cannot grant user privileges to domain {0}. DETAIL: {1}'.
format(self.name, e))
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 = {}
headers['X-API-Key'] = self.PDNS_API_KEY
try:
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')
return {
'status': 'ok',
'msg': 'Update from Master successfully'
}
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 doesnot exist'}
def get_domain_dnssec(self, domain_name):
"""
Get domain DNSSEC information
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
headers = {}
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')
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 doesnot exist'}
def enable_domain_dnssec(self, domain_name):
"""
Enable domain DNSSEC
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
headers = {}
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',
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',
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 = {}
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')
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',
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 doesnot 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 = {}
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',
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

@@ -0,0 +1,33 @@
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:
logging.error(
'Unable to set DomainSetting value. DETAIL: {0}'.format(e))
logging.debug(traceback.format_exc())
db.session.rollback()
return False

View File

@@ -0,0 +1,65 @@
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'}

View File

@@ -0,0 +1,42 @@
from .base import db
class DomainTemplateRecord(db.Model):
__tablename__ = "domain_template_record"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255))
type = db.Column(db.String(64))
ttl = db.Column(db.Integer)
data = db.Column(db.Text)
status = db.Column(db.Boolean)
template_id = db.Column(db.Integer, db.ForeignKey('domain_template.id'))
template = db.relationship('DomainTemplate', back_populates='records')
def __repr__(self):
return '<DomainTemplateRecord {0}>'.format(self.id)
def __init__(self,
id=None,
name=None,
type=None,
ttl=None,
data=None,
status=None):
self.id = id
self.name = name
self.type = type
self.ttl = ttl
self.data = data
self.status = status
def apply(self):
try:
db.session.commit()
except Exception as e:
logging.error(
'Can not update domain template table. Error: {0}'.format(e))
db.session.rollback()
return {
'status': 'error',
'msg': 'Can not update domain template table'
}

View File

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

View File

@@ -0,0 +1,48 @@
from flask import current_app
from datetime import datetime
from .base import db
class History(db.Model):
id = db.Column(db.Integer, primary_key=True)
msg = db.Column(db.String(256))
# detail = db.Column(db.Text().with_variant(db.Text(length=2**24-2), 'mysql'))
detail = db.Column(db.Text())
created_by = db.Column(db.String(128))
created_on = db.Column(db.DateTime, default=datetime.utcnow)
def __init__(self, id=None, msg=None, detail=None, created_by=None):
self.id = id
self.msg = msg
self.detail = detail
self.created_by = created_by
def __repr__(self):
return '<History {0}>'.format(self.msg)
def add(self):
"""
Add an event to history table
"""
h = History()
h.msg = self.msg
h.detail = self.detail
h.created_by = self.created_by
db.session.add(h)
db.session.commit()
def remove_all(self):
"""
Remove all history from DB
"""
try:
db.session.query(History).delete()
db.session.commit()
current_app.logger.info("Removed all history")
return True
except Exception as e:
db.session.rollback()
current_app.logger.error("Cannot remove history. DETAIL: {0}".format(e))
current_app.logger.debug(traceback.format_exc())
return False

View File

@@ -0,0 +1,607 @@
import traceback
import itertools
import dns.reversename
import dns.inet
import dns.name
from distutils.version import StrictVersion
from flask import current_app
from urllib.parse import urljoin
from distutils.util import strtobool
from .. import utils
from .base import db
from .setting import Setting
from .domain import Domain
from .domain_setting import DomainSetting
class Record(object):
"""
This is not a model, it's just an object
which be assigned data from PowerDNS API
"""
def __init__(self, name=None, type=None, status=None, ttl=None, data=None):
self.name = name
self.type = type
self.status = status
self.ttl = ttl
self.data = data
# PDNS configs
self.PDNS_STATS_URL = Setting().get('pdns_api_url')
self.PDNS_API_KEY = Setting().get('pdns_api_key')
self.PDNS_VERSION = Setting().get('pdns_version')
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
self.PRETTY_IPV6_PTR = Setting().get('pretty_ipv6_ptr')
if StrictVersion(self.PDNS_VERSION) >= StrictVersion('4.0.0'):
self.NEW_SCHEMA = True
else:
self.NEW_SCHEMA = False
def get_record_data(self, domain):
"""
Query domain's DNS records via API
"""
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
try:
jdata = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain)),
timeout=int(Setting().get('pdns_api_timeout')),
headers=headers)
except Exception as e:
current_app.logger.error(
"Cannot fetch domain's record data from remote powerdns api. DETAIL: {0}"
.format(e))
return False
if self.NEW_SCHEMA:
rrsets = jdata['rrsets']
for rrset in rrsets:
r_name = rrset['name'].rstrip('.')
if self.PRETTY_IPV6_PTR: # only if activated
if rrset['type'] == 'PTR': # only ptr
if 'ip6.arpa' in r_name: # only if v6-ptr
r_name = dns.reversename.to_address(
dns.name.from_text(r_name))
rrset['name'] = r_name
rrset['content'] = rrset['records'][0]['content']
rrset['disabled'] = rrset['records'][0]['disabled']
return {'records': rrsets}
return jdata
def add(self, domain):
"""
Add a record to domain
"""
# validate record first
r = self.get_record_data(domain)
records = r['records']
check = list(filter(lambda check: check['name'] == self.name, records))
if check:
r = check[0]
if r['type'] in ('A', 'AAAA', 'CNAME'):
return {
'status': 'error',
'msg':
'Record already exists with type "A", "AAAA" or "CNAME"'
}
# continue if the record is ready to be added
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
if self.NEW_SCHEMA:
data = {
"rrsets": [{
"name":
self.name.rstrip('.') + '.',
"type":
self.type,
"changetype":
"REPLACE",
"ttl":
self.ttl,
"records": [{
"content": self.data,
"disabled": self.status,
}]
}]
}
else:
data = {
"rrsets": [{
"name":
self.name,
"type":
self.type,
"changetype":
"REPLACE",
"records": [{
"content": self.data,
"disabled": self.status,
"name": self.name,
"ttl": self.ttl,
"type": self.type
}]
}]
}
try:
jdata = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='PATCH',
data=data)
current_app.logger.debug(jdata)
return {'status': 'ok', 'msg': 'Record was added successfully'}
except Exception as e:
current_app.logger.error(
"Cannot add record {0}/{1}/{2} to domain {3}. DETAIL: {4}".
format(self.name, self.type, self.data, domain, e))
return {
'status': 'error',
'msg':
'There was something wrong, please contact administrator'
}
def compare(self, domain_name, new_records):
"""
Compare new records with current powerdns record data
Input is a list of hashes (records)
"""
# get list of current records we have in powerdns
current_records = self.get_record_data(domain_name)['records']
# convert them to list of list (just has [name, type]) instead of list of hash
# to compare easier
list_current_records = [[x['name'], x['type']]
for x in current_records]
list_new_records = [[x['name'], x['type']] for x in new_records]
# get list of deleted records
# they are the records which exist in list_current_records but not in list_new_records
list_deleted_records = [
x for x in list_current_records if x not in list_new_records
]
# convert back to list of hash
deleted_records = [
x for x in current_records
if [x['name'], x['type']] in list_deleted_records and (
x['type'] in Setting().get_records_allow_to_edit()
and x['type'] != 'SOA')
]
# return a tuple
return deleted_records, new_records
def apply(self, domain, post_records):
"""
Apply record changes to domain
"""
records = []
for r in post_records:
r_name = domain if r['record_name'] in [
'@', ''
] else r['record_name'] + '.' + domain
r_type = r['record_type']
if self.PRETTY_IPV6_PTR: # only if activated
if self.NEW_SCHEMA: # only if new schema
if r_type == 'PTR': # only ptr
if ':' in r['record_name']: # dirty ipv6 check
r_name = r['record_name']
r_data = domain if r_type == 'CNAME' and r['record_data'] in [
'@', ''
] else r['record_data']
record = {
"name": r_name,
"type": r_type,
"content": r_data,
"disabled":
True if r['record_status'] == 'Disabled' else False,
"ttl": int(r['record_ttl']) if r['record_ttl'] else 3600,
}
records.append(record)
deleted_records, new_records = self.compare(domain, records)
records = []
for r in deleted_records:
r_name = r['name'].rstrip(
'.') + '.' if self.NEW_SCHEMA else r['name']
r_type = r['type']
if self.PRETTY_IPV6_PTR: # only if activated
if self.NEW_SCHEMA: # only if new schema
if r_type == 'PTR': # only ptr
if ':' in r['name']: # dirty ipv6 check
r_name = dns.reversename.from_address(
r['name']).to_text()
record = {
"name": r_name,
"type": r_type,
"changetype": "DELETE",
"records": []
}
records.append(record)
postdata_for_delete = {"rrsets": records}
records = []
for r in new_records:
if self.NEW_SCHEMA:
r_name = r['name'].rstrip('.') + '.'
r_type = r['type']
if self.PRETTY_IPV6_PTR: # only if activated
if r_type == 'PTR': # only ptr
if ':' in r['name']: # dirty ipv6 check
r_name = r['name']
record = {
"name":
r_name,
"type":
r_type,
"changetype":
"REPLACE",
"ttl":
r['ttl'],
"records": [{
"content": r['content'],
"disabled": r['disabled'],
}]
}
else:
record = {
"name":
r['name'],
"type":
r['type'],
"changetype":
"REPLACE",
"records": [{
"content": r['content'],
"disabled": r['disabled'],
"name": r['name'],
"ttl": r['ttl'],
"type": r['type'],
"priority":
10, # priority field for pdns 3.4.1. https://doc.powerdns.com/md/authoritative/upgrading/
}]
}
records.append(record)
# Adjustment to add multiple records which described in
# https://github.com/ngoduykhanh/PowerDNS-Admin/issues/5#issuecomment-181637576
final_records = []
records = sorted(records,
key=lambda item:
(item["name"], item["type"], item["changetype"]))
for key, group in itertools.groupby(
records, lambda item:
(item["name"], item["type"], item["changetype"])):
if self.NEW_SCHEMA:
r_name = key[0]
r_type = key[1]
r_changetype = key[2]
if self.PRETTY_IPV6_PTR: # only if activated
if r_type == 'PTR': # only ptr
if ':' in r_name: # dirty ipv6 check
r_name = dns.reversename.from_address(
r_name).to_text()
new_record = {
"name": r_name,
"type": r_type,
"changetype": r_changetype,
"ttl": None,
"records": []
}
for item in group:
temp_content = item['records'][0]['content']
temp_disabled = item['records'][0]['disabled']
if key[1] in ['MX', 'CNAME', 'SRV', 'NS']:
if temp_content.strip()[-1:] != '.':
temp_content += '.'
if new_record['ttl'] is None:
new_record['ttl'] = item['ttl']
new_record['records'].append({
"content": temp_content,
"disabled": temp_disabled
})
final_records.append(new_record)
else:
final_records.append({
"name":
key[0],
"type":
key[1],
"changetype":
key[2],
"records": [{
"content": item['records'][0]['content'],
"disabled": item['records'][0]['disabled'],
"name": key[0],
"ttl": item['records'][0]['ttl'],
"type": key[1],
"priority": 10,
} for item in group]
})
postdata_for_new = {"rrsets": final_records}
current_app.logger.debug(postdata_for_new)
current_app.logger.debug(postdata_for_delete)
current_app.logger.info(
urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain)))
try:
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
jdata1 = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain)),
headers=headers,
method='PATCH',
data=postdata_for_delete)
jdata2 = utils.fetch_json(
urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='PATCH',
data=postdata_for_new)
if 'error' in jdata1.keys():
current_app.logger.error('Cannot apply record changes.')
current_app.logger.debug(jdata1['error'])
return {'status': 'error', 'msg': jdata1['error']}
elif 'error' in jdata2.keys():
current_app.logger.error('Cannot apply record changes.')
current_app.logger.debug(jdata2['error'])
return {'status': 'error', 'msg': jdata2['error']}
else:
self.auto_ptr(domain, new_records, deleted_records)
self.update_db_serial(domain)
current_app.logger.info('Record was applied successfully.')
return {
'status': 'ok',
'msg': 'Record was applied successfully'
}
except Exception as e:
current_app.logger.error(
"Cannot apply record changes to domain {0}. Error: {1}".format(
domain, e))
current_app.logger.debug(traceback.format_exc())
return {
'status': 'error',
'msg':
'There was something wrong, please contact administrator'
}
def auto_ptr(self, domain, new_records, deleted_records):
"""
Add auto-ptr records
"""
domain_obj = Domain.query.filter(Domain.name == domain).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')
if system_auto_ptr or domain_auto_ptr:
try:
d = Domain()
for r in new_records:
if r['type'] in ['A', 'AAAA']:
r_name = r['name'] + '.'
r_content = r['content']
reverse_host_address = dns.reversename.from_address(
r_content).to_text()
domain_reverse_name = d.get_reverse_domain_name(
reverse_host_address)
d.create_reverse_domain(domain, domain_reverse_name)
self.name = dns.reversename.from_address(
r_content).to_text().rstrip('.')
self.type = 'PTR'
self.status = r['disabled']
self.ttl = r['ttl']
self.data = r_name
self.add(domain_reverse_name)
for r in deleted_records:
if r['type'] in ['A', 'AAAA']:
r_content = r['content']
reverse_host_address = dns.reversename.from_address(
r_content).to_text()
domain_reverse_name = d.get_reverse_domain_name(
reverse_host_address)
self.name = reverse_host_address
self.type = 'PTR'
self.data = r_content
self.delete(domain_reverse_name)
return {
'status': 'ok',
'msg': 'Auto-PTR record was updated successfully'
}
except Exception as e:
current_app.logger.error(
"Cannot update auto-ptr record changes to domain {0}. Error: {1}"
.format(domain, e))
current_app.logger.debug(traceback.format_exc())
return {
'status':
'error',
'msg':
'Auto-PTR creation failed. There was something wrong, please contact administrator.'
}
def delete(self, domain):
"""
Delete a record from domain
"""
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
data = {
"rrsets": [{
"name": self.name.rstrip('.') + '.',
"type": self.type,
"changetype": "DELETE",
"records": []
}]
}
try:
jdata = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='PATCH',
data=data)
current_app.logger.debug(jdata)
return {'status': 'ok', 'msg': 'Record was removed successfully'}
except Exception as e:
current_app.logger.error(
"Cannot remove record {0}/{1}/{2} from domain {3}. DETAIL: {4}"
.format(self.name, self.type, self.data, domain, e))
return {
'status': 'error',
'msg':
'There was something wrong, please contact administrator'
}
def is_allowed_edit(self):
"""
Check if record is allowed to edit
"""
return self.type in Setting().get_records_allow_to_edit()
def is_allowed_delete(self):
"""
Check if record is allowed to removed
"""
return (self.type in Setting().get_records_allow_to_edit()
and self.type != 'SOA')
def exists(self, domain):
"""
Check if record is present within domain records, and if it's present set self to found record
"""
jdata = self.get_record_data(domain)
jrecords = jdata['records']
for jr in jrecords:
if jr['name'] == self.name and jr['type'] == self.type:
self.name = jr['name']
self.type = jr['type']
self.status = jr['disabled']
self.ttl = jr['ttl']
self.data = jr['content']
self.priority = 10
return True
return False
def update(self, domain, content):
"""
Update single record
"""
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
if self.NEW_SCHEMA:
data = {
"rrsets": [{
"name":
self.name + '.',
"type":
self.type,
"ttl":
self.ttl,
"changetype":
"REPLACE",
"records": [{
"content": content,
"disabled": self.status,
}]
}]
}
else:
data = {
"rrsets": [{
"name":
self.name,
"type":
self.type,
"changetype":
"REPLACE",
"records": [{
"content": content,
"disabled": self.status,
"name": self.name,
"ttl": self.ttl,
"type": self.type,
"priority": 10
}]
}]
}
try:
utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='PATCH',
data=data)
current_app.logger.debug("dyndns data: {0}".format(data))
return {'status': 'ok', 'msg': 'Record was updated successfully'}
except Exception as e:
current_app.logger.error(
"Cannot add record {0}/{1}/{2} to domain {3}. DETAIL: {4}".
format(self.name, self.type, self.data, domain, e))
return {
'status': 'error',
'msg':
'There was something wrong, please contact administrator'
}
def update_db_serial(self, domain):
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
jdata = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='GET')
serial = jdata['serial']
domain = Domain.query.filter(Domain.name == domain).first()
if domain:
domain.serial = serial
db.session.commit()
return {
'status': True,
'msg': 'Synced local serial for domain name {0}'.format(domain)
}
else:
return {
'status': False,
'msg':
'Could not find domain name {0} in local db'.format(domain)
}

View File

@@ -0,0 +1,25 @@
class RecordEntry(object):
"""
This is not a model, it's just an object
which will store records entries from PowerDNS API
"""
def __init__(self,
name=None,
type=None,
status=None,
ttl=None,
data=None,
is_allowed_edit=False):
self.name = name
self.type = type
self.status = status
self.ttl = ttl
self.data = data
self._is_allowed_edit = is_allowed_edit
self._is_allowed_delete = is_allowed_edit and self.type != 'SOA'
def is_allowed_edit(self):
return self._is_allowed_edit
def is_allowed_delete(self):
return self._is_allowed_delete

View File

@@ -0,0 +1,23 @@
from .base import db
class Role(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), index=True, unique=True)
description = db.Column(db.String(128))
users = db.relationship('User', backref='role', lazy=True)
apikeys = db.relationship('ApiKey', back_populates='role', lazy=True)
def __init__(self, id=None, name=None, description=None):
self.id = id
self.name = name
self.description = description
# allow database autoincrement to do its own ID assignments
def __init__(self, name=None, description=None):
self.id = None
self.name = name
self.description = description
def __repr__(self):
return '<Role {0}r>'.format(self.name)

View File

@@ -0,0 +1,64 @@
import traceback
from flask import current_app
from urllib.parse import urljoin
from ..lib import utils
from .base import db
from .setting import Setting
class Server(object):
"""
This is not a model, it's just an object
which be assigned data from PowerDNS API
"""
def __init__(self, server_id=None, server_config=None):
self.server_id = server_id
self.server_config = server_config
# PDNS configs
self.PDNS_STATS_URL = Setting().get('pdns_api_url')
self.PDNS_API_KEY = Setting().get('pdns_api_key')
self.PDNS_VERSION = Setting().get('pdns_version')
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
def get_config(self):
"""
Get server config
"""
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
try:
jdata = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/{0}/config'.format(self.server_id)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='GET')
return jdata
except Exception as e:
current_app.logger.error(
"Can not get server configuration. DETAIL: {0}".format(e))
current_app.logger.debug(traceback.format_exc())
return []
def get_statistic(self):
"""
Get server statistics
"""
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
try:
jdata = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/{0}/statistics'.format(self.server_id)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='GET')
return jdata
except Exception as e:
current_app.logger.error(
"Can not get server statistics. DETAIL: {0}".format(e))
current_app.logger.debug(traceback.format_exc())
return []

View File

@@ -0,0 +1,268 @@
import sys
import pytimeparse
from ast import literal_eval
from distutils.util import strtobool
from distutils.version import StrictVersion
from .base import db
class Setting(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64))
value = db.Column(db.Text())
defaults = {
'maintenance': False,
'fullscreen_layout': True,
'record_helper': True,
'login_ldap_first': True,
'default_record_table_size': 15,
'default_domain_table_size': 10,
'auto_ptr': False,
'record_quick_edit': True,
'pretty_ipv6_ptr': False,
'dnssec_admins_only': False,
'allow_user_create_domain': False,
'bg_domain_updates': False,
'site_name': 'PowerDNS-Admin',
'session_timeout': 10,
'pdns_api_url': '',
'pdns_api_key': '',
'pdns_api_timeout': 30,
'pdns_version': '4.1.1',
'local_db_enabled': True,
'signup_enabled': True,
'ldap_enabled': False,
'ldap_type': 'ldap',
'ldap_uri': '',
'ldap_base_dn': '',
'ldap_admin_username': '',
'ldap_admin_password': '',
'ldap_filter_basic': '',
'ldap_filter_username': '',
'ldap_sg_enabled': False,
'ldap_admin_group': '',
'ldap_operator_group': '',
'ldap_user_group': '',
'ldap_domain': '',
'github_oauth_enabled': False,
'github_oauth_key': '',
'github_oauth_secret': '',
'github_oauth_scope': 'email',
'github_oauth_api_url': 'https://api.github.com/user',
'github_oauth_token_url':
'https://github.com/login/oauth/access_token',
'github_oauth_authorize_url':
'https://github.com/login/oauth/authorize',
'google_oauth_enabled': False,
'google_oauth_client_id': '',
'google_oauth_client_secret': '',
'google_token_url': 'https://oauth2.googleapis.com/token',
'google_oauth_scope': 'openid email profile',
'google_authorize_url': 'https://accounts.google.com/o/oauth2/v2/auth',
'google_base_url': 'https://www.googleapis.com/oauth2/v3/',
'oidc_oauth_enabled': False,
'oidc_oauth_key': '',
'oidc_oauth_secret': '',
'oidc_oauth_scope': 'email',
'oidc_oauth_api_url': '',
'oidc_oauth_token_url': '',
'oidc_oauth_authorize_url': '',
'forward_records_allow_edit': {
'A': True,
'AAAA': True,
'AFSDB': False,
'ALIAS': False,
'CAA': True,
'CERT': False,
'CDNSKEY': False,
'CDS': False,
'CNAME': True,
'DNSKEY': False,
'DNAME': False,
'DS': False,
'HINFO': False,
'KEY': False,
'LOC': True,
'LUA': False,
'MX': True,
'NAPTR': False,
'NS': True,
'NSEC': False,
'NSEC3': False,
'NSEC3PARAM': False,
'OPENPGPKEY': False,
'PTR': True,
'RP': False,
'RRSIG': False,
'SOA': False,
'SPF': True,
'SSHFP': False,
'SRV': True,
'TKEY': False,
'TSIG': False,
'TLSA': False,
'SMIMEA': False,
'TXT': True,
'URI': False
},
'reverse_records_allow_edit': {
'A': False,
'AAAA': False,
'AFSDB': False,
'ALIAS': False,
'CAA': False,
'CERT': False,
'CDNSKEY': False,
'CDS': False,
'CNAME': False,
'DNSKEY': False,
'DNAME': False,
'DS': False,
'HINFO': False,
'KEY': False,
'LOC': True,
'LUA': False,
'MX': False,
'NAPTR': False,
'NS': True,
'NSEC': False,
'NSEC3': False,
'NSEC3PARAM': False,
'OPENPGPKEY': False,
'PTR': True,
'RP': False,
'RRSIG': False,
'SOA': False,
'SPF': False,
'SSHFP': False,
'SRV': False,
'TKEY': False,
'TSIG': False,
'TLSA': False,
'SMIMEA': False,
'TXT': True,
'URI': False
},
'ttl_options': '1 minute,5 minutes,30 minutes,60 minutes,24 hours',
}
def __init__(self, id=None, name=None, value=None):
self.id = id
self.name = name
self.value = value
# allow database autoincrement to do its own ID assignments
def __init__(self, name=None, value=None):
self.id = None
self.name = name
self.value = value
def set_maintenance(self, mode):
maintenance = Setting.query.filter(
Setting.name == 'maintenance').first()
if maintenance is None:
value = self.defaults['maintenance']
maintenance = Setting(name='maintenance', value=str(value))
db.session.add(maintenance)
mode = str(mode)
try:
if maintenance.value != mode:
maintenance.value = mode
db.session.commit()
return True
except Exception as e:
logging.error('Cannot set maintenance to {0}. DETAIL: {1}'.format(
mode, e))
logging.debug(traceback.format_exec())
db.session.rollback()
return False
def toggle(self, setting):
current_setting = Setting.query.filter(Setting.name == setting).first()
if current_setting is None:
value = self.defaults[setting]
current_setting = Setting(name=setting, value=str(value))
db.session.add(current_setting)
try:
if current_setting.value == "True":
current_setting.value = "False"
else:
current_setting.value = "True"
db.session.commit()
return True
except Exception as e:
logging.error('Cannot toggle setting {0}. DETAIL: {1}'.format(
setting, e))
logging.debug(traceback.format_exec())
db.session.rollback()
return False
def set(self, setting, value):
current_setting = Setting.query.filter(Setting.name == setting).first()
if current_setting is None:
current_setting = Setting(name=setting, value=None)
db.session.add(current_setting)
value = str(value)
try:
current_setting.value = value
db.session.commit()
return True
except Exception as e:
logging.error('Cannot edit setting {0}. DETAIL: {1}'.format(
setting, e))
logging.debug(traceback.format_exec())
db.session.rollback()
return False
def get(self, setting):
if setting in self.defaults:
result = self.query.filter(Setting.name == setting).first()
if result is not None:
return strtobool(result.value) if result.value in [
'True', 'False'
] else result.value
else:
return self.defaults[setting]
else:
logging.error('Unknown setting queried: {0}'.format(setting))
def get_records_allow_to_edit(self):
return list(
set(self.get_forward_records_allow_to_edit() +
self.get_reverse_records_allow_to_edit()))
def get_forward_records_allow_to_edit(self):
records = self.get('forward_records_allow_edit')
f_records = literal_eval(records) if isinstance(records,
str) else records
r_name = [r for r in f_records if f_records[r]]
# Sort alphabetically if python version is smaller than 3.6
if sys.version_info[0] < 3 or (sys.version_info[0] == 3
and sys.version_info[1] < 6):
r_name.sort()
return r_name
def get_reverse_records_allow_to_edit(self):
records = self.get('reverse_records_allow_edit')
r_records = literal_eval(records) if isinstance(records,
str) else records
r_name = [r for r in r_records if r_records[r]]
# Sort alphabetically if python version is smaller than 3.6
if sys.version_info[0] < 3 or (sys.version_info[0] == 3
and sys.version_info[1] < 6):
r_name.sort()
return r_name
def get_ttl_options(self):
return [(pytimeparse.parse(ttl), ttl)
for ttl in self.get('ttl_options').split(',')]

View File

@@ -0,0 +1,581 @@
import os
import base64
import bcrypt
import ldap
import ldap.filter
from flask import current_app
from flask_login import AnonymousUserMixin
from .base import db
from .role import Role
from .domain_user import DomainUser
class Anonymous(AnonymousUserMixin):
def __init__(self):
self.username = 'Anonymous'
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
password = db.Column(db.String(64))
firstname = db.Column(db.String(64))
lastname = db.Column(db.String(64))
email = db.Column(db.String(128))
otp_secret = db.Column(db.String(16))
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
def __init__(self,
id=None,
username=None,
password=None,
plain_text_password=None,
firstname=None,
lastname=None,
role_id=None,
email=None,
otp_secret=None,
reload_info=True):
self.id = id
self.username = username
self.password = password
self.plain_text_password = plain_text_password
self.firstname = firstname
self.lastname = lastname
self.role_id = role_id
self.email = email
self.otp_secret = otp_secret
if reload_info:
user_info = self.get_user_info_by_id(
) if id else self.get_user_info_by_username()
if user_info:
self.id = user_info.id
self.username = user_info.username
self.firstname = user_info.firstname
self.lastname = user_info.lastname
self.email = user_info.email
self.role_id = user_info.role_id
self.otp_secret = user_info.otp_secret
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
try:
return unicode(self.id) # python 2
except NameError:
return str(self.id) # python 3
def __repr__(self):
return '<User {0}>'.format(self.username)
def get_totp_uri(self):
return "otpauth://totp/PowerDNS-Admin:{0}?secret={1}&issuer=PowerDNS-Admin".format(
self.username, self.otp_secret)
def verify_totp(self, token):
totp = pyotp.TOTP(self.otp_secret)
return totp.verify(token)
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
pw = plain_text_password if plain_text_password else self.plain_text_password
return bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt())
def check_password(self, hashed_password):
# Check hased 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 get_user_info_by_id(self):
user_info = User.query.get(int(self.id))
return user_info
def get_user_info_by_username(self):
user_info = User.query.filter(User.username == self.username).first()
return user_info
def ldap_init_conn(self):
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
conn = ldap.initialize(Setting().get('ldap_uri'))
conn.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
conn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND)
conn.set_option(ldap.OPT_X_TLS_DEMAND, True)
conn.set_option(ldap.OPT_DEBUG_LEVEL, 255)
conn.protocol_version = ldap.VERSION3
return conn
def ldap_search(self, searchFilter, baseDN):
searchScope = ldap.SCOPE_SUBTREE
retrieveAttributes = None
try:
conn = self.ldap_init_conn()
if Setting().get('ldap_type') == 'ad':
conn.simple_bind_s(
"{0}@{1}".format(self.username,
Setting().get('ldap_domain')),
self.password)
else:
conn.simple_bind_s(Setting().get('ldap_admin_username'),
Setting().get('ldap_admin_password'))
ldap_result_id = conn.search(baseDN, searchScope, searchFilter,
retrieveAttributes)
result_set = []
while 1:
result_type, result_data = conn.result(ldap_result_id, 0)
if (result_data == []):
break
else:
if result_type == ldap.RES_SEARCH_ENTRY:
result_set.append(result_data)
return result_set
except ldap.LDAPError as e:
current_app.logger.error(e)
current_app.logger.debug('baseDN: {0}'.format(baseDN))
current_app.logger.debug(traceback.format_exc())
def ldap_auth(self, ldap_username, password):
try:
conn = self.ldap_init_conn()
conn.simple_bind_s(ldap_username, password)
return True
except ldap.LDAPError as e:
current_app.logger.error(e)
return False
def ad_recursive_groups(self, groupDN):
"""
Recursively list groups belonging to a group. It will allow checking deep in the Active Directory
whether a user is allowed to enter or not
"""
LDAP_BASE_DN = Setting().get('ldap_base_dn')
groupSearchFilter = "(&(objectcategory=group)(member=%s))" % ldap.filter.escape_filter_chars(
groupDN)
result = [groupDN]
try:
groups = self.ldap_search(groupSearchFilter, LDAP_BASE_DN)
for group in groups:
result += [group[0][0]]
if 'memberOf' in group[0][1]:
for member in group[0][1]['memberOf']:
result += self.ad_recursive_groups(
member.decode("utf-8"))
return result
except ldap.LDAPError as e:
current_app.logger.exception("Recursive AD Group search error")
return result
def is_validate(self, method, src_ip=''):
"""
Validate user credential
"""
role_name = 'User'
if method == 'LOCAL':
user_info = User.query.filter(
User.username == self.username).first()
if user_info:
if user_info.password and self.check_password(
user_info.password):
current_app.logger.info(
'User "{0}" logged in successfully. Authentication request from {1}'
.format(self.username, src_ip))
return True
current_app.logger.error(
'User "{0}" inputted a wrong password. Authentication request from {1}'
.format(self.username, src_ip))
return False
current_app.logger.warning(
'User "{0}" does not exist. Authentication request from {1}'.
format(self.username, src_ip))
return False
if method == 'LDAP':
LDAP_TYPE = Setting().get('ldap_type')
LDAP_BASE_DN = Setting().get('ldap_base_dn')
LDAP_FILTER_BASIC = Setting().get('ldap_filter_basic')
LDAP_FILTER_USERNAME = Setting().get('ldap_filter_username')
LDAP_ADMIN_GROUP = Setting().get('ldap_admin_group')
LDAP_OPERATOR_GROUP = Setting().get('ldap_operator_group')
LDAP_USER_GROUP = Setting().get('ldap_user_group')
LDAP_GROUP_SECURITY_ENABLED = Setting().get('ldap_sg_enabled')
# validate AD user password
if Setting().get('ldap_type') == 'ad':
ldap_username = "{0}@{1}".format(self.username,
Setting().get('ldap_domain'))
if not self.ldap_auth(ldap_username, self.password):
current_app.logger.error(
'User "{0}" input a wrong LDAP password. Authentication request from {1}'
.format(self.username, src_ip))
return False
searchFilter = "(&({0}={1}){2})".format(LDAP_FILTER_USERNAME,
self.username,
LDAP_FILTER_BASIC)
current_app.logger.debug('Ldap searchFilter {0}'.format(searchFilter))
ldap_result = self.ldap_search(searchFilter, LDAP_BASE_DN)
current_app.logger.debug('Ldap search result: {0}'.format(ldap_result))
if not ldap_result:
current_app.logger.warning(
'LDAP User "{0}" does not exist. Authentication request from {1}'
.format(self.username, src_ip))
return False
else:
try:
ldap_username = ldap.filter.escape_filter_chars(
ldap_result[0][0][0])
if Setting().get('ldap_type') != 'ad':
# validate ldap user password
if not self.ldap_auth(ldap_username, self.password):
current_app.logger.error(
'User "{0}" input a wrong LDAP password. Authentication request from {1}'
.format(self.username, src_ip))
return False
# check if LDAP_GROUP_SECURITY_ENABLED is True
# user can be assigned to ADMIN or USER role.
if LDAP_GROUP_SECURITY_ENABLED:
try:
if LDAP_TYPE == 'ldap':
if (self.ldap_search(searchFilter,
LDAP_ADMIN_GROUP)):
role_name = 'Administrator'
current_app.logger.info(
'User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'
.format(self.username,
LDAP_ADMIN_GROUP))
elif (self.ldap_search(searchFilter,
LDAP_OPERATOR_GROUP)):
role_name = 'Operator'
current_app.logger.info(
'User {0} is part of the "{1}" group that allows operator access to PowerDNS-Admin'
.format(self.username,
LDAP_OPERATOR_GROUP))
elif (self.ldap_search(searchFilter,
LDAP_USER_GROUP)):
current_app.logger.info(
'User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'
.format(self.username,
LDAP_USER_GROUP))
else:
current_app.logger.error(
'User {0} is not part of the "{1}", "{2}" or "{3}" groups that allow access to PowerDNS-Admin'
.format(self.username,
LDAP_ADMIN_GROUP,
LDAP_OPERATOR_GROUP,
LDAP_USER_GROUP))
return False
elif LDAP_TYPE == 'ad':
user_ldap_groups = []
user_ad_member_of = ldap_result[0][0][1].get(
'memberOf')
if not user_ad_member_of:
current_app.logger.error(
'User {0} does not belong to any group while LDAP_GROUP_SECURITY_ENABLED is ON'
.format(self.username))
return False
for group in [
g.decode("utf-8")
for g in user_ad_member_of
]:
user_ldap_groups += self.ad_recursive_groups(
group)
if (LDAP_ADMIN_GROUP in user_ldap_groups):
role_name = 'Administrator'
current_app.logger.info(
'User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'
.format(self.username,
LDAP_ADMIN_GROUP))
elif (LDAP_OPERATOR_GROUP in user_ldap_groups):
role_name = 'Operator'
current_app.logger.info(
'User {0} is part of the "{1}" group that allows operator access to PowerDNS-Admin'
.format(self.username,
LDAP_OPERATOR_GROUP))
elif (LDAP_USER_GROUP in user_ldap_groups):
current_app.logger.info(
'User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'
.format(self.username,
LDAP_USER_GROUP))
else:
current_app.logger.error(
'User {0} is not part of the "{1}", "{2}" or "{3}" groups that allow access to PowerDNS-Admin'
.format(self.username,
LDAP_ADMIN_GROUP,
LDAP_OPERATOR_GROUP,
LDAP_USER_GROUP))
return False
else:
current_app.logger.error('Invalid LDAP type')
return False
except Exception as e:
current_app.logger.error(
'LDAP group lookup for user "{0}" has failed. Authentication request from {1}'
.format(self.username, src_ip))
current_app.logger.debug(traceback.format_exc())
return False
except Exception as e:
current_app.logger.error('Wrong LDAP configuration. {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
return False
# create user if not exist in the db
if not User.query.filter(User.username == self.username).first():
self.firstname = self.username
self.lastname = ''
try:
# try to get user's firstname, lastname and email address from LDAP attributes
if LDAP_TYPE == 'ldap':
self.firstname = ldap_result[0][0][1]['givenName'][
0].decode("utf-8")
self.lastname = ldap_result[0][0][1]['sn'][0].decode(
"utf-8")
self.email = ldap_result[0][0][1]['mail'][0].decode(
"utf-8")
elif LDAP_TYPE == 'ad':
self.firstname = ldap_result[0][0][1]['name'][
0].decode("utf-8")
self.email = ldap_result[0][0][1]['userPrincipalName'][
0].decode("utf-8")
except Exception as e:
current_app.logger.warning(
"Reading ldap data threw an exception {0}".format(e))
current_app.logger.debug(traceback.format_exc())
# first register user will be in Administrator role
if User.query.count() == 0:
self.role_id = Role.query.filter_by(
name='Administrator').first().id
else:
self.role_id = Role.query.filter_by(
name=role_name).first().id
self.create_user()
current_app.logger.info('Created user "{0}" in the DB'.format(
self.username))
# user already exists in database, set their role based on group membership (if enabled)
if LDAP_GROUP_SECURITY_ENABLED:
self.set_role(role_name)
return True
else:
current_app.logger.error('Unsupported authentication method')
return False
# def get_apikeys(self, domain_name=None):
# info = []
# apikey_query = db.session.query(ApiKey) \
# .join(Domain.apikeys) \
# .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(User.id == self.id)
# if domain_name:
# info = apikey_query.filter(Domain.name == domain_name).all()
# else:
# info = apikey_query.all()
# return info
def create_user(self):
"""
If user logged in successfully via LDAP in the first time
We will create a local user (in DB) in order to manage user
profile such as name, roles,...
"""
# Set an invalid password hash for non local users
self.password = '*'
db.session.add(self)
db.session.commit()
def create_local_user(self):
"""
Create local user witch stores username / password in the DB
"""
# check if username existed
user = User.query.filter(User.username == self.username).first()
if user:
return {'status': False, 'msg': 'Username is already in use'}
# check if email existed
user = User.query.filter(User.email == self.email).first()
if user:
return {'status': False, 'msg': 'Email address is already in use'}
# first register user will be in Administrator role
self.role_id = Role.query.filter_by(name='User').first().id
if User.query.count() == 0:
self.role_id = Role.query.filter_by(
name='Administrator').first().id
self.password = self.get_hashed_password(
self.plain_text_password) if self.plain_text_password else '*'
if self.password and self.password != '*':
self.password = self.password.decode("utf-8")
db.session.add(self)
db.session.commit()
return {'status': True, 'msg': 'Created user successfully'}
def update_local_user(self):
"""
Update local user
"""
# Sanity check - account name
if self.username == "":
return {'status': False, 'msg': 'No user name specified'}
# read user and check that it exists
user = User.query.filter(User.username == self.username).first()
if not user:
return {'status': False, 'msg': 'User does not exist'}
# check if new email exists (only if changed)
if user.email != self.email:
checkuser = User.query.filter(User.email == self.email).first()
if checkuser:
return {
'status': False,
'msg': 'New email address is already in use'
}
user.firstname = self.firstname
user.lastname = self.lastname
user.email = self.email
# store new password hash (only if changed)
if self.plain_text_password != "":
user.password = self.get_hashed_password(
self.plain_text_password).decode("utf-8")
db.session.commit()
return {'status': True, 'msg': 'User updated successfully'}
def update_profile(self, enable_otp=None):
"""
Update user profile
"""
user = User.query.filter(User.username == self.username).first()
if not user:
return False
user.firstname = self.firstname if self.firstname else user.firstname
user.lastname = self.lastname if self.lastname else user.lastname
user.email = self.email if self.email else user.email
user.password = self.get_hashed_password(
self.plain_text_password).decode(
"utf-8") if self.plain_text_password else user.password
if enable_otp is not None:
user.otp_secret = ""
if enable_otp == True:
# generate the opt secret key
user.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8')
try:
db.session.add(user)
db.session.commit()
return True
except Exception:
db.session.rollback()
return False
def get_domains(self):
"""
Get list of domains which the user is granted to have
access.
Note: This doesn't include the permission granting from Account
which user belong to
"""
return self.get_domain_query().all()
def delete(self):
"""
Delete a user
"""
# revoke all user privileges first
self.revoke_privilege()
try:
User.query.filter(User.username == self.username).delete()
db.session.commit()
return True
except Exception as e:
db.session.rollback()
current_app.logger.error('Cannot delete user {0} from DB. DETAIL: {1}'.format(
self.username, e))
return False
def revoke_privilege(self):
"""
Revoke all privileges from a user
"""
user = User.query.filter(User.username == self.username).first()
if user:
user_id = user.id
try:
DomainUser.query.filter(DomainUser.user_id == user_id).delete()
db.session.commit()
return True
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot revoke user {0} privileges. DETAIL: {1}'.format(
self.username, e))
return False
return False
def set_role(self, role_name):
role = Role.query.filter(Role.name == role_name).first()
if role:
user = User.query.filter(User.username == self.username).first()
user.role_id = role.id
db.session.commit()
return {'status': True, 'msg': 'Set user role successfully'}
else:
return {'status': False, 'msg': 'Role does not exist'}

View File

@@ -0,0 +1,25 @@
from .base import login_manager, handle_bad_request, handle_unauthorized_access, handle_access_forbidden, handle_page_not_found, handle_internal_server_error
from .index import index_bp
from .user import user_bp
from .dashboard import dashboard_bp
from .domain import domain_bp
from .admin import admin_bp
from .api import api_bp
def init_app(app):
login_manager.init_app(app)
app.register_blueprint(index_bp)
app.register_blueprint(user_bp)
app.register_blueprint(dashboard_bp)
app.register_blueprint(domain_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(api_bp)
app.register_error_handler(400, handle_bad_request)
app.register_error_handler(401, handle_unauthorized_access)
app.register_error_handler(403, handle_access_forbidden)
app.register_error_handler(404, handle_page_not_found)
app.register_error_handler(500, handle_internal_server_error)

View File

@@ -0,0 +1,994 @@
import re
import json
import traceback
import datetime
from ast import literal_eval
from distutils.version import StrictVersion
from flask import Blueprint, render_template, make_response, url_for, current_app, g, session, request, redirect, jsonify, abort
from flask_login import login_user, login_required, current_user
from .base import login_manager
from ..decorators import operator_role_required, admin_role_required
from ..models.user import User, Anonymous
from ..models.account import Account
from ..models.account_user import AccountUser
from ..models.role import Role
from ..models.server import Server
from ..models.setting import Setting
from ..models.history import History
from ..models.domain import Domain
from ..models.record import Record
from ..models.domain_template import DomainTemplate
from ..models.domain_template_record import DomainTemplateRecord
admin_bp = Blueprint('admin',
__name__,
template_folder='templates',
url_prefix='/admin')
@admin_bp.route('/pdns', methods=['GET'])
@login_required
@operator_role_required
def pdns_stats():
if not Setting().get('pdns_api_url') or not Setting().get(
'pdns_api_key') or not Setting().get('pdns_version'):
return redirect(url_for('admin.setting_pdns'))
domains = Domain.query.all()
users = User.query.all()
server = Server(server_id='localhost')
configs = server.get_config()
statistics = server.get_statistic()
history_number = History.query.count()
if statistics:
uptime = list([
uptime for uptime in statistics if uptime['name'] == 'uptime'
])[0]['value']
else:
uptime = 0
return render_template('admin_pdns_stats.html',
domains=domains,
users=users,
configs=configs,
statistics=statistics,
uptime=uptime,
history_number=history_number)
@admin_bp.route('/user/edit/<user_username>', methods=['GET', 'POST'])
@admin_bp.route('/user/edit', methods=['GET', 'POST'])
@login_required
@operator_role_required
def edit_user(user_username=None):
if user_username:
user = User.query.filter(User.username == user_username).first()
create = False
if not user:
return render_template('errors/404.html'), 404
if user.role.name == 'Administrator' and current_user.role.name != 'Administrator':
return render_template('errors/401.html'), 401
else:
user = None
create = True
if request.method == 'GET':
return render_template('admin_edit_user.html',
user=user,
create=create)
elif request.method == 'POST':
fdata = request.form
if create:
user_username = fdata['username']
user = User(username=user_username,
plain_text_password=fdata['password'],
firstname=fdata['firstname'],
lastname=fdata['lastname'],
email=fdata['email'],
reload_info=False)
if create:
if fdata['password'] == "":
return render_template('admin_edit_user.html',
user=user,
create=create,
blank_password=True)
result = user.create_local_user()
history = History(msg='Created user {0}'.format(user.username),
created_by=current_user.username)
else:
result = user.update_local_user()
history = History(msg='Updated user {0}'.format(user.username),
created_by=current_user.username)
if result['status']:
history.add()
return redirect(url_for('admin.manage_user'))
return render_template('admin_edit_user.html',
user=user,
create=create,
error=result['msg'])
@admin_bp.route('/manage-user', methods=['GET', 'POST'])
@login_required
@operator_role_required
def manage_user():
if request.method == 'GET':
roles = Role.query.all()
users = User.query.order_by(User.username).all()
return render_template('admin_manage_user.html',
users=users,
roles=roles)
if request.method == 'POST':
#
# post data should in format
# {'action': 'delete_user', 'data': 'username'}
#
try:
jdata = request.json
data = jdata['data']
if jdata['action'] == 'user_otp_disable':
user = User(username=data)
result = user.update_profile(enable_otp=False)
if result:
history = History(
msg='Two factor authentication disabled for user {0}'.
format(data),
created_by=current_user.username)
history.add()
return make_response(
jsonify({
'status':
'ok',
'msg':
'Two factor authentication has been disabled for user.'
}), 200)
else:
return make_response(
jsonify({
'status':
'error',
'msg':
'Cannot disable two factor authentication for user.'
}), 500)
elif jdata['action'] == 'delete_user':
user = User(username=data)
if user.username == current_user.username:
return make_response(
jsonify({
'status': 'error',
'msg': 'You cannot delete yourself.'
}), 400)
# Remove account associations first
user_accounts = Account.query.join(AccountUser).join(
User).filter(AccountUser.user_id == user.id,
AccountUser.account_id == Account.id).all()
for uc in user_accounts:
uc.revoke_privileges_by_id(user.id)
# Then delete the user
result = user.delete()
if result:
history = History(msg='Delete username {0}'.format(data),
created_by=current_user.username)
history.add()
return make_response(
jsonify({
'status': 'ok',
'msg': 'User has been removed.'
}), 200)
else:
return make_response(
jsonify({
'status': 'error',
'msg': 'Cannot remove user.'
}), 500)
elif jdata['action'] == 'revoke_user_privileges':
user = User(username=data)
result = user.revoke_privilege()
if result:
history = History(
msg='Revoke {0} user privileges'.format(data),
created_by=current_user.username)
history.add()
return make_response(
jsonify({
'status': 'ok',
'msg': 'Revoked user privileges.'
}), 200)
else:
return make_response(
jsonify({
'status': 'error',
'msg': 'Cannot revoke user privilege.'
}), 500)
elif jdata['action'] == 'update_user_role':
username = data['username']
role_name = data['role_name']
if username == current_user.username:
return make_response(
jsonify({
'status': 'error',
'msg': 'You cannot change you own roles.'
}), 400)
user = User.query.filter(User.username == username).first()
if not user:
return make_response(
jsonify({
'status': 'error',
'msg': 'User does not exist.'
}), 404)
if user.role.name == 'Administrator' and current_user.role.name != 'Administrator':
return make_response(
jsonify({
'status':
'error',
'msg':
'You do not have permission to change Administrator users role.'
}), 400)
if role_name == 'Administrator' and current_user.role.name != 'Administrator':
return make_response(
jsonify({
'status':
'error',
'msg':
'You do not have permission to promote a user to Administrator role.'
}), 400)
user = User(username=username)
result = user.set_role(role_name)
if result['status']:
history = History(
msg='Change user role of {0} to {1}'.format(
username, role_name),
created_by=current_user.username)
history.add()
return make_response(
jsonify({
'status': 'ok',
'msg': 'Changed user role successfully.'
}), 200)
else:
return make_response(
jsonify({
'status':
'error',
'msg':
'Cannot change user role. {0}'.format(
result['msg'])
}), 500)
else:
return make_response(
jsonify({
'status': 'error',
'msg': 'Action not supported.'
}), 400)
except Exception as e:
current_app.logger.error(
'Cannot update user. Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
return make_response(
jsonify({
'status':
'error',
'msg':
'There is something wrong, please contact Administrator.'
}), 400)
@admin_bp.route('/account/edit/<account_name>', methods=['GET', 'POST'])
@admin_bp.route('/account/edit', methods=['GET', 'POST'])
@login_required
@operator_role_required
def edit_account(account_name=None):
users = User.query.all()
if request.method == 'GET':
if account_name is None:
return render_template('admin_edit_account.html',
users=users,
create=1)
else:
account = Account.query.filter(
Account.name == account_name).first()
account_user_ids = account.get_user()
return render_template('admin_edit_account.html',
account=account,
account_user_ids=account_user_ids,
users=users,
create=0)
if request.method == 'POST':
fdata = request.form
new_user_list = request.form.getlist('account_multi_user')
# on POST, synthesize account and account_user_ids from form data
if not account_name:
account_name = fdata['accountname']
account = Account(name=account_name,
description=fdata['accountdescription'],
contact=fdata['accountcontact'],
mail=fdata['accountmail'])
account_user_ids = []
for username in new_user_list:
userid = User(username=username).get_user_info_by_username().id
account_user_ids.append(userid)
create = int(fdata['create'])
if create:
# account __init__ sanitizes and lowercases the name, so to manage expectations
# we let the user reenter the name until it's not empty and it's valid (ignoring the case)
if account.name == "" or account.name != account_name.lower():
return render_template('admin_edit_account.html',
account=account,
account_user_ids=account_user_ids,
users=users,
create=create,
invalid_accountname=True)
if Account.query.filter(Account.name == account.name).first():
return render_template('admin_edit_account.html',
account=account,
account_user_ids=account_user_ids,
users=users,
create=create,
duplicate_accountname=True)
result = account.create_account()
history = History(msg='Create account {0}'.format(account.name),
created_by=current_user.username)
else:
result = account.update_account()
history = History(msg='Update account {0}'.format(account.name),
created_by=current_user.username)
if result['status']:
account.grant_privileges(new_user_list)
history.add()
return redirect(url_for('admin.manage_account'))
return render_template('admin_edit_account.html',
account=account,
account_user_ids=account_user_ids,
users=users,
create=create,
error=result['msg'])
@admin_bp.route('/manage-account', methods=['GET', 'POST'])
@login_required
@operator_role_required
def manage_account():
if request.method == 'GET':
accounts = Account.query.order_by(Account.name).all()
for account in accounts:
account.user_num = AccountUser.query.filter(
AccountUser.account_id == account.id).count()
return render_template('admin_manage_account.html', accounts=accounts)
if request.method == 'POST':
#
# post data should in format
# {'action': 'delete_account', 'data': 'accountname'}
#
try:
jdata = request.json
data = jdata['data']
if jdata['action'] == 'delete_account':
account = Account.query.filter(Account.name == data).first()
if not account:
return make_response(
jsonify({
'status': 'error',
'msg': 'Account not found.'
}), 404)
# Remove account association from domains first
for domain in account.domains:
Domain(name=domain.name).assoc_account(None)
# Then delete the account
result = account.delete_account()
if result:
history = History(msg='Delete account {0}'.format(data),
created_by=current_user.username)
history.add()
return make_response(
jsonify({
'status': 'ok',
'msg': 'Account has been removed.'
}), 200)
else:
return make_response(
jsonify({
'status': 'error',
'msg': 'Cannot remove account.'
}), 500)
else:
return make_response(
jsonify({
'status': 'error',
'msg': 'Action not supported.'
}), 400)
except Exception as e:
current_app.logger.error(
'Cannot update account. Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
return make_response(
jsonify({
'status':
'error',
'msg':
'There is something wrong, please contact Administrator.'
}), 400)
@admin_bp.route('/history', methods=['GET', 'POST'])
@login_required
@operator_role_required
def history():
if request.method == 'POST':
if current_user.role.name != 'Administrator':
return make_response(
jsonify({
'status': 'error',
'msg': 'You do not have permission to remove history.'
}), 401)
h = History()
result = h.remove_all()
if result:
history = History(msg='Remove all histories',
created_by=current_user.username)
history.add()
return make_response(
jsonify({
'status': 'ok',
'msg': 'Changed user role successfully.'
}), 200)
else:
return make_response(
jsonify({
'status': 'error',
'msg': 'Can not remove histories.'
}), 500)
if request.method == 'GET':
histories = History.query.all()
return render_template('admin_history.html', histories=histories)
@admin_bp.route('/setting/basic', methods=['GET'])
@login_required
@operator_role_required
def setting_basic():
if request.method == 'GET':
settings = [
'maintenance', 'fullscreen_layout', 'record_helper',
'login_ldap_first', 'default_record_table_size',
'default_domain_table_size', 'auto_ptr', 'record_quick_edit',
'pretty_ipv6_ptr', 'dnssec_admins_only',
'allow_user_create_domain', 'bg_domain_updates', 'site_name',
'session_timeout', 'ttl_options', 'pdns_api_timeout'
]
return render_template('admin_setting_basic.html', settings=settings)
@admin_bp.route('/setting/basic/<path:setting>/edit', methods=['POST'])
@login_required
@operator_role_required
def setting_basic_edit(setting):
jdata = request.json
new_value = jdata['value']
result = Setting().set(setting, new_value)
if (result):
return make_response(
jsonify({
'status': 'ok',
'msg': 'Toggled setting successfully.'
}), 200)
else:
return make_response(
jsonify({
'status': 'error',
'msg': 'Unable to toggle setting.'
}), 500)
@admin_bp.route('/setting/basic/<path:setting>/toggle', methods=['POST'])
@login_required
@operator_role_required
def setting_basic_toggle(setting):
result = Setting().toggle(setting)
if (result):
return make_response(
jsonify({
'status': 'ok',
'msg': 'Toggled setting successfully.'
}), 200)
else:
return make_response(
jsonify({
'status': 'error',
'msg': 'Unable to toggle setting.'
}), 500)
@admin_bp.route('/setting/pdns', methods=['GET', 'POST'])
@login_required
@admin_role_required
def setting_pdns():
if request.method == 'GET':
pdns_api_url = Setting().get('pdns_api_url')
pdns_api_key = Setting().get('pdns_api_key')
pdns_version = Setting().get('pdns_version')
return render_template('admin_setting_pdns.html',
pdns_api_url=pdns_api_url,
pdns_api_key=pdns_api_key,
pdns_version=pdns_version)
elif request.method == 'POST':
pdns_api_url = request.form.get('pdns_api_url')
pdns_api_key = request.form.get('pdns_api_key')
pdns_version = request.form.get('pdns_version')
Setting().set('pdns_api_url', pdns_api_url)
Setting().set('pdns_api_key', pdns_api_key)
Setting().set('pdns_version', pdns_version)
return render_template('admin_setting_pdns.html',
pdns_api_url=pdns_api_url,
pdns_api_key=pdns_api_key,
pdns_version=pdns_version)
@admin_bp.route('/setting/dns-records', methods=['GET', 'POST'])
@login_required
@operator_role_required
def setting_records():
if request.method == 'GET':
_fr = Setting().get('forward_records_allow_edit')
_rr = Setting().get('reverse_records_allow_edit')
f_records = literal_eval(_fr) if isinstance(_fr, str) else _fr
r_records = literal_eval(_rr) if isinstance(_rr, str) else _rr
return render_template('admin_setting_records.html',
f_records=f_records,
r_records=r_records)
elif request.method == 'POST':
fr = {}
rr = {}
records = Setting().defaults['forward_records_allow_edit']
for r in records:
fr[r] = True if request.form.get('fr_{0}'.format(
r.lower())) else False
rr[r] = True if request.form.get('rr_{0}'.format(
r.lower())) else False
Setting().set('forward_records_allow_edit', str(fr))
Setting().set('reverse_records_allow_edit', str(rr))
return redirect(url_for('admin.setting_records'))
@admin_bp.route('/setting/authentication', methods=['GET', 'POST'])
@login_required
@admin_role_required
def setting_authentication():
if request.method == 'GET':
return render_template('admin_setting_authentication.html')
elif request.method == 'POST':
conf_type = request.form.get('config_tab')
result = None
if conf_type == 'general':
local_db_enabled = True if request.form.get(
'local_db_enabled') else False
signup_enabled = True if request.form.get(
'signup_enabled', ) else False
if not local_db_enabled and not Setting().get('ldap_enabled'):
result = {
'status':
False,
'msg':
'Local DB and LDAP Authentication can not be disabled at the same time.'
}
else:
Setting().set('local_db_enabled', local_db_enabled)
Setting().set('signup_enabled', signup_enabled)
result = {'status': True, 'msg': 'Saved successfully'}
elif conf_type == 'ldap':
ldap_enabled = True if request.form.get('ldap_enabled') else False
if not ldap_enabled and not Setting().get('local_db_enabled'):
result = {
'status':
False,
'msg':
'Local DB and LDAP Authentication can not be disabled at the same time.'
}
else:
Setting().set('ldap_enabled', ldap_enabled)
Setting().set('ldap_type', request.form.get('ldap_type'))
Setting().set('ldap_uri', request.form.get('ldap_uri'))
Setting().set('ldap_base_dn', request.form.get('ldap_base_dn'))
Setting().set('ldap_admin_username',
request.form.get('ldap_admin_username'))
Setting().set('ldap_admin_password',
request.form.get('ldap_admin_password'))
Setting().set('ldap_filter_basic',
request.form.get('ldap_filter_basic'))
Setting().set('ldap_filter_username',
request.form.get('ldap_filter_username'))
Setting().set(
'ldap_sg_enabled', True
if request.form.get('ldap_sg_enabled') == 'ON' else False)
Setting().set('ldap_admin_group',
request.form.get('ldap_admin_group'))
Setting().set('ldap_operator_group',
request.form.get('ldap_operator_group'))
Setting().set('ldap_user_group',
request.form.get('ldap_user_group'))
Setting().set('ldap_domain', request.form.get('ldap_domain'))
result = {'status': True, 'msg': 'Saved successfully'}
elif conf_type == 'google':
Setting().set(
'google_oauth_enabled',
True if request.form.get('google_oauth_enabled') else False)
Setting().set('google_oauth_client_id',
request.form.get('google_oauth_client_id'))
Setting().set('google_oauth_client_secret',
request.form.get('google_oauth_client_secret'))
Setting().set('google_token_url',
request.form.get('google_token_url'))
Setting().set('google_oauth_scope',
request.form.get('google_oauth_scope'))
Setting().set('google_authorize_url',
request.form.get('google_authorize_url'))
Setting().set('google_base_url',
request.form.get('google_base_url'))
result = {
'status': True,
'msg': 'Saved successfully. Please reload PDA to take effect.'
}
elif conf_type == 'github':
Setting().set(
'github_oauth_enabled',
True if request.form.get('github_oauth_enabled') else False)
Setting().set('github_oauth_key',
request.form.get('github_oauth_key'))
Setting().set('github_oauth_secret',
request.form.get('github_oauth_secret'))
Setting().set('github_oauth_scope',
request.form.get('github_oauth_scope'))
Setting().set('github_oauth_api_url',
request.form.get('github_oauth_api_url'))
Setting().set('github_oauth_token_url',
request.form.get('github_oauth_token_url'))
Setting().set('github_oauth_authorize_url',
request.form.get('github_oauth_authorize_url'))
result = {
'status': True,
'msg': 'Saved successfully. Please reload PDA to take effect.'
}
elif conf_type == 'oidc':
Setting().set(
'oidc_oauth_enabled',
True if request.form.get('oidc_oauth_enabled') else False)
Setting().set('oidc_oauth_key', request.form.get('oidc_oauth_key'))
Setting().set('oidc_oauth_secret',
request.form.get('oidc_oauth_secret'))
Setting().set('oidc_oauth_scope',
request.form.get('oidc_oauth_scope'))
Setting().set('oidc_oauth_api_url',
request.form.get('oidc_oauth_api_url'))
Setting().set('oidc_oauth_token_url',
request.form.get('oidc_oauth_token_url'))
Setting().set('oidc_oauth_authorize_url',
request.form.get('oidc_oauth_authorize_url'))
result = {
'status': True,
'msg': 'Saved successfully. Please reload PDA to take effect.'
}
else:
return abort(400)
return render_template('admin_setting_authentication.html',
result=result)
@admin_bp.route('/templates', methods=['GET', 'POST'])
@admin_bp.route('/templates/list', methods=['GET', 'POST'])
@login_required
@operator_role_required
def templates():
templates = DomainTemplate.query.all()
return render_template('template.html', templates=templates)
@admin_bp.route('/template/create', methods=['GET', 'POST'])
@login_required
@operator_role_required
def create_template():
if request.method == 'GET':
return render_template('template_add.html')
if request.method == 'POST':
try:
name = request.form.getlist('name')[0]
description = request.form.getlist('description')[0]
if ' ' in name or not name or not type:
flash("Please correct your input", 'error')
return redirect(url_for('admin.create_template'))
if DomainTemplate.query.filter(
DomainTemplate.name == name).first():
flash(
"A template with the name {0} already exists!".format(
name), 'error')
return redirect(url_for('admin.create_template'))
t = DomainTemplate(name=name, description=description)
result = t.create()
if result['status'] == 'ok':
history = History(msg='Add domain template {0}'.format(name),
detail=str({
'name': name,
'description': description
}),
created_by=current_user.username)
history.add()
return redirect(url_for('admin.templates'))
else:
flash(result['msg'], 'error')
return redirect(url_for('admin.create_template'))
except Exception as e:
current_app.logger.error(
'Cannot create domain template. Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
abort(500)
@admin_bp.route('/template/create-from-zone', methods=['POST'])
@login_required
@operator_role_required
def create_template_from_zone():
try:
jdata = request.json
name = jdata['name']
description = jdata['description']
domain_name = jdata['domain']
if ' ' in name or not name or not type:
return make_response(
jsonify({
'status': 'error',
'msg': 'Please correct template name'
}), 400)
if DomainTemplate.query.filter(DomainTemplate.name == name).first():
return make_response(
jsonify({
'status':
'error',
'msg':
'A template with the name {0} already exists!'.format(name)
}), 409)
t = DomainTemplate(name=name, description=description)
result = t.create()
if result['status'] == 'ok':
history = History(msg='Add domain template {0}'.format(name),
detail=str({
'name': name,
'description': description
}),
created_by=current_user.username)
history.add()
records = []
r = Record()
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
# query domain info from PowerDNS API
zone_info = r.get_record_data(domain.name)
if zone_info:
jrecords = zone_info['records']
if StrictVersion(Setting().get(
'pdns_version')) >= StrictVersion('4.0.0'):
for jr in jrecords:
if jr['type'] in Setting().get_records_allow_to_edit():
name = '@' if jr['name'] == domain_name else re.sub(
'\.{}$'.format(domain_name), '', jr['name'])
for subrecord in jr['records']:
record = DomainTemplateRecord(
name=name,
type=jr['type'],
status=True
if subrecord['disabled'] else False,
ttl=jr['ttl'],
data=subrecord['content'])
records.append(record)
else:
for jr in jrecords:
if jr['type'] in Setting().get_records_allow_to_edit():
name = '@' if jr['name'] == domain_name else re.sub(
'\.{}$'.format(domain_name), '', jr['name'])
record = DomainTemplateRecord(
name=name,
type=jr['type'],
status=True if jr['disabled'] else False,
ttl=jr['ttl'],
data=jr['content'])
records.append(record)
result_records = t.replace_records(records)
if result_records['status'] == 'ok':
return make_response(
jsonify({
'status': 'ok',
'msg': result['msg']
}), 200)
else:
t.delete_template()
return make_response(
jsonify({
'status': 'error',
'msg': result_records['msg']
}), 500)
else:
return make_response(
jsonify({
'status': 'error',
'msg': result['msg']
}), 500)
except Exception as e:
current_app.logger.error(
'Cannot create template from zone. Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
return make_response(
jsonify({
'status': 'error',
'msg': 'Error when applying new changes'
}), 500)
@admin_bp.route('/template/<path:template>/edit', methods=['GET'])
@login_required
@operator_role_required
def edit_template(template):
try:
t = DomainTemplate.query.filter(
DomainTemplate.name == template).first()
records_allow_to_edit = Setting().get_records_allow_to_edit()
quick_edit = Setting().get('record_quick_edit')
ttl_options = Setting().get_ttl_options()
if t is not None:
records = []
for jr in t.records:
if jr.type in records_allow_to_edit:
record = DomainTemplateRecord(
name=jr.name,
type=jr.type,
status='Disabled' if jr.status else 'Active',
ttl=jr.ttl,
data=jr.data)
records.append(record)
return render_template('template_edit.html',
template=t.name,
records=records,
editable_records=records_allow_to_edit,
quick_edit=quick_edit,
ttl_options=ttl_options)
except Exception as e:
current_app.logger.error(
'Cannot open domain template page. DETAIL: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
abort(500)
return redirect(url_for('admin.templates'))
@admin_bp.route('/template/<path:template>/apply',
methods=['POST'],
strict_slashes=False)
@login_required
def apply_records(template):
try:
jdata = request.json
records = []
for j in jdata['records']:
name = '@' if j['record_name'] in ['@', ''] else j['record_name']
type = j['record_type']
data = j['record_data']
disabled = True if j['record_status'] == 'Disabled' else False
ttl = int(j['record_ttl']) if j['record_ttl'] else 3600
dtr = DomainTemplateRecord(name=name,
type=type,
data=data,
status=disabled,
ttl=ttl)
records.append(dtr)
t = DomainTemplate.query.filter(
DomainTemplate.name == template).first()
result = t.replace_records(records)
if result['status'] == 'ok':
jdata.pop('_csrf_token',
None) # don't store csrf token in the history.
history = History(
msg='Apply domain template record changes to domain template {0}'
.format(template),
detail=str(json.dumps(jdata)),
created_by=current_user.username)
history.add()
return make_response(jsonify(result), 200)
else:
return make_response(jsonify(result), 400)
except Exception as e:
current_app.logger.error(
'Cannot apply record changes to the template. Error: {0}'.format(
e))
current_app.logger.debug(traceback.format_exc())
return make_response(
jsonify({
'status': 'error',
'msg': 'Error when applying new changes'
}), 500)
@admin_bp.route('/template/<path:template>/delete', methods=['POST'])
@login_required
@operator_role_required
def delete_template(template):
try:
t = DomainTemplate.query.filter(
DomainTemplate.name == template).first()
if t is not None:
result = t.delete_template()
if result['status'] == 'ok':
history = History(
msg='Deleted domain template {0}'.format(template),
detail=str({'name': template}),
created_by=current_user.username)
history.add()
return redirect(url_for('admin.templates'))
else:
flash(result['msg'], 'error')
return redirect(url_for('admin.templates'))
except Exception as e:
current_app.logger.error(
'Cannot delete template. Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
abort(500)
return redirect(url_for('admin.templates'))

518
powerdnsadmin/routes/api.py Normal file
View File

@@ -0,0 +1,518 @@
import json
from urllib.parse import urljoin
from flask import Blueprint, g, request, abort, current_app
from flask_login import current_user
from ..models.base import db
from ..models import Domain, DomainUser, Account, AccountUser, History, Setting, ApiKey
from ..lib import utils, helper
from ..lib.schema import ApiKeySchema, DomainSchema, ApiPlainKeySchema
from ..lib.errors import DomainNotExists, DomainAlreadyExists, DomainAccessForbidden, RequestIsNotJSON, ApiKeyCreateFail, ApiKeyNotUsable, NotEnoughPrivileges
from ..decorators import api_basic_auth, api_can_create_domain, is_json, apikey_auth, apikey_is_admin, apikey_can_access_domain
api_bp = Blueprint('api', __name__, url_prefix='/api/v1')
apikey_schema = ApiKeySchema(many=True)
domain_schema = DomainSchema(many=True)
apikey_plain_schema = ApiPlainKeySchema(many=True)
def get_user_domains():
domains = 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 == current_user.id,
AccountUser.user_id == current_user.id
)).all()
return domains
@api_bp.errorhandler(400)
def handle_400(err):
return json.dumps({"msg": "Bad Request"}), 400
@api_bp.errorhandler(401)
def handle_401(err):
return json.dumps({"msg": "Unauthorized"}), 401
@api_bp.errorhandler(409)
def handle_409(err):
return json.dumps({"msg": "Conflict"}), 409
@api_bp.errorhandler(500)
def handle_500(err):
return json.dumps({"msg": "Internal Server Error"}), 500
@api_bp.errorhandler(DomainNotExists)
def handle_domain_not_exists(err):
return json.dumps(err.to_dict()), err.status_code
@api_bp.errorhandler(DomainAlreadyExists)
def handle_domain_already_exists(err):
return json.dumps(err.to_dict()), err.status_code
@api_bp.errorhandler(DomainAccessForbidden)
def handle_domain_access_forbidden(err):
return json.dumps(err.to_dict()), err.status_code
@api_bp.errorhandler(ApiKeyCreateFail)
def handle_apikey_create_fail(err):
return json.dumps(err.to_dict()), err.status_code
@api_bp.errorhandler(ApiKeyNotUsable)
def handle_apikey_not_usable(err):
return json.dumps(err.to_dict()), err.status_code
@api_bp.errorhandler(NotEnoughPrivileges)
def handle_not_enough_privileges(err):
return json.dumps(err.to_dict()), err.status_code
@api_bp.errorhandler(RequestIsNotJSON)
def handle_request_is_not_json(err):
return json.dumps(err.to_dict()), err.status_code
@api_bp.before_request
@is_json
def before_request():
pass
@api_bp.route('/pdnsadmin/zones', methods=['POST'])
@api_basic_auth
@api_can_create_domain
def api_login_create_zone():
pdns_api_url = Setting().get('pdns_api_url')
pdns_api_key = Setting().get('pdns_api_key')
pdns_version = Setting().get('pdns_version')
api_uri_with_prefix = utils.pdns_api_extended_uri(pdns_version)
api_full_uri = api_uri_with_prefix + '/servers/localhost/zones'
headers = {}
headers['X-API-Key'] = pdns_api_key
msg_str = "Sending request to powerdns API {0}"
msg = msg_str.format(request.get_json(force=True))
current_app.logger.debug(msg)
try:
resp = utils.fetch_remote(urljoin(pdns_api_url, api_full_uri),
method='POST',
data=request.get_json(force=True),
headers=headers,
accept='application/json; q=1')
except Exception as e:
current_app.logger.error("Cannot create domain. Error: {}".format(e))
abort(500)
if resp.status_code == 201:
current_app.logger.debug("Request to powerdns API successful")
data = request.get_json(force=True)
history = History(msg='Add domain {0}'.format(
data['name'].rstrip('.')),
detail=json.dumps(data),
created_by=current_user.username)
history.add()
if current_user.role.name not in ['Administrator', 'Operator']:
current_app.logger.debug(
"User is ordinary user, assigning created domain")
domain = Domain(name=data['name'].rstrip('.'))
domain.update()
domain.grant_privileges([current_user.username])
domain = Domain()
domain.update()
if resp.status_code == 409:
raise(DomainAlreadyExists)
return resp.content, resp.status_code, resp.headers.items()
@api_bp.route('/pdnsadmin/zones', methods=['GET'])
@api_basic_auth
def api_login_list_zones():
if current_user.role.name not in ['Administrator', 'Operator']:
domain_obj_list = get_user_domains()
else:
domain_obj_list = Domain.query.all()
domain_obj_list = [] if domain_obj_list is None else domain_obj_list
return json.dumps(domain_schema.dump(domain_obj_list)), 200
@api_bp.route('/pdnsadmin/zones/<string:domain_name>', methods=['DELETE'])
@api_basic_auth
@api_can_create_domain
def api_login_delete_zone(domain_name):
pdns_api_url = Setting().get('pdns_api_url')
pdns_api_key = Setting().get('pdns_api_key')
pdns_version = Setting().get('pdns_version')
api_uri_with_prefix = utils.pdns_api_extended_uri(pdns_version)
api_full_uri = api_uri_with_prefix + '/servers/localhost/zones'
api_full_uri += '/' + domain_name
headers = {}
headers['X-API-Key'] = pdns_api_key
domain = Domain.query.filter(Domain.name == domain_name)
if not domain:
abort(404)
if current_user.role.name not in ['Administrator', 'Operator']:
user_domains_obj_list = get_user_domains()
user_domains_list = [item.name for item in user_domains_obj_list]
if domain_name not in user_domains_list:
raise DomainAccessForbidden()
msg_str = "Sending request to powerdns API {0}"
current_app.logger.debug(msg_str.format(domain_name))
try:
resp = utils.fetch_remote(urljoin(pdns_api_url, api_full_uri),
method='DELETE',
headers=headers,
accept='application/json; q=1')
if resp.status_code == 204:
current_app.logger.debug("Request to powerdns API successful")
history = History(msg='Delete domain {0}'.format(domain_name),
detail='',
created_by=current_user.username)
history.add()
domain = Domain()
domain.update()
except Exception as e:
current_app.logger.error('Error: {0}'.format(e))
abort(500)
return resp.content, resp.status_code, resp.headers.items()
@api_bp.route('/pdnsadmin/apikeys', methods=['POST'])
@api_basic_auth
def api_generate_apikey():
data = request.get_json()
description = None
role_name = None
apikey = None
domain_obj_list = []
abort(400) if 'domains' not in data else None
abort(400) if not isinstance(data['domains'], (list, )) else None
abort(400) if 'role' not in data else None
description = data['description'] if 'description' in data else None
role_name = data['role']
domains = data['domains']
if role_name == 'User' and len(domains) == 0:
current_app.logger.error("Apikey with User role must have domains")
raise ApiKeyNotUsable()
elif role_name == 'User':
domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all()
if len(domain_obj_list) == 0:
msg = "One of supplied domains does not exists"
current_app.logger.error(msg)
raise DomainNotExists(message=msg)
if current_user.role.name not in ['Administrator', 'Operator']:
# domain list of domain api key should be valid for
# if not any domain error
# role of api key, user cannot assign role above for api key
if role_name != 'User':
msg = "User cannot assign other role than User"
current_app.logger.error(msg)
raise NotEnoughPrivileges(message=msg)
user_domain_obj_list = get_user_domains()
domain_list = [item.name for item in domain_obj_list]
user_domain_list = [item.name for item in user_domain_obj_list]
current_app.logger.debug("Input domain list: {0}".format(domain_list))
current_app.logger.debug(
"User domain list: {0}".format(user_domain_list))
inter = set(domain_list).intersection(set(user_domain_list))
if not (len(inter) == len(domain_list)):
msg = "You don't have access to one of domains"
current_app.logger.error(msg)
raise DomainAccessForbidden(message=msg)
apikey = ApiKey(desc=description,
role_name=role_name,
domains=domain_obj_list)
try:
apikey.create()
except Exception as e:
current_app.logger.error('Error: {0}'.format(e))
raise ApiKeyCreateFail(message='Api key create failed')
return json.dumps(apikey_plain_schema.dump([apikey])), 201
@api_bp.route('/pdnsadmin/apikeys', defaults={'domain_name': None})
@api_bp.route('/pdnsadmin/apikeys/<string:domain_name>')
@api_basic_auth
def api_get_apikeys(domain_name):
apikeys = []
current_app.logger.debug("Getting apikeys")
if current_user.role.name not in ['Administrator', 'Operator']:
if domain_name:
msg = "Check if domain {0} exists and \
is allowed for user." .format(domain_name)
current_app.logger.debug(msg)
apikeys = current_user.get_apikeys(domain_name)
if not apikeys:
raise DomainAccessForbidden(name=domain_name)
current_app.logger.debug(apikey_schema.dump(apikeys))
else:
msg_str = "Getting all allowed domains for user {0}"
msg = msg_str.format(current_user.username)
current_app.logger.debug(msg)
try:
apikeys = current_user.get_apikeys()
current_app.logger.debug(apikey_schema.dump(apikeys))
except Exception as e:
current_app.logger.error('Error: {0}'.format(e))
abort(500)
else:
current_app.logger.debug("Getting all domains for administrative user")
try:
apikeys = ApiKey.query.all()
current_app.logger.debug(apikey_schema.dump(apikeys))
except Exception as e:
current_app.logger.error('Error: {0}'.format(e))
abort(500)
return json.dumps(apikey_schema.dump(apikeys)), 200
@api_bp.route('/pdnsadmin/apikeys/<int:apikey_id>', methods=['DELETE'])
@api_basic_auth
def api_delete_apikey(apikey_id):
apikey = ApiKey.query.get(apikey_id)
if not apikey:
abort(404)
current_app.logger.debug(current_user.role.name)
if current_user.role.name not in ['Administrator', 'Operator']:
apikeys = current_user.get_apikeys()
user_domains_obj_list = current_user.get_domain().all()
apikey_domains_obj_list = apikey.domains
user_domains_list = [item.name for item in user_domains_obj_list]
apikey_domains_list = [item.name for item in apikey_domains_obj_list]
apikeys_ids = [apikey_item.id for apikey_item in apikeys]
inter = set(apikey_domains_list).intersection(set(user_domains_list))
if not (len(inter) == len(apikey_domains_list)):
msg = "You don't have access to some domains apikey belongs to"
current_app.logger.error(msg)
raise DomainAccessForbidden(message=msg)
if apikey_id not in apikeys_ids:
raise DomainAccessForbidden()
try:
apikey.delete()
except Exception as e:
current_app.logger.error('Error: {0}'.format(e))
abort(500)
return '', 204
@api_bp.route('/pdnsadmin/apikeys/<int:apikey_id>', methods=['PUT'])
@api_basic_auth
def api_update_apikey(apikey_id):
# if role different and user is allowed to change it, update
# if apikey domains are different and user is allowed to handle
# that domains update domains
data = request.get_json()
description = data['description'] if 'description' in data else None
role_name = data['role'] if 'role' in data else None
domains = data['domains'] if 'domains' in data else None
domain_obj_list = None
apikey = ApiKey.query.get(apikey_id)
if not apikey:
abort(404)
current_app.logger.debug('Updating apikey with id {0}'.format(apikey_id))
if role_name == 'User' and len(domains) == 0:
current_app.logger.error("Apikey with User role must have domains")
raise ApiKeyNotUsable()
elif role_name == 'User':
domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all()
if len(domain_obj_list) == 0:
msg = "One of supplied domains does not exists"
current_app.logger.error(msg)
raise DomainNotExists(message=msg)
if current_user.role.name not in ['Administrator', 'Operator']:
if role_name != 'User':
msg = "User cannot assign other role than User"
current_app.logger.error(msg)
raise NotEnoughPrivileges(message=msg)
apikeys = current_user.get_apikeys()
apikey_domains = [item.name for item in apikey.domains]
apikeys_ids = [apikey_item.id for apikey_item in apikeys]
user_domain_obj_list = current_user.get_domain().all()
domain_list = [item.name for item in domain_obj_list]
user_domain_list = [item.name for item in user_domain_obj_list]
current_app.logger.debug("Input domain list: {0}".format(domain_list))
current_app.logger.debug(
"User domain list: {0}".format(user_domain_list))
inter = set(domain_list).intersection(set(user_domain_list))
if not (len(inter) == len(domain_list)):
msg = "You don't have access to one of domains"
current_app.logger.error(msg)
raise DomainAccessForbidden(message=msg)
if apikey_id not in apikeys_ids:
msg = 'Apikey does not belong to domain to which user has access'
current_app.logger.error(msg)
raise DomainAccessForbidden()
if set(domains) == set(apikey_domains):
current_app.logger.debug(
"Domains are same, apikey domains won't be updated")
domains = None
if role_name == apikey.role:
current_app.logger.debug("Role is same, apikey role won't be updated")
role_name = None
if description == apikey.description:
msg = "Description is same, apikey description won't be updated"
current_app.logger.debug(msg)
description = None
try:
apikey = ApiKey.query.get(apikey_id)
apikey.update(role_name=role_name,
domains=domains,
description=description)
except Exception as e:
current_app.logger.error('Error: {0}'.format(e))
abort(500)
return '', 204
@api_bp.route(
'/servers/<string:server_id>/zones/<string:zone_id>/<path:subpath>',
methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
@apikey_auth
@apikey_can_access_domain
def api_zone_subpath_forward(server_id, zone_id, subpath):
resp = helper.forward_request()
return resp.content, resp.status_code, resp.headers.items()
@api_bp.route('/servers/<string:server_id>/zones/<string:zone_id>',
methods=['GET', 'PUT', 'PATCH', 'DELETE'])
@apikey_auth
@apikey_can_access_domain
def api_zone_forward(server_id, zone_id):
resp = helper.forward_request()
domain = Domain()
domain.update()
return resp.content, resp.status_code, resp.headers.items()
@api_bp.route('/servers', methods=['GET'])
@apikey_auth
@apikey_is_admin
def api_server_forward():
resp = helper.forward_request()
return resp.content, resp.status_code, resp.headers.items()
@api_bp.route('/servers/<path:subpath>', methods=['GET', 'PUT'])
@apikey_auth
@apikey_is_admin
def api_server_sub_forward(subpath):
resp = helper.forward_request()
return resp.content, resp.status_code, resp.headers.items()
@api_bp.route('/servers/<string:server_id>/zones', methods=['POST'])
@apikey_auth
def api_create_zone(server_id):
resp = helper.forward_request()
if resp.status_code == 201:
current_app.logger.debug("Request to powerdns API successful")
data = request.get_json(force=True)
history = History(msg='Add domain {0}'.format(
data['name'].rstrip('.')),
detail=json.dumps(data),
created_by=g.apikey.description)
history.add()
if g.apikey.role.name not in ['Administrator', 'Operator']:
current_app.logger.debug(
"Apikey is user key, assigning created domain")
domain = Domain(name=data['name'].rstrip('.'))
g.apikey.domains.append(domain)
domain = Domain()
domain.update()
return resp.content, resp.status_code, resp.headers.items()
@api_bp.route('/servers/<string:server_id>/zones', methods=['GET'])
@apikey_auth
def api_get_zones(server_id):
if g.apikey.role.name not in ['Administrator', 'Operator']:
domain_obj_list = g.apikey.domains
else:
domain_obj_list = Domain.query.all()
return json.dumps(domain_schema.dump(domain_obj_list)), 200
#endpoint to snychronize Domains in background
@api_bp.route('/sync_domains', methods=['GET'])
@apikey_auth
def sync_domains():
domain = Domain()
domain.update()
return 'Finished synchronization in background', 200

View File

@@ -0,0 +1,65 @@
import base64
from flask import render_template, url_for, redirect, session, request, current_app
from flask_login import LoginManager, login_user
from ..models.user import User
login_manager = LoginManager()
def handle_bad_request(e):
return render_template('errors/400.html', code=400, message=e), 400
def handle_unauthorized_access(e):
session['next'] = request.script_root + request.path
return redirect(url_for('index.login'))
def handle_access_forbidden(e):
return render_template('errors/403.html', code=403, message=e), 403
def handle_page_not_found(e):
return render_template('errors/404.html', code=404, message=e), 404
def handle_internal_server_error(e):
return render_template('errors/500.html', code=500, message=e), 500
@login_manager.user_loader
def load_user(id):
"""
This will be current_user
"""
return User.query.get(int(id))
@login_manager.request_loader
def login_via_authorization_header(request):
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 TypeError as e:
return None
user = User(username=username,
password=password,
plain_text_password=password)
try:
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 auth == False:
return None
else:
# login_user(user, remember=False)
return User.query.filter(User.id==user.id).first()
except Exception as e:
current_app.logger.error('Error: {0}'.format(e))
return None
return None

View File

@@ -0,0 +1,166 @@
from flask import Blueprint, render_template, make_response, url_for, current_app, request, jsonify
from flask_login import login_required, current_user
from sqlalchemy import not_, or_
from ..lib.utils import customBoxes
from ..models.user import User
from ..models.account import Account
from ..models.account_user import AccountUser
from ..models.domain import Domain
from ..models.domain_user import DomainUser
from ..models.setting import Setting
from ..models.history import History
from ..models.server import Server
from ..models.base import db
dashboard_bp = Blueprint('dashboard',
__name__,
template_folder='templates',
url_prefix='/dashboard')
@dashboard_bp.route('/domains-custom/<path:boxId>', methods=['GET'])
@login_required
def domains_custom(boxId):
if current_user.role.name in ['Administrator', 'Operator']:
domains = Domain.query
else:
# Get query for domain to which the user has access permission.
# This includes direct domain permission AND permission through
# account membership
domains = 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 == current_user.id,
AccountUser.user_id == current_user.id
))
template = current_app.jinja_env.get_template("dashboard_domain.html")
render = template.make_module(vars={"current_user": current_user})
columns = [
Domain.name, Domain.dnssec, Domain.type, Domain.serial, Domain.master,
Domain.account
]
# History.created_on.desc()
order_by = []
for i in range(len(columns)):
column_index = request.args.get("order[{0}][column]".format(i))
sort_direction = request.args.get("order[{0}][dir]".format(i))
if column_index is None:
break
if sort_direction != "asc" and sort_direction != "desc":
sort_direction = "asc"
column = columns[int(column_index)]
order_by.append(getattr(column, sort_direction)())
if order_by:
domains = domains.order_by(*order_by)
if boxId == "reverse":
for boxId in customBoxes.order:
if boxId == "reverse": continue
domains = domains.filter(
not_(Domain.name.ilike(customBoxes.boxes[boxId][1])))
else:
domains = domains.filter(Domain.name.ilike(
customBoxes.boxes[boxId][1]))
total_count = domains.count()
search = request.args.get("search[value]")
if search:
start = "" if search.startswith("^") else "%"
end = "" if search.endswith("$") else "%"
if current_user.role.name in ['Administrator', 'Operator']:
domains = domains.outerjoin(Account).filter(
Domain.name.ilike(start + search.strip("^$") + end)
| Account.name.ilike(start + search.strip("^$") + end)
| Account.description.ilike(start + search.strip("^$") + end))
else:
domains = domains.filter(
Domain.name.ilike(start + search.strip("^$") + end))
filtered_count = domains.count()
start = int(request.args.get("start", 0))
length = min(int(request.args.get("length", 0)), 100)
if length != -1:
domains = domains[start:start + length]
data = []
for domain in domains:
data.append([
render.name(domain),
render.dnssec(domain),
render.type(domain),
render.serial(domain),
render.master(domain),
render.account(domain),
render.actions(domain),
])
response_data = {
"draw": int(request.args.get("draw", 0)),
"recordsTotal": total_count,
"recordsFiltered": filtered_count,
"data": data,
}
return jsonify(response_data)
@dashboard_bp.route('/', methods=['GET', 'POST'])
@login_required
def dashboard():
if not Setting().get('pdns_api_url') or not Setting().get(
'pdns_api_key') or not Setting().get('pdns_version'):
return redirect(url_for('admin_setting_pdns'))
BG_DOMAIN_UPDATE = Setting().get('bg_domain_updates')
if not BG_DOMAIN_UPDATE:
current_app.logger.info('Updating domains in foreground...')
Domain().update()
else:
current_app.logger.info('Updating domains in background...')
# Stats for dashboard
domain_count = Domain.query.count()
user_num = User.query.count()
history_number = History.query.count()
history = History.query.order_by(History.created_on.desc()).limit(4)
server = Server(server_id='localhost')
statistics = server.get_statistic()
if statistics:
uptime = list([
uptime for uptime in statistics if uptime['name'] == 'uptime'
])[0]['value']
else:
uptime = 0
# Add custom boxes to render_template
return render_template('dashboard.html',
custom_boxes=customBoxes,
domain_count=domain_count,
user_num=user_num,
history_number=history_number,
uptime=uptime,
histories=history,
show_bg_domain_button=BG_DOMAIN_UPDATE)
@dashboard_bp.route('/domains-updater', methods=['GET', 'POST'])
@login_required
def domains_updater():
current_app.logger.debug('Update domains in background')
d = Domain().update()
response_data = {
"result": d,
}
return jsonify(response_data)

View File

@@ -0,0 +1,525 @@
import re
import json
import traceback
import datetime
from distutils.version import StrictVersion
from flask import Blueprint, render_template, make_response, url_for, current_app, g, session, request, redirect, abort, jsonify
from flask_login import login_user, login_required, current_user
from .base import login_manager
from ..decorators import can_create_domain, operator_role_required, can_access_domain, can_configure_dnssec
from ..models.user import User, Anonymous
from ..models.account import Account
from ..models.setting import Setting
from ..models.history import History
from ..models.domain import Domain
from ..models.record import Record
from ..models.record_entry import RecordEntry
from ..models.domain_template import DomainTemplate
from ..models.domain_template_record import DomainTemplateRecord
from ..models.domain_setting import DomainSetting
domain_bp = Blueprint('domain',
__name__,
template_folder='templates',
url_prefix='/domain')
@domain_bp.route('/<path:domain_name>', methods=['GET'])
@login_required
@can_access_domain
def domain(domain_name):
r = Record()
domain = Domain.query.filter(Domain.name == domain_name).first()
if not domain:
abort(404)
# query domain info from PowerDNS API
zone_info = r.get_record_data(domain.name)
if zone_info:
jrecords = zone_info['records']
else:
# can not get any record, API server might be down
abort(500)
quick_edit = Setting().get('record_quick_edit')
records_allow_to_edit = Setting().get_records_allow_to_edit()
forward_records_allow_to_edit = Setting(
).get_forward_records_allow_to_edit()
reverse_records_allow_to_edit = Setting(
).get_reverse_records_allow_to_edit()
ttl_options = Setting().get_ttl_options()
records = []
if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'):
for jr in jrecords:
if jr['type'] in records_allow_to_edit:
for subrecord in jr['records']:
record = RecordEntry(name=jr['name'],
type=jr['type'],
status='Disabled' if
subrecord['disabled'] else 'Active',
ttl=jr['ttl'],
data=subrecord['content'],
is_allowed_edit=True)
records.append(record)
if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name):
editable_records = forward_records_allow_to_edit
else:
editable_records = reverse_records_allow_to_edit
return render_template('domain.html',
domain=domain,
records=records,
editable_records=editable_records,
quick_edit=quick_edit,
ttl_options=ttl_options)
else:
for jr in jrecords:
if jr['type'] in records_allow_to_edit:
record = RecordEntry(
name=jr['name'],
type=jr['type'],
status='Disabled' if jr['disabled'] else 'Active',
ttl=jr['ttl'],
data=jr['content'],
is_allowed_edit=True)
records.append(record)
if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name):
editable_records = forward_records_allow_to_edit
else:
editable_records = reverse_records_allow_to_edit
return render_template('domain.html',
domain=domain,
records=records,
editable_records=editable_records,
quick_edit=quick_edit,
ttl_options=ttl_options)
@domain_bp.route('/add', methods=['GET', 'POST'])
@login_required
@can_create_domain
def add():
templates = DomainTemplate.query.all()
if request.method == 'POST':
try:
domain_name = request.form.getlist('domain_name')[0]
domain_type = request.form.getlist('radio_type')[0]
domain_template = request.form.getlist('domain_template')[0]
soa_edit_api = request.form.getlist('radio_type_soa_edit_api')[0]
account_id = request.form.getlist('accountid')[0]
if ' ' in domain_name or not domain_name or not domain_type:
return render_template('errors/400.html',
msg="Please correct your input"), 400
if domain_type == 'slave':
if request.form.getlist('domain_master_address'):
domain_master_string = request.form.getlist(
'domain_master_address')[0]
domain_master_string = domain_master_string.replace(
' ', '')
domain_master_ips = domain_master_string.split(',')
else:
domain_master_ips = []
account_name = Account().get_name_by_id(account_id)
d = Domain()
result = d.add(domain_name=domain_name,
domain_type=domain_type,
soa_edit_api=soa_edit_api,
domain_master_ips=domain_master_ips,
account_name=account_name)
if result['status'] == 'ok':
history = History(msg='Add domain {0}'.format(domain_name),
detail=str({
'domain_type': domain_type,
'domain_master_ips': domain_master_ips,
'account_id': account_id
}),
created_by=current_user.username)
history.add()
# grant user access to the domain
Domain(name=domain_name).grant_privileges(
[current_user.id])
# apply template if needed
if domain_template != '0':
template = DomainTemplate.query.filter(
DomainTemplate.id == domain_template).first()
template_records = DomainTemplateRecord.query.filter(
DomainTemplateRecord.template_id ==
domain_template).all()
record_data = []
for template_record in template_records:
record_row = {
'record_data': template_record.data,
'record_name': template_record.name,
'record_status': template_record.status,
'record_ttl': template_record.ttl,
'record_type': template_record.type
}
record_data.append(record_row)
r = Record()
result = r.apply(domain_name, record_data)
if result['status'] == 'ok':
history = History(
msg=
'Applying template {0} to {1}, created records successfully.'
.format(template.name, domain_name),
detail=str(result),
created_by=current_user.username)
history.add()
else:
history = History(
msg=
'Applying template {0} to {1}, FAILED to created records.'
.format(template.name, domain_name),
detail=str(result),
created_by=current_user.username)
history.add()
return redirect(url_for('dashboard.dashboard'))
else:
return render_template('errors/400.html',
msg=result['msg']), 400
except Exception as e:
current_app.logger.error('Cannot add domain. Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
abort(500)
else:
accounts = Account.query.all()
return render_template('domain_add.html',
templates=templates,
accounts=accounts)
@domain_bp.route('/setting/<path:domain_name>/delete', methods=['POST'])
@login_required
@operator_role_required
def delete(domain_name):
d = Domain()
result = d.delete(domain_name)
if result['status'] == 'error':
abort(500)
history = History(msg='Delete domain {0}'.format(domain_name),
created_by=current_user.username)
history.add()
return redirect(url_for('dashboard.dashboard'))
@domain_bp.route('/setting/<path:domain_name>/manage', methods=['GET', 'POST'])
@login_required
@operator_role_required
def setting(domain_name):
if request.method == 'GET':
domain = Domain.query.filter(Domain.name == domain_name).first()
if not domain:
abort(404)
users = User.query.all()
accounts = Account.query.all()
# get list of user ids to initialize selection data
d = Domain(name=domain_name)
domain_user_ids = d.get_user()
account = d.get_account()
return render_template('domain_setting.html',
domain=domain,
users=users,
domain_user_ids=domain_user_ids,
accounts=accounts,
domain_account=account)
if request.method == 'POST':
# username in right column
new_user_list = request.form.getlist('domain_multi_user[]')
new_user_ids = [
user.id for user in User.query.filter(
User.username.in_(new_user_list)).all() if user
]
# grant/revoke user privileges
d = Domain(name=domain_name)
d.grant_privileges(new_user_ids)
history = History(
msg='Change domain {0} access control'.format(domain_name),
detail=str({'user_has_access': new_user_list}),
created_by=current_user.username)
history.add()
return redirect(url_for('domain.setting', domain_name=domain_name))
@domain_bp.route('/setting/<path:domain_name>/change_soa_setting',
methods=['POST'])
@login_required
@operator_role_required
def change_soa_edit_api(domain_name):
domain = Domain.query.filter(Domain.name == domain_name).first()
if not domain:
abort(404)
new_setting = request.form.get('soa_edit_api')
if new_setting is None:
abort(500)
if new_setting == '0':
return redirect(url_for('domain.setting', domain_name=domain_name))
d = Domain()
status = d.update_soa_setting(domain_name=domain_name,
soa_edit_api=new_setting)
if status['status'] != None:
users = User.query.all()
accounts = Account.query.all()
d = Domain(name=domain_name)
domain_user_ids = d.get_user()
account = d.get_account()
return render_template('domain_setting.html',
domain=domain,
users=users,
domain_user_ids=domain_user_ids,
accounts=accounts,
domain_account=account)
else:
abort(500)
@domain_bp.route('/setting/<path:domain_name>/change_account',
methods=['POST'])
@login_required
@operator_role_required
def change_account(domain_name):
domain = Domain.query.filter(Domain.name == domain_name).first()
if not domain:
abort(404)
account_id = request.form.get('accountid')
status = Domain(name=domain.name).assoc_account(account_id)
if status['status']:
return redirect(url_for('domain.setting', domain_name=domain.name))
else:
abort(500)
@domain_bp.route('/<path:domain_name>/apply',
methods=['POST'],
strict_slashes=False)
@login_required
@can_access_domain
def record_apply(domain_name):
#TODO: filter removed records / name modified records.
try:
jdata = request.json
submitted_serial = jdata['serial']
submitted_record = jdata['record']
domain = Domain.query.filter(Domain.name == domain_name).first()
current_app.logger.debug(
'Your submitted serial: {0}'.format(submitted_serial))
current_app.logger.debug('Current domain serial: {0}'.format(
domain.serial))
if domain:
if int(submitted_serial) != domain.serial:
return make_response(
jsonify({
'status':
'error',
'msg':
'The zone has been changed by another session or user. Please refresh this web page to load updated records.'
}), 500)
else:
return make_response(
jsonify({
'status':
'error',
'msg':
'Domain name {0} does not exist'.format(domain_name)
}), 404)
r = Record()
result = r.apply(domain_name, submitted_record)
if result['status'] == 'ok':
jdata.pop('_csrf_token',
None) # don't store csrf token in the history.
history = History(
msg='Apply record changes to domain {0}'.format(domain_name),
detail=str(json.dumps(jdata)),
created_by=current_user.username)
history.add()
return make_response(jsonify(result), 200)
else:
return make_response(jsonify(result), 400)
except Exception as e:
current_app.logger.error(
'Cannot apply record changes. Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
return make_response(
jsonify({
'status': 'error',
'msg': 'Error when applying new changes'
}), 500)
@domain_bp.route('/<path:domain_name>/update',
methods=['POST'],
strict_slashes=False)
@login_required
@can_access_domain
def record_update(domain_name):
"""
This route is used for domain work as Slave Zone only
Pulling the records update from its Master
"""
try:
jdata = request.json
domain_name = jdata['domain']
d = Domain()
result = d.update_from_master(domain_name)
if result['status'] == 'ok':
return make_response(
jsonify({
'status': 'ok',
'msg': result['msg']
}), 200)
else:
return make_response(
jsonify({
'status': 'error',
'msg': result['msg']
}), 500)
except Exception as e:
current_app.logger.error('Cannot update record. Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
return make_response(
jsonify({
'status': 'error',
'msg': 'Error when applying new changes'
}), 500)
@domain_bp.route('/<path:domain_name>/info', methods=['GET'])
@login_required
@can_access_domain
def info(domain_name):
domain = Domain()
domain_info = domain.get_domain_info(domain_name)
return make_response(jsonify(domain_info), 200)
@domain_bp.route('/<path:domain_name>/dnssec', methods=['GET'])
@login_required
@can_access_domain
def dnssec(domain_name):
domain = Domain()
dnssec = domain.get_domain_dnssec(domain_name)
return make_response(jsonify(dnssec), 200)
@domain_bp.route('/<path:domain_name>/dnssec/enable', methods=['POST'])
@login_required
@can_access_domain
@can_configure_dnssec
def dnssec_enable(domain_name):
domain = Domain()
dnssec = domain.enable_domain_dnssec(domain_name)
return make_response(jsonify(dnssec), 200)
@domain_bp.route('/<path:domain_name>/dnssec/disable', methods=['POST'])
@login_required
@can_access_domain
@can_configure_dnssec
def dnssec_disable(domain_name):
domain = Domain()
dnssec = domain.get_domain_dnssec(domain_name)
for key in dnssec['dnssec']:
domain.delete_dnssec_key(domain_name, key['id'])
return make_response(jsonify({'status': 'ok', 'msg': 'DNSSEC removed.'}))
@domain_bp.route('/<path:domain_name>/manage-setting', methods=['GET', 'POST'])
@login_required
@operator_role_required
def admin_setdomainsetting(domain_name):
if request.method == 'POST':
#
# post data should in format
# {'action': 'set_setting', 'setting': 'default_action, 'value': 'True'}
#
try:
jdata = request.json
data = jdata['data']
if jdata['action'] == 'set_setting':
new_setting = data['setting']
new_value = str(data['value'])
domain = Domain.query.filter(
Domain.name == domain_name).first()
setting = DomainSetting.query.filter(
DomainSetting.domain == domain).filter(
DomainSetting.setting == new_setting).first()
if setting:
if setting.set(new_value):
history = History(
msg='Setting {0} changed value to {1} for {2}'.
format(new_setting, new_value, domain.name),
created_by=current_user.username)
history.add()
return make_response(
jsonify({
'status': 'ok',
'msg': 'Setting updated.'
}))
else:
return make_response(
jsonify({
'status': 'error',
'msg': 'Unable to set value of setting.'
}))
else:
if domain.add_setting(new_setting, new_value):
history = History(
msg=
'New setting {0} with value {1} for {2} has been created'
.format(new_setting, new_value, domain.name),
created_by=current_user.username)
history.add()
return make_response(
jsonify({
'status': 'ok',
'msg': 'New setting created and updated.'
}))
else:
return make_response(
jsonify({
'status': 'error',
'msg': 'Unable to create new setting.'
}))
else:
return make_response(
jsonify({
'status': 'error',
'msg': 'Action not supported.'
}), 400)
except Exception as e:
current_app.logger.error(
'Cannot change domain setting. Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
return make_response(
jsonify({
'status':
'error',
'msg':
'There is something wrong, please contact Administrator.'
}), 400)

View File

@@ -0,0 +1,686 @@
import os
import json
import traceback
import datetime
import ipaddress
from distutils.util import strtobool
from yaml import Loader, load
from onelogin.saml2.utils import OneLogin_Saml2_Utils
from flask import Blueprint, render_template, make_response, url_for, current_app, g, session, request, redirect, abort
from flask_login import login_user, logout_user, login_required, current_user
from .base import login_manager
from ..lib import utils
from ..decorators import dyndns_login_required
from ..models.base import db
from ..models.user import User, Anonymous
from ..models.role import Role
from ..models.account import Account
from ..models.account_user import AccountUser
from ..models.domain import Domain
from ..models.domain_user import DomainUser
from ..models.domain_setting import DomainSetting
from ..models.record import Record
from ..models.setting import Setting
from ..models.history import History
from ..services.google import google_oauth
from ..services.github import github_oauth
from ..services.oidc import oidc_oauth
google = None
github = None
oidc = None
index_bp = Blueprint('index',
__name__,
template_folder='templates',
url_prefix='/')
@index_bp.before_app_first_request
def register_modules():
global google
global github
global oidc
google = google_oauth()
github = github_oauth()
oidc = oidc_oauth()
@index_bp.before_request
def before_request():
# Check if user is anonymous
g.user = current_user
login_manager.anonymous_user = Anonymous
# Check site is in maintenance mode
maintenance = Setting().get('maintenance')
if maintenance and current_user.is_authenticated and current_user.role.name not in [
'Administrator', 'Operator'
]:
return render_template('maintenance.html')
# Manage session timeout
session.permanent = True
current_app.permanent_session_lifetime = datetime.timedelta(
minutes=int(Setting().get('session_timeout')))
session.modified = True
@index_bp.route('/', methods=['GET'])
@login_required
def index():
return redirect(url_for('dashboard.dashboard'))
@index_bp.route('/google/login')
def google_login():
if not Setting().get('google_oauth_enabled') or google is None:
current_app.logger.error(
'Google OAuth is disabled or you have not yet reloaded the pda application after enabling.'
)
abort(400)
else:
redirect_uri = url_for('google_authorized', _external=True)
return google.authorize_redirect(redirect_uri)
@index_bp.route('/github/login')
def github_login():
if not Setting().get('github_oauth_enabled') or github is None:
current_app.logger.error(
'Github OAuth is disabled or you have not yet reloaded the pda application after enabling.'
)
abort(400)
else:
redirect_uri = url_for('github_authorized', _external=True)
return github.authorize_redirect(redirect_uri)
@index_bp.route('/oidc/login')
def oidc_login():
if not Setting().get('oidc_oauth_enabled') or oidc is None:
current_app.logger.error(
'OIDC OAuth is disabled or you have not yet reloaded the pda application after enabling.'
)
abort(400)
else:
redirect_uri = url_for('oidc_authorized', _external=True)
return oidc.authorize_redirect(redirect_uri)
@index_bp.route('/login', methods=['GET', 'POST'])
def login():
SAML_ENABLED = current_app.config.get('SAML_ENABLED')
if g.user is not None and current_user.is_authenticated:
return redirect(url_for('dashboard.dashboard'))
if 'google_token' in session:
user_data = json.loads(google.get('userinfo').text)
first_name = user_data['given_name']
surname = user_data['family_name']
email = user_data['email']
user = User.query.filter_by(username=email).first()
if user is None:
user = User.query.filter_by(email=email).first()
if not user:
user = User(username=email,
firstname=first_name,
lastname=surname,
plain_text_password=None,
email=email)
result = user.create_local_user()
if not result['status']:
session.pop('google_token', None)
return redirect(url_for('index.login'))
session['user_id'] = user.id
login_user(user, remember=False)
session['authentication_type'] = 'OAuth'
return redirect(url_for('index.index'))
if 'github_token' in session:
me = json.loads(github.get('user').text)
github_username = me['login']
github_name = me['name']
github_email = me['email']
user = User.query.filter_by(username=github_username).first()
if user is None:
user = User.query.filter_by(email=github_email).first()
if not user:
user = User(username=github_username,
plain_text_password=None,
firstname=github_name,
lastname='',
email=github_email)
result = user.create_local_user()
if not result['status']:
session.pop('github_token', None)
return redirect(url_for('index.login'))
session['user_id'] = user.id
session['authentication_type'] = 'OAuth'
login_user(user, remember=False)
return redirect(url_for('index.index'))
if 'oidc_token' in session:
me = json.loads(oidc.get('userinfo').text)
oidc_username = me["preferred_username"]
oidc_givenname = me["name"]
oidc_familyname = ""
oidc_email = me["email"]
user = User.query.filter_by(username=oidc_username).first()
if not user:
user = User(username=oidc_username,
plain_text_password=None,
firstname=oidc_givenname,
lastname=oidc_familyname,
email=oidc_email)
result = user.create_local_user()
if not result['status']:
session.pop('oidc_token', None)
return redirect(url_for('index.login'))
session['user_id'] = user.id
session['authentication_type'] = 'OAuth'
login_user(user, remember=False)
return redirect(url_for('index.index'))
if request.method == 'GET':
return render_template('login.html', saml_enabled=SAML_ENABLED)
elif request.method == 'POST':
# process Local-DB authentication
username = request.form['username']
password = request.form['password']
otp_token = request.form.get('otptoken')
auth_method = request.form.get('auth_method', 'LOCAL')
session[
'authentication_type'] = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL'
remember_me = True if 'remember' in request.form else False
user = User(username=username,
password=password,
plain_text_password=password)
try:
auth = user.is_validate(method=auth_method,
src_ip=request.remote_addr)
if auth == False:
return render_template('login.html',
saml_enabled=SAML_ENABLED,
error='Invalid credentials')
except Exception as e:
current_app.logger.error(
"Cannot authenticate user. Error: {}".format(e))
current_app.logger.debug(traceback.format_exc())
return render_template('login.html',
saml_enabled=SAML_ENABLED,
error=e)
# check if user enabled OPT authentication
if user.otp_secret:
if otp_token and otp_token.isdigit():
good_token = user.verify_totp(otp_token)
if not good_token:
return render_template('login.html',
saml_enabled=SAML_ENABLED,
error='Invalid credentials')
else:
return render_template('login.html',
saml_enabled=SAML_ENABLED,
error='Token required')
login_user(user, remember=remember_me)
return redirect(session.get('next', url_for('index.index')))
def clear_session():
session.pop('user_id', None)
session.pop('github_token', None)
session.pop('google_token', None)
session.pop('authentication_type', None)
session.clear()
logout_user()
@index_bp.route('/logout')
def logout():
if current_app.config.get(
'SAML_ENABLED'
) and 'samlSessionIndex' in session and current_app.config.get(
'SAML_LOGOUT'):
req = utils.prepare_flask_request(request)
auth = utils.init_saml_auth(req)
if current_app.config.get('SAML_LOGOUT_URL'):
return redirect(
auth.logout(
name_id_format=
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
return_to=current_app.config.get('SAML_LOGOUT_URL'),
session_index=session['samlSessionIndex'],
name_id=session['samlNameId']))
return redirect(
auth.logout(
name_id_format=
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
session_index=session['samlSessionIndex'],
name_id=session['samlNameId']))
clear_session()
return redirect(url_for('index.login'))
@index_bp.route('/register', methods=['GET', 'POST'])
def register():
if Setting().get('signup_enabled'):
if request.method == 'GET':
return render_template('register.html')
elif request.method == 'POST':
username = request.form['username']
password = request.form['password']
firstname = request.form.get('firstname')
lastname = request.form.get('lastname')
email = request.form.get('email')
rpassword = request.form.get('rpassword')
if not username or not password or not email:
return render_template(
'register.html', error='Please input required information')
if password != rpassword:
return render_template(
'register.html',
error="Password confirmation does not match")
user = User(username=username,
plain_text_password=password,
firstname=firstname,
lastname=lastname,
email=email)
try:
result = user.create_local_user()
if result and result['status']:
return redirect(url_for('index.login'))
else:
return render_template('register.html',
error=result['msg'])
except Exception as e:
return render_template('register.html', error=e)
else:
return render_template('errors/404.html'), 404
@index_bp.route('/nic/checkip.html', methods=['GET', 'POST'])
def dyndns_checkip():
# This route covers the default ddclient 'web' setting for the checkip service
return render_template('dyndns.html',
response=request.environ.get(
'HTTP_X_REAL_IP', request.remote_addr))
@index_bp.route('/nic/update', methods=['GET', 'POST'])
@dyndns_login_required
def dyndns_update():
# dyndns protocol response codes in use are:
# good: update successful
# nochg: IP address already set to update address
# nohost: hostname does not exist for this user account
# 911: server error
# have to use 200 HTTP return codes because ddclient does not read the return string if the code is other than 200
# reference: https://help.dyn.com/remote-access-api/perform-update/
# reference: https://help.dyn.com/remote-access-api/return-codes/
hostname = request.args.get('hostname')
myip = request.args.get('myip')
if not hostname:
history = History(msg="DynDNS update: missing hostname parameter",
created_by=current_user.username)
history.add()
return render_template('dyndns.html', response='nohost'), 200
try:
if current_user.role.name in ['Administrator', 'Operator']:
domains = Domain.query.all()
else:
# Get query for domain to which the user has access permission.
# This includes direct domain permission AND permission through
# account membership
domains = 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 == current_user.id,
AccountUser.user_id == current_user.id
)).all()
except Exception as e:
current_app.logger.error('DynDNS Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
return render_template('dyndns.html', response='911'), 200
domain = None
domain_segments = hostname.split('.')
for index in range(len(domain_segments)):
full_domain = '.'.join(domain_segments)
potential_domain = Domain.query.filter(
Domain.name == full_domain).first()
if potential_domain in domains:
domain = potential_domain
break
domain_segments.pop(0)
if not domain:
history = History(
msg=
"DynDNS update: attempted update of {0} but it does not exist for this user"
.format(hostname),
created_by=current_user.username)
history.add()
return render_template('dyndns.html', response='nohost'), 200
myip_addr = []
if myip:
for address in myip.split(','):
myip_addr += utils.validate_ipaddress(address)
remote_addr = utils.validate_ipaddress(
request.headers.get('X-Forwarded-For',
request.remote_addr).split(', ')[:1])
response = 'nochg'
for ip in myip_addr or remote_addr:
if isinstance(ip, ipaddress.IPv4Address):
rtype = 'A'
else:
rtype = 'AAAA'
r = Record(name=hostname, type=rtype)
# Check if the user requested record exists within this domain
if r.exists(domain.name) and r.is_allowed_edit():
if r.data == str(ip):
# Record content did not change, return 'nochg'
history = History(
msg=
"DynDNS update: attempted update of {0} but record did not change"
.format(hostname),
created_by=current_user.username)
history.add()
else:
oldip = r.data
result = r.update(domain.name, str(ip))
if result['status'] == 'ok':
history = History(
msg=
'DynDNS update: updated {0} record {1} in zone {2}, it changed from {3} to {4}'
.format(rtype, hostname, domain.name, oldip, str(ip)),
detail=str(result),
created_by=current_user.username)
history.add()
response = 'good'
else:
response = '911'
break
elif r.is_allowed_edit():
ondemand_creation = DomainSetting.query.filter(
DomainSetting.domain == domain).filter(
DomainSetting.setting == 'create_via_dyndns').first()
if (ondemand_creation is not None) and (strtobool(
ondemand_creation.value) == True):
record = Record(name=hostname,
type=rtype,
data=str(ip),
status=False,
ttl=3600)
result = record.add(domain.name)
if result['status'] == 'ok':
history = History(
msg=
'DynDNS update: created record {0} in zone {1}, it now represents {2}'
.format(hostname, domain.name, str(ip)),
detail=str(result),
created_by=current_user.username)
history.add()
response = 'good'
else:
history = History(
msg=
'DynDNS update: attempted update of {0} but it does not exist for this user'
.format(hostname),
created_by=current_user.username)
history.add()
return render_template('dyndns.html', response=response), 200
### START SAML AUTHENTICATION ###
@index_bp.route('/saml/login')
def saml_login():
if not current_app.config.get('SAML_ENABLED'):
abort(400)
req = utils.prepare_flask_request(request)
auth = utils.init_saml_auth(req)
redirect_url = OneLogin_Saml2_Utils.get_self_url(req) + url_for(
'saml_authorized')
return redirect(auth.login(return_to=redirect_url))
@index_bp.route('/saml/metadata')
def saml_metadata():
if not current_app.config.get('SAML_ENABLED'):
current_app.logger.error("SAML authentication is disabled.")
abort(400)
req = utils.prepare_flask_request(request)
auth = utils.init_saml_auth(req)
settings = auth.get_settings()
metadata = settings.get_sp_metadata()
errors = settings.validate_metadata(metadata)
if len(errors) == 0:
resp = make_response(metadata, 200)
resp.headers['Content-Type'] = 'text/xml'
else:
resp = make_response(errors.join(', '), 500)
return resp
@index_bp.route('/saml/authorized', methods=['GET', 'POST'])
def saml_authorized():
errors = []
if not current_app.config.get('SAML_ENABLED'):
current_app.logger.error("SAML authentication is disabled.")
abort(400)
req = utils.prepare_flask_request(request)
auth = utils.init_saml_auth(req)
auth.process_response()
errors = auth.get_errors()
if len(errors) == 0:
session['samlUserdata'] = auth.get_attributes()
session['samlNameId'] = auth.get_nameid()
session['samlSessionIndex'] = auth.get_session_index()
self_url = OneLogin_Saml2_Utils.get_self_url(req)
self_url = self_url + req['script_name']
if 'RelayState' in request.form and self_url != request.form[
'RelayState']:
return redirect(auth.redirect_to(request.form['RelayState']))
if current_app.config.get('SAML_ATTRIBUTE_USERNAME', False):
username = session['samlUserdata'][
current_app.config['SAML_ATTRIBUTE_USERNAME']][0].lower()
else:
username = session['samlNameId'].lower()
user = User.query.filter_by(username=username).first()
if not user:
# create user
user = User(username=username,
plain_text_password=None,
email=session['samlNameId'])
user.create_local_user()
session['user_id'] = user.id
email_attribute_name = current_app.config.get('SAML_ATTRIBUTE_EMAIL',
'email')
givenname_attribute_name = current_app.config.get(
'SAML_ATTRIBUTE_GIVENNAME', 'givenname')
surname_attribute_name = current_app.config.get(
'SAML_ATTRIBUTE_SURNAME', 'surname')
name_attribute_name = current_app.config.get('SAML_ATTRIBUTE_NAME',
None)
account_attribute_name = current_app.config.get(
'SAML_ATTRIBUTE_ACCOUNT', None)
admin_attribute_name = current_app.config.get('SAML_ATTRIBUTE_ADMIN',
None)
group_attribute_name = current_app.config.get('SAML_ATTRIBUTE_GROUP',
None)
admin_group_name = current_app.config.get('SAML_GROUP_ADMIN_NAME',
None)
group_to_account_mapping = create_group_to_account_mapping()
if email_attribute_name in session['samlUserdata']:
user.email = session['samlUserdata'][email_attribute_name][
0].lower()
if givenname_attribute_name in session['samlUserdata']:
user.firstname = session['samlUserdata'][givenname_attribute_name][
0]
if surname_attribute_name in session['samlUserdata']:
user.lastname = session['samlUserdata'][surname_attribute_name][0]
if name_attribute_name in session['samlUserdata']:
name = session['samlUserdata'][name_attribute_name][0].split(' ')
user.firstname = name[0]
user.lastname = ' '.join(name[1:])
if group_attribute_name:
user_groups = session['samlUserdata'].get(group_attribute_name, [])
else:
user_groups = []
if admin_attribute_name or group_attribute_name:
user_accounts = set(user.get_account())
saml_accounts = []
for group_mapping in group_to_account_mapping:
mapping = group_mapping.split('=')
group = mapping[0]
account_name = mapping[1]
if group in user_groups:
account = handle_account(account_name)
saml_accounts.append(account)
for account_name in session['samlUserdata'].get(
account_attribute_name, []):
account = handle_account(account_name)
saml_accounts.append(account)
saml_accounts = set(saml_accounts)
for account in saml_accounts - user_accounts:
account.add_user(user)
history = History(msg='Adding {0} to account {1}'.format(
user.username, account.name),
created_by='SAML Assertion')
history.add()
for account in user_accounts - saml_accounts:
account.remove_user(user)
history = History(msg='Removing {0} from account {1}'.format(
user.username, account.name),
created_by='SAML Assertion')
history.add()
if admin_attribute_name and 'true' in session['samlUserdata'].get(
admin_attribute_name, []):
uplift_to_admin(user)
elif admin_group_name in user_groups:
uplift_to_admin(user)
elif admin_attribute_name or group_attribute_name:
if user.role.name != 'User':
user.role_id = Role.query.filter_by(name='User').first().id
history = History(msg='Demoting {0} to user'.format(
user.username),
created_by='SAML Assertion')
history.add()
user.plain_text_password = None
user.update_profile()
session['authentication_type'] = 'SAML'
login_user(user, remember=False)
return redirect(url_for('index'))
else:
return render_template('errors/SAML.html', errors=errors)
def create_group_to_account_mapping():
group_to_account_mapping_string = current_app.config.get(
'SAML_GROUP_TO_ACCOUNT_MAPPING', None)
if group_to_account_mapping_string and len(
group_to_account_mapping_string.strip()) > 0:
group_to_account_mapping = group_to_account_mapping_string.split(',')
else:
group_to_account_mapping = []
return group_to_account_mapping
def handle_account(account_name):
clean_name = ''.join(c for c in account_name.lower()
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
if len(clean_name) > Account.name.type.length:
logging.error(
"Account name {0} too long. Truncated.".format(clean_name))
account = Account.query.filter_by(name=clean_name).first()
if not account:
account = Account(name=clean_name.lower(),
description='',
contact='',
mail='')
account.create_account()
history = History(msg='Account {0} created'.format(account.name),
created_by='SAML Assertion')
history.add()
return account
def uplift_to_admin(user):
if user.role.name != 'Administrator':
user.role_id = Role.query.filter_by(name='Administrator').first().id
history = History(msg='Promoting {0} to administrator'.format(
user.username),
created_by='SAML Assertion')
history.add()
@index_bp.route('/saml/sls')
def saml_logout():
req = utils.prepare_flask_request(request)
auth = utils.init_saml_auth(req)
url = auth.process_slo()
errors = auth.get_errors()
if len(errors) == 0:
clear_session()
if url is not None:
return redirect(url)
elif current_app.config.get('SAML_LOGOUT_URL') is not None:
return redirect(current_app.config.get('SAML_LOGOUT_URL'))
else:
return redirect(url_for('login'))
else:
return render_template('errors/SAML.html', errors=errors)
### END SAML AUTHENTICATION ###
@index_bp.route('/swagger', methods=['GET'])
def swagger_spec():
try:
spec_path = os.path.join(current_app.root_path, "swagger-spec.yaml")
spec = open(spec_path, 'r')
loaded_spec = load(spec.read(), Loader)
except Exception as e:
current_app.logger.error(
'Cannot view swagger spec. Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
abort(500)
resp = make_response(json.dumps(loaded_spec), 200)
resp.headers['Content-Type'] = 'application/json'
return resp

View File

@@ -0,0 +1,89 @@
import qrcode as qrc
import qrcode.image.svg as qrc_svg
from io import BytesIO
from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, current_app, session, g
from flask_login import current_user, login_user, logout_user, login_required
from .base import login_manager
from ..models.user import User
from ..models.role import Role
user_bp = Blueprint('user',
__name__,
template_folder='templates',
url_prefix='/user')
@user_bp.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
if request.method == 'GET':
return render_template('user_profile.html')
if request.method == 'POST':
if session['authentication_type'] == 'LOCAL':
firstname = request.form[
'firstname'] if 'firstname' in request.form else ''
lastname = request.form[
'lastname'] if 'lastname' in request.form else ''
email = request.form['email'] if 'email' in request.form else ''
new_password = request.form[
'password'] if 'password' in request.form else ''
else:
firstname = lastname = email = new_password = ''
logging.warning(
'Authenticated externally. User {0} information will not allowed to update the profile'
.format(current_user.username))
if request.data:
jdata = request.json
data = jdata['data']
if jdata['action'] == 'enable_otp':
if session['authentication_type'] in ['LOCAL', 'LDAP']:
enable_otp = data['enable_otp']
user = User(username=current_user.username)
user.update_profile(enable_otp=enable_otp)
return make_response(
jsonify({
'status':
'ok',
'msg':
'Change OTP Authentication successfully. Status: {0}'
.format(enable_otp)
}), 200)
else:
return make_response(
jsonify({
'status':
'error',
'msg':
'User {0} is externally. You are not allowed to update the OTP'
.format(current_user.username)
}), 400)
user = User(username=current_user.username,
plain_text_password=new_password,
firstname=firstname,
lastname=lastname,
email=email,
reload_info=False)
user.update_profile()
return render_template('user_profile.html')
@user_bp.route('/qrcode')
@login_required
def qrcode():
if not current_user:
return redirect(url_for('index'))
img = qrc.make(current_user.get_totp_uri(),
image_factory=qrc_svg.SvgPathImage)
stream = BytesIO()
img.save(stream)
return stream.getvalue(), 200, {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}

View File

@@ -0,0 +1,4 @@
from .base import authlib_oauth_client
def init_app(app):
authlib_oauth_client.init_app(app)

View File

@@ -0,0 +1,3 @@
from authlib.flask.client import OAuth
authlib_oauth_client = OAuth()

View File

@@ -0,0 +1,42 @@
from flask import request, session, redirect, url_for, current_app
from .base import authlib_oauth_client
from ..models.setting import Setting
def github_oauth():
if not Setting().get('github_oauth_enabled'):
return None
def fetch_github_token():
return session.get('github_token')
def update_token(token):
session['google_token'] = token
return token
github = authlib_oauth_client.register(
'github',
client_id=Setting().get('github_oauth_key'),
client_secret=Setting().get('github_oauth_secret'),
request_token_params={'scope': Setting().get('github_oauth_scope')},
api_base_url=Setting().get('github_oauth_api_url'),
request_token_url=None,
access_token_url=Setting().get('github_oauth_token_url'),
authorize_url=Setting().get('github_oauth_authorize_url'),
client_kwargs={'scope': Setting().get('github_oauth_scope')},
fetch_token=fetch_github_token,
update_token=update_token)
@current_app.route('/github/authorized')
def github_authorized():
session['github_oauthredir'] = url_for('.github_authorized',
_external=True)
token = github.authorize_access_token()
if token is None:
return 'Access denied: reason=%s error=%s' % (
request.args['error'], request.args['error_description'])
session['github_token'] = (token)
return redirect(url_for('index.login'))
return github

View File

@@ -0,0 +1,44 @@
from flask import request, session, redirect, url_for, current_app
from .base import authlib_oauth_client
from ..models.setting import Setting
def google_oauth():
if not Setting().get('google_oauth_enabled'):
return None
def fetch_google_token():
return session.get('google_token')
def update_token(token):
session['google_token'] = token
return token
google = authlib_oauth_client.register(
'google',
client_id=Setting().get('google_oauth_client_id'),
client_secret=Setting().get('google_oauth_client_secret'),
api_base_url=Setting().get('google_base_url'),
request_token_url=None,
access_token_url=Setting().get('google_token_url'),
authorize_url=Setting().get('google_authorize_url'),
client_kwargs={'scope': Setting().get('google_oauth_scope')},
fetch_token=fetch_google_token,
update_token=update_token)
@current_app.route('/google/authorized')
def google_authorized():
session['google_oauthredir'] = url_for(
'.google_authorized', _external=True)
token = google.authorize_access_token()
if token is None:
return 'Access denied: reason=%s error=%s' % (
request.args['error_reason'],
request.args['error_description']
)
session['google_token'] = (token)
return redirect(url_for('index.login'))
return google

View File

@@ -0,0 +1,41 @@
from flask import request, session, redirect, url_for, current_app
from .base import authlib_oauth_client
from ..models.setting import Setting
def oidc_oauth():
if not Setting().get('oidc_oauth_enabled'):
return None
def fetch_oidc_token():
return session.get('oidc_token')
def update_token(token):
session['google_token'] = token
return token
oidc = authlib_oauth_client.register(
'oidc',
client_id=Setting().get('oidc_oauth_key'),
client_secret=Setting().get('oidc_oauth_secret'),
api_base_url=Setting().get('oidc_oauth_api_url'),
request_token_url=None,
access_token_url=Setting().get('oidc_oauth_token_url'),
authorize_url=Setting().get('oidc_oauth_authorize_url'),
client_kwargs={'scope': Setting().get('oidc_oauth_scope')},
fetch_token=fetch_oidc_token,
update_token=update_token)
@current_app.route('/oidc/authorized')
def oidc_authorized():
session['oidc_oauthredir'] = url_for('.oidc_authorized',
_external=True)
token = oidc.authorize_access_token()
if token is None:
return 'Access denied: reason=%s error=%s' % (
request.args['error'], request.args['error_description'])
session['oidc_token'] = (token)
return redirect(url_for('index.login'))
return oidc

View File

@@ -0,0 +1,37 @@
.length-break {
word-break: break-all !important;
}
table td {
font-family:'Roboto Mono' !important;
}
/*Fit default macbook screen*/
.small-box .icon {
font-size: 70px !important;
}
.btn {
padding: 5px 4px !important;
}
/* Pretty JSON */
.json-pre {
background-color: ghostwhite;
border: 1px solid silver;
padding: 10px 20px;
margin: 20px;
}
.json-key {
color: brown;
}
.json-value {
color: navy;
}
.json-string {
color: olive;
}
.user-footer {
background-color: #222d32 !important;
}

View File

@@ -0,0 +1,262 @@
var dnssecKeyList = []
function applyChanges(data, url, showResult, refreshPage) {
$.ajax({
type : "POST",
url : url,
data : JSON.stringify(data),// now data come in this function
contentType : "application/json; charset=utf-8",
crossDomain : true,
dataType : "json",
success : function(data, status, jqXHR) {
console.log("Applied changes successfully.")
if (showResult) {
var modal = $("#modal_success");
modal.find('.modal-body p').text("Applied changes successfully");
modal.modal('show');
}
if (refreshPage) {
location.reload(true);
}
},
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);
modal.find('.modal-body p').text(responseJson['msg']);
modal.modal('show');
}
});
}
function applyRecordChanges(data, domain) {
$.ajax({
type : "POST",
url : $SCRIPT_ROOT + '/domain/' + domain + '/apply',
data : JSON.stringify(data),// now data come in this function
contentType : "application/json; charset=utf-8",
crossDomain : true,
dataType : "json",
success : function(data, status, jqXHR) {
// update Apply button value
$.getJSON($SCRIPT_ROOT + '/domain/' + domain + '/info', function(data) {
$(".button_apply_changes").val(data['serial']);
});
console.log("Applied changes successfully.")
var modal = $("#modal_success");
modal.find('.modal-body p').text("Applied changes successfully");
modal.modal('show');
},
error : function(jqXHR, status) {
console.log(jqXHR);
var modal = $("#modal_error");
var responseJson = jQuery.parseJSON(jqXHR.responseText);
modal.find('.modal-body p').text(responseJson['msg']);
modal.modal('show');
}
});
}
function getTableData(table) {
// reformat - pretty format
var records = []
table.rows().every(function() {
var r = this.data();
var record = {}
record["record_name"] = r[0].trim();
record["record_type"] = r[1].trim();
record["record_status"] = r[2].trim();
record["record_ttl"] = r[3].trim();
record["record_data"] = r[4].trim();
records.push(record);
});
return records
}
function saveRow(oTable, nRow) {
var status = 'Disabled';
var jqInputs = $(oTable.row(nRow).node()).find("input");
var jqSelect = $(oTable.row(nRow).node()).find("select");
if (jqSelect[1].value == 'false') {
status = 'Active';
}
oTable.cell(nRow,0).data(jqInputs[0].value);
oTable.cell(nRow,1).data(jqSelect[0].value);
oTable.cell(nRow,2).data(status);
oTable.cell(nRow,3).data(jqSelect[2].value);
oTable.cell(nRow,4).data(jqInputs[1].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,5).data(button_edit);
oTable.cell(nRow,6).data(button_delete);
oTable.draw();
}
function restoreRow(oTable, nRow) {
var aData = oTable.row(nRow).data();
oTable.row(nRow).data(aData);
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[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 = '<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
if (aData[2] == 'Active'){
isDisabled = 'false';
}
SelectElement('record_type', aData[1]);
SelectElement('record_status', isDisabled);
SelectElement('record_ttl', aData[3]);
}
function SelectElement(elementID, valueToSelect)
{
var element = document.getElementById(elementID);
element.value = valueToSelect;
}
function enable_dns_sec(url, csrf_token) {
$.post(url, {'_csrf_token': csrf_token}, function(data) {
var modal = $("#modal_dnssec_info");
if (data['status'] == 'error'){
modal.find('.modal-body p').text(data['msg']);
}
else {
modal.modal('hide');
//location.reload();
window.location.reload(true);
}
}, 'json')
}
function getdnssec(url, domain){
$.getJSON(url, function(data) {
var dnssec_footer = '';
var modal = $("#modal_dnssec_info");
if (data['status'] == 'error'){
modal.find('.modal-body p').text(data['msg']);
}
else {
var dnssec_msg = '';
var dnssec = data['dnssec'];
if (dnssec.length == 0 && parseFloat(PDNS_VERSION) >= 4.1) {
dnssec_msg = '<h3>DNSSEC is disabled. Click on Enable to activate it.';
modal.find('.modal-body p').html(dnssec_msg);
dnssec_footer = '<button type="button" class="btn btn-flat btn-success button_dnssec_enable pull-left" id="'+domain+'">Enable</button><button type="button" class="btn btn-flat btn-default pull-right" data-dismiss="modal">Cancel</button>';
modal.find('.modal-footer ').html(dnssec_footer);
}
else {
if (parseFloat(PDNS_VERSION) >= 4.1) {
dnssec_footer = '<button type="button" class="btn btn-flat btn-danger button_dnssec_disable pull-left" id="'+domain+'">Disable DNSSEC</button><button type="button" class="btn btn-flat btn-default pull-right" data-dismiss="modal">Close</button>';
modal.find('.modal-footer ').html(dnssec_footer);
}
for (var i = 0; i < dnssec.length; i++) {
if (dnssec[i]['active']){
dnssec_msg += '<form>'+
'<h3><strong>'+dnssec[i]['keytype']+'</strong></h3>'+
'<strong>DNSKEY</strong>'+
'<input class="form-control" autocomplete="off" type="text" readonly="true" value="'+dnssec[i]['dnskey']+'">'+
'</form>'+
'<br/>';
if(dnssec[i]['ds']){
var dsList = dnssec[i]['ds'];
dnssec_msg += '<strong>DS</strong>';
for (var j = 0; j < dsList.length; j++){
dnssec_msg += '<input class="form-control" autocomplete="off" type="text" readonly="true" value="'+dsList[j]+'">';
}
}
dnssec_msg += '</form>';
}
}
}
modal.find('.modal-body p').html(dnssec_msg);
}
modal.modal('show');
});
}
function reload_domains(url) {
$.getJSON(url, function(data) {
$('#modal_bg_reload_content').html("<i class=\"fa fa-check\"></i> Finished: " + data['result']['msg']);
})
}
// pretty JSON
json_library = {
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>: ';
}
if (pVal){
r = r + (pVal[0] == '"' ? str : val) + pVal + '</span>';
}
return r + (pEnd || '');
},
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;
return JSON.stringify(jsonData, null, 3)
.replace(/&/g, '&amp;').replace(/\\"/g, '&quot;')
.replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(jsonLine, json_library.replacer);
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,394 @@
/*!
* Validator v0.11.9 for Bootstrap 3, by @1000hz
* Copyright 2017 Cina Saffary
* Licensed under http://opensource.org/licenses/MIT
*
* https://github.com/1000hz/bootstrap-validator
*/
+function ($) {
'use strict';
// VALIDATOR CLASS DEFINITION
// ==========================
function getValue($el) {
return $el.is('[type="checkbox"]') ? $el.prop('checked') :
$el.is('[type="radio"]') ? !!$('[name="' + $el.attr('name') + '"]:checked').length :
$el.is('select[multiple]') ? ($el.val() || []).length :
$el.val()
}
var Validator = function (element, options) {
this.options = options
this.validators = $.extend({}, Validator.VALIDATORS, options.custom)
this.$element = $(element)
this.$btn = $('button[type="submit"], input[type="submit"]')
.filter('[form="' + this.$element.attr('id') + '"]')
.add(this.$element.find('input[type="submit"], button[type="submit"]'))
this.update()
this.$element.on('input.bs.validator change.bs.validator focusout.bs.validator', $.proxy(this.onInput, this))
this.$element.on('submit.bs.validator', $.proxy(this.onSubmit, this))
this.$element.on('reset.bs.validator', $.proxy(this.reset, this))
this.$element.find('[data-match]').each(function () {
var $this = $(this)
var target = $this.attr('data-match')
$(target).on('input.bs.validator', function (e) {
getValue($this) && $this.trigger('input.bs.validator')
})
})
// run validators for fields with values, but don't clobber server-side errors
this.$inputs.filter(function () {
return getValue($(this)) && !$(this).closest('.has-error').length
}).trigger('focusout')
this.$element.attr('novalidate', true) // disable automatic native validation
}
Validator.VERSION = '0.11.9'
Validator.INPUT_SELECTOR = ':input:not([type="hidden"], [type="submit"], [type="reset"], button)'
Validator.FOCUS_OFFSET = 20
Validator.DEFAULTS = {
delay: 500,
html: false,
disable: true,
focus: true,
custom: {},
errors: {
match: 'Does not match',
minlength: 'Not long enough'
},
feedback: {
success: 'glyphicon-ok',
error: 'glyphicon-remove'
}
}
Validator.VALIDATORS = {
'native': function ($el) {
var el = $el[0]
if (el.checkValidity) {
return !el.checkValidity() && !el.validity.valid && (el.validationMessage || "error!")
}
},
'match': function ($el) {
var target = $el.attr('data-match')
return $el.val() !== $(target).val() && Validator.DEFAULTS.errors.match
},
'minlength': function ($el) {
var minlength = $el.attr('data-minlength')
return $el.val().length < minlength && Validator.DEFAULTS.errors.minlength
}
}
Validator.prototype.update = function () {
var self = this
this.$inputs = this.$element.find(Validator.INPUT_SELECTOR)
.add(this.$element.find('[data-validate="true"]'))
.not(this.$element.find('[data-validate="false"]')
.each(function () { self.clearErrors($(this)) })
)
this.toggleSubmit()
return this
}
Validator.prototype.onInput = function (e) {
var self = this
var $el = $(e.target)
var deferErrors = e.type !== 'focusout'
if (!this.$inputs.is($el)) return
this.validateInput($el, deferErrors).done(function () {
self.toggleSubmit()
})
}
Validator.prototype.validateInput = function ($el, deferErrors) {
var value = getValue($el)
var prevErrors = $el.data('bs.validator.errors')
if ($el.is('[type="radio"]')) $el = this.$element.find('input[name="' + $el.attr('name') + '"]')
var e = $.Event('validate.bs.validator', {relatedTarget: $el[0]})
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
var self = this
return this.runValidators($el).done(function (errors) {
$el.data('bs.validator.errors', errors)
errors.length
? deferErrors ? self.defer($el, self.showErrors) : self.showErrors($el)
: self.clearErrors($el)
if (!prevErrors || errors.toString() !== prevErrors.toString()) {
e = errors.length
? $.Event('invalid.bs.validator', {relatedTarget: $el[0], detail: errors})
: $.Event('valid.bs.validator', {relatedTarget: $el[0], detail: prevErrors})
self.$element.trigger(e)
}
self.toggleSubmit()
self.$element.trigger($.Event('validated.bs.validator', {relatedTarget: $el[0]}))
})
}
Validator.prototype.runValidators = function ($el) {
var errors = []
var deferred = $.Deferred()
$el.data('bs.validator.deferred') && $el.data('bs.validator.deferred').reject()
$el.data('bs.validator.deferred', deferred)
function getValidatorSpecificError(key) {
return $el.attr('data-' + key + '-error')
}
function getValidityStateError() {
var validity = $el[0].validity
return validity.typeMismatch ? $el.attr('data-type-error')
: validity.patternMismatch ? $el.attr('data-pattern-error')
: validity.stepMismatch ? $el.attr('data-step-error')
: validity.rangeOverflow ? $el.attr('data-max-error')
: validity.rangeUnderflow ? $el.attr('data-min-error')
: validity.valueMissing ? $el.attr('data-required-error')
: null
}
function getGenericError() {
return $el.attr('data-error')
}
function getErrorMessage(key) {
return getValidatorSpecificError(key)
|| getValidityStateError()
|| getGenericError()
}
$.each(this.validators, $.proxy(function (key, validator) {
var error = null
if ((getValue($el) || $el.attr('required')) &&
($el.attr('data-' + key) !== undefined || key == 'native') &&
(error = validator.call(this, $el))) {
error = getErrorMessage(key) || error
!~errors.indexOf(error) && errors.push(error)
}
}, this))
if (!errors.length && getValue($el) && $el.attr('data-remote')) {
this.defer($el, function () {
var data = {}
data[$el.attr('name')] = getValue($el)
$.get($el.attr('data-remote'), data)
.fail(function (jqXHR, textStatus, error) { errors.push(getErrorMessage('remote') || error) })
.always(function () { deferred.resolve(errors)})
})
} else deferred.resolve(errors)
return deferred.promise()
}
Validator.prototype.validate = function () {
var self = this
$.when(this.$inputs.map(function (el) {
return self.validateInput($(this), false)
})).then(function () {
self.toggleSubmit()
self.focusError()
})
return this
}
Validator.prototype.focusError = function () {
if (!this.options.focus) return
var $input = this.$element.find(".has-error:first :input")
if ($input.length === 0) return
$('html, body').animate({scrollTop: $input.offset().top - Validator.FOCUS_OFFSET}, 250)
$input.focus()
}
Validator.prototype.showErrors = function ($el) {
var method = this.options.html ? 'html' : 'text'
var errors = $el.data('bs.validator.errors')
var $group = $el.closest('.form-group')
var $block = $group.find('.help-block.with-errors')
var $feedback = $group.find('.form-control-feedback')
if (!errors.length) return
errors = $('<ul/>')
.addClass('list-unstyled')
.append($.map(errors, function (error) { return $('<li/>')[method](error) }))
$block.data('bs.validator.originalContent') === undefined && $block.data('bs.validator.originalContent', $block.html())
$block.empty().append(errors)
$group.addClass('has-error has-danger')
$group.hasClass('has-feedback')
&& $feedback.removeClass(this.options.feedback.success)
&& $feedback.addClass(this.options.feedback.error)
&& $group.removeClass('has-success')
}
Validator.prototype.clearErrors = function ($el) {
var $group = $el.closest('.form-group')
var $block = $group.find('.help-block.with-errors')
var $feedback = $group.find('.form-control-feedback')
$block.html($block.data('bs.validator.originalContent'))
$group.removeClass('has-error has-danger has-success')
$group.hasClass('has-feedback')
&& $feedback.removeClass(this.options.feedback.error)
&& $feedback.removeClass(this.options.feedback.success)
&& getValue($el)
&& $feedback.addClass(this.options.feedback.success)
&& $group.addClass('has-success')
}
Validator.prototype.hasErrors = function () {
function fieldErrors() {
return !!($(this).data('bs.validator.errors') || []).length
}
return !!this.$inputs.filter(fieldErrors).length
}
Validator.prototype.isIncomplete = function () {
function fieldIncomplete() {
var value = getValue($(this))
return !(typeof value == "string" ? $.trim(value) : value)
}
return !!this.$inputs.filter('[required]').filter(fieldIncomplete).length
}
Validator.prototype.onSubmit = function (e) {
this.validate()
if (this.isIncomplete() || this.hasErrors()) e.preventDefault()
}
Validator.prototype.toggleSubmit = function () {
if (!this.options.disable) return
this.$btn.toggleClass('disabled', this.isIncomplete() || this.hasErrors())
}
Validator.prototype.defer = function ($el, callback) {
callback = $.proxy(callback, this, $el)
if (!this.options.delay) return callback()
window.clearTimeout($el.data('bs.validator.timeout'))
$el.data('bs.validator.timeout', window.setTimeout(callback, this.options.delay))
}
Validator.prototype.reset = function () {
this.$element.find('.form-control-feedback')
.removeClass(this.options.feedback.error)
.removeClass(this.options.feedback.success)
this.$inputs
.removeData(['bs.validator.errors', 'bs.validator.deferred'])
.each(function () {
var $this = $(this)
var timeout = $this.data('bs.validator.timeout')
window.clearTimeout(timeout) && $this.removeData('bs.validator.timeout')
})
this.$element.find('.help-block.with-errors')
.each(function () {
var $this = $(this)
var originalContent = $this.data('bs.validator.originalContent')
$this
.removeData('bs.validator.originalContent')
.html(originalContent)
})
this.$btn.removeClass('disabled')
this.$element.find('.has-error, .has-danger, .has-success').removeClass('has-error has-danger has-success')
return this
}
Validator.prototype.destroy = function () {
this.reset()
this.$element
.removeAttr('novalidate')
.removeData('bs.validator')
.off('.bs.validator')
this.$inputs
.off('.bs.validator')
this.options = null
this.validators = null
this.$element = null
this.$btn = null
this.$inputs = null
return this
}
// VALIDATOR PLUGIN DEFINITION
// ===========================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var options = $.extend({}, Validator.DEFAULTS, $this.data(), typeof option == 'object' && option)
var data = $this.data('bs.validator')
if (!data && option == 'destroy') return
if (!data) $this.data('bs.validator', (data = new Validator(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.validator
$.fn.validator = Plugin
$.fn.validator.Constructor = Validator
// VALIDATOR NO CONFLICT
// =====================
$.fn.validator.noConflict = function () {
$.fn.validator = old
return this
}
// VALIDATOR DATA-API
// ==================
$(window).on('load', function () {
$('form[data-toggle="validator"]').each(function () {
var $form = $(this)
Plugin.call($form, $form.data())
})
})
}(jQuery);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,131 @@
{% 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.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li><a href="{{ url_for('admin.manage_account') }}">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.edit_account') }}{% else %}{{ url_for('admin.edit_account', account_name=account.name) }}{% endif %}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<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,166 @@
{% extends "base.html" %}
{% set active_page = "admin_users" %}
{% block title %}<title>Edit User - {{ 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.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li><a href="{{ url_for('admin.manage_user') }}">User</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.edit_user') }}{% else %}{{ url_for('admin.edit_user', user_username=user.username) }}{% endif %}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<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,
'_csrf_token': '{{ csrf_token() }}'
}
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manage-user', 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 events</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.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({'_csrf_token': '{{ csrf_token() }}'}, $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

@@ -0,0 +1,138 @@
{% extends "base.html" %}
{% set active_page = "admin_accounts" %}
{% block title %}
<title>Account Management - {{ SITE_NAME }}</title>
{% endblock %} {% block dashboard_stat %}
<section class="content-header">
<h1>
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 class="active">Accounts</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">Account Management</h3>
</div>
<div class="box-body">
<a href="{{ url_for('admin.edit_account') }}">
<button type="button" class="btn btn-flat btn-primary pull-left button_add_account">
Add Account&nbsp;<i class="fa fa-plus"></i>
</button>
</a>
</div>
<div class="box-body">
<table id="tbl_accounts" class="table table-bordered table-striped">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Contact</th>
<th>Mail</th>
<th>Member</th>
<th>Domain</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for account in accounts %}
<tr class="odd gradeX">
<td>{{ account.name }}</td>
<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) }}'">
Edit&nbsp;<i class="fa fa-cog"></i>
</button>
<button type="button" class="btn btn-flat btn-danger button_delete"
id="{{ account.name }}">
Delete&nbsp;<i class="fa fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</section>
{% 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"]
],
"pageLength": 10
});
// handle deletion of account
$(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 + "?";
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.modal('hide');
})
modal.modal('show');
});
</script>
{% endblock %}
{% block modals %}
<div class="modal fade modal-warning" id="modal_delete">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Confirmation</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-left" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-flat btn-danger" id="button_delete_confirm">Delete</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
{% endblock %}

View File

@@ -0,0 +1,205 @@
{% extends "base.html" %}
{% set active_page = "admin_users" %}
{% block title %}
<title>User Management - {{ SITE_NAME }}</title>
{% endblock %} {% block dashboard_stat %}
<section class="content-header">
<h1>
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 class="active">User</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">User Management</h3>
</div>
<div class="box-body">
<a href="{{ url_for('admin.edit_user') }}">
<button type="button" class="btn btn-flat btn-primary pull-left button_add_user">
Add User&nbsp;<i class="fa fa-plus"></i>
</button>
</a>
</div>
<div class="box-body">
<table id="tbl_users" class="table table-bordered table-striped">
<thead>
<tr>
<th>Username</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Role</th>
<th>Privileges</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr class="odd gradeX">
<td>{{ user.username }}</td>
<td>{{ user.firstname }}</td>
<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 %}>
{% for role in roles %}
<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 %}>
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 %}>
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 %}>
Delete&nbsp;<i class="fa fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</section>
{% 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"]
],
"pageLength": 10
});
// handle revocation of privileges
$(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.";
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.modal('hide');
})
modal.modal('show');
});
// handle deletion of user
$(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 + "?";
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.modal('hide');
})
modal.modal('show');
});
// handle user role changing
$(document.body).on('change', '.user_role', 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() }}'
};
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manage-user', showResult = true);
});
</script>
{% endblock %}
{% block modals %}
<div class="modal fade modal-warning" id="modal_revoke">
<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_revoke_confirm">Revoke</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<div class="modal fade modal-warning" id="modal_delete">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Confirmation</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-left" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-flat btn-danger" id="button_delete_confirm">Delete</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
{% endblock %}

View File

@@ -0,0 +1,116 @@
{% extends "base.html" %}
{% set active_page = "admin_console" %}
{% block title %}<title>Admin Console - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
PowerDNS server configuration & statistics
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li class="active">Admin Console</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">PDNS Statistics</h3>
</div>
<div class="box-body">
<table id="tbl_statistics" class="table table-bordered table-striped">
<thead>
<tr>
<th width="6%">Docs</th>
<th>Statistic</th>
<th>Value</th>
</tr>
</thead>
<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>{{ statistic['name'] }}</td>
<td>{{ statistic['value'] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">PDNS Configuration</h3>
</div>
<div class="box-body">
<table id="tbl_configuration" class="table table-bordered table-striped">
<thead>
<tr>
<th width="6%">Docs</th>
<th>Statistic</th>
<th>Value</th>
</tr>
</thead>
<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>{{ config['name'] }}</td>
<td>
{{ config['value'] }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</section>
{% 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 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,655 @@
{% 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.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li><a href="#">Setting</a></li>
<li class="active">Authentication</li>
</ol>
<script>
function ldapSelection() {
if (document.getElementById('ldap').checked) {
document.getElementById('ldap_openldap_fields').style.display = 'block';
document.getElementById('ldap_ad_fields').style.display = 'none';
} else {
document.getElementById('ldap_openldap_fields').style.display = 'none';
document.getElementById('ldap_ad_fields').style.display = 'block';
}
}
window.onload = function() {
ldapSelection();
}
</script>
</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>
<li><a href="#tabs-oidc" data-toggle="tab">OpenID Connect OAuth</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="tabs-general">
<form role="form" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<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" name="_csrf_token" value="{{ csrf_token() }}">
<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" onclick="javascript:ldapSelection();" value="ldap" {% if SETTING.get('ldap_type')=='ldap' %}checked{% endif %}> OpenLDAP
</label>
&nbsp;&nbsp;&nbsp;
<label>
<input type="radio" name="ldap_type" id="ad" onclick="javascript:ldapSelection();" 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 id="ldap_openldap_fields">
<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>
</div>
<div id="ldap_ad_fields">
<div class="form-group">
<label for="ldap_domain">Active Directory domain</label>
<input type="text" class="form-control" name="ldap_domain" id="ldap_domain" placeholder="Active Directory domain" data-error="Please input Actve Directory domain value" value="{{ SETTING.get('ldap_domain') }}">
<span class="help-block with-errors"></span>
</div>
</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. Not needed for Active Directory authentication.
</li>
<li>
LDAP admin password - The password of LDAP administrator user. Not needed for Active Directory authentication.
</li>
<li>
Active Directory domain - Active Directory domain used.
</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> 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" name="_csrf_token" value="{{ csrf_token() }}">
<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_oauth_scope">Scope</label>
<input type="text" class="form-control" name="google_oauth_scope" id="google_oauth_scope" placeholder="e.g. email profile" data-error="Please input scope" value="{{ SETTING.get('google_oauth_scope') }}">
<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" name="_csrf_token" value="{{ csrf_token() }}">
<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="Github 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="Github 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 class="tab-pane" id="tabs-oidc">
<div class="row">
<div class="col-md-4">
<form role="form" method="post" data-toggle="validator">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" value="oidc" name="config_tab" />
<fieldset>
<legend>GENERAL</legend>
<div class="form-group">
<input type="checkbox" id="oidc_oauth_enabled" name="oidc_oauth_enabled" class="checkbox" {% if SETTING.get('oidc_oauth_enabled') %}checked{% endif %}>
<label for="oidc_oauth_enabled">Enable OpenID Connect OAuth</label>
</div>
<div class="form-group">
<label for="oidc_oauth_key">Client key</label>
<input type="text" class="form-control" name="oidc_oauth_key" id="oidc_oauth_key" placeholder="OIDC OAuth client ID" data-error="Please input Client key" value="{{ SETTING.get('oidc_oauth_key') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="oidc_oauth_secret">Client secret</label>
<input type="text" class="form-control" name="oidc_oauth_secret" id="oidc_oauth_secret" placeholder="OIDC OAuth client secret" data-error="Please input Client secret" value="{{ SETTING.get('oidc_oauth_secret') }}">
<span class="help-block with-errors"></span>
</div>
</fieldset>
<fieldset>
<legend>ADVANCE</legend>
<div class="form-group">
<label for="oidc_oauth_scope">Scope</label>
<input type="text" class="form-control" name="oidc_oauth_scope" id="oidc_oauth_scope" placeholder="e.g. email" data-error="Please input scope" value="{{ SETTING.get('oidc_oauth_scope') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="oidc_oauth_api_url">API URL</label>
<input type="text" class="form-control" name="oidc_oauth_api_url" id="oidc_oauth_api_url" placeholder="e.g. https://api.oidc.com/user" data-error="Please input API URL" value="{{ SETTING.get('oidc_oauth_api_url') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="oidc_oauth_token_url">Token URL</label>
<input type="text" class="form-control" name="oidc_oauth_token_url" id="oidc_oauth_token_url" placeholder="e.g. https://oidc.com/login/oauth/access_token" data-error="Please input Token URL" value="{{ SETTING.get('oidc_oauth_token_url') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group">
<label for="oidc_oauth_authorize_url">Authorize URL</label>
<input type="text" class="form-control" name="oidc_oauth_authorize_url" id="oidc_oauth_authorize_url" placeholder="e.g. https://oidc.com/login/oauth/authorize" data-error="Plesae input Authorize URL" value="{{ SETTING.get('oidc_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);
if ($('#ldap').is(":checked") ) {
$('#ldap_admin_username').prop('required', true);
$('#ldap_admin_password').prop('required', true);
$('#ldap_domain').prop('required', false);
} else {
$('#ldap_admin_username').prop('required', false);
$('#ldap_admin_password').prop('required', false);
$('#ldap_domain').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_operator_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_operator_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_operator_group').prop('required', true);
$('#ldap_user_group').prop('required', true);
} else {
$('#ldap_admin_group').prop('required', false);
$('#ldap_operator_group').prop('required', false);
$('#ldap_user_group').prop('required', false);
}
});
$("input[name='ldap_type']" ).change(function(){
if ($('#ldap').is(":checked") && $('#ldap_enabled').is(":checked")) {
$('#ldap_admin_group').prop('required', true);
$('#ldap_operator_group').prop('required', true);
$('#ldap_user_group').prop('required', true);
$('#ldap_domain').prop('required', false);
} else {
$('#ldap_admin_group').prop('required', false);
$('#ldap_operator_group').prop('required', false);
$('#ldap_user_group').prop('required', false);
$('#ldap_domain').prop('required', true);
}
});
// init validation requirement at first time page load
{% if SETTING.get('ldap_enabled') %}
$('#ldap_uri').prop('required', true);
$('#ldap_base_dn').prop('required', true);
if ($('#ldap').is(":checked") ) {
$('#ldap_admin_username').prop('required', true);
$('#ldap_admin_password').prop('required', true);
$('#ldap_domain').prop('required', false);
} else {
$('#ldap_admin_username').prop('required', false);
$('#ldap_admin_password').prop('required', false);
$('#ldap_domain').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_operator_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_oauth_scope').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_oauth_scope').prop('required', false);
$('#google_authorize_url').prop('required', false);
$('#google_base_url').prop('required', false);
}
});
// init validation requirement 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_oauth_scope').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 requirement at first time page load
{% if SETTING.get('github_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
// START: OIDC tab js
$('#oidc_oauth_enabled').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
}).on('ifChanged', function(e) {
var is_enabled = e.currentTarget.checked;
if (is_enabled){
$('#oidc_oauth_key').prop('required', true);
$('#oidc_oauth_secret').prop('required', true);
$('#oidc_oauth_scope').prop('required', true);
$('#oidc_oauth_api_url').prop('required', true);
$('#oidc_oauth_token_url').prop('required', true);
$('#oidc_oauth_authorize_url').prop('required', true);
} else {
$('#oidc_oauth_key').prop('required', false);
$('#oidc_oauth_secret').prop('required', false);
$('#oidc_oauth_scope').prop('required', false);
$('#oidc_oauth_api_url').prop('required', false);
$('#oidc_oauth_token_url').prop('required', false);
$('#oidc_oauth_authorize_url').prop('required', false);
}
});
// init validation requirement at first time page load
{% if SETTING.get('oidc_oauth_enabled') %}
$('#oidc_oauth_key').prop('required', true);
$('#oidc_oauth_secret').prop('required', true);
$('#oidc_oauth_scope').prop('required', true);
$('#oidc_oauth_api_url').prop('required', true);
$('#oidc_oauth_token_url').prop('required', true);
$('#oidc_oauth_authorize_url').prop('required', true);
{% endif %}
//END: OIDC Tab JS
</script>
{% endblock %}

View File

@@ -0,0 +1,97 @@
{% extends "base.html" %}
{% set active_page = "admin_settings" %}
{% block title %}
<title>Basic Settings - {{ SITE_NAME }}</title>
{% endblock %} {% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
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="#">Setting</a></li>
<li class="active">Basic</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">Basic Settings</h3>
</div>
<div class="box-body">
<table id="tbl_settings" class="table table-bordered table-striped">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
<th>Change</th>
</tr>
</thead>
<tbody>
{% for setting in settings %}
<tr class="odd ">
<td>{{ setting }}</td>
{% 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 }}">
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 }}">
Save&nbsp;<i class="fa fa-info"></i>
</button>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</section>
{% endblock %}
{% block extrascripts %}
<script>
// set up history data table
$("#tbl_settings").DataTable({
"paging": false,
"lengthChange": false,
"searching": true,
"ordering": true,
"info": true,
"autoWidth": false
});
$(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)
});
$(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() }}'
};
applyChanges(postdata, $SCRIPT_ROOT + '/admin/setting/basic/' + setting + '/edit', false, true)
});
</script>
{% endblock %}

View File

@@ -0,0 +1,93 @@
{% 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.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">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<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-Admin 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,83 @@
{% 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.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="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="create" value="{{ create }}">
<div class="box-body">
<table class="table table-bordered">
<tr>
<th style="width: 10px">#</th>
<th style="width: 40px">Record</th>
<th>Forward Zone</th>
<th>Reverse Zone</th>
</tr>
{% for record in f_records %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ record }}</td>
<td>
<input type="checkbox" id="fr_{{ record|lower }}" name="fr_{{ record|lower }}"
class="checkbox" {% if f_records[record] %}checked{% endif %}>
</td>
<td>
<input type="checkbox" id="rr_{{ record|lower }}" name="rr_{{ record|lower }}"
class="checkbox" {% if r_records[record] %}checked{% endif %}>
</td>
</tr>
{% endfor %}
</table>
</div>
<div class="box-footer">
<button type="submit" class="btn btn-flat btn-primary">Update</button>
</div>
</form>
</div>
</div>
<div class="col-md-7">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">Help</h3>
</div>
<div class="box-body">
<p>Select record types you allow user to edit in the forward zone and reverse zone. Take a look at
<a href="https://doc.powerdns.com/authoritative/appendices/types.html">PowerDNS docs</a> for
full list of supported record types.</p>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extrascripts %}
<script>
$('.checkbox').iCheck({
checkboxClass: 'icheckbox_square-blue',
increaseArea: '20%'
})
</script>
{% endblock %}

View File

@@ -0,0 +1,229 @@
<!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.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">
<img src="{{ current_user.email|email_to_gravatar_url(size=80) }}" class="user-image" alt="User Image"/>
<span class="hidden-xs">
{{ current_user.firstname }}
</span>
</a>
<ul class="dropdown-menu">
<li class="user-header">
<img src="{{ current_user.email|email_to_gravatar_url(size=160) }}" class="img-circle" alt="User Image"/>
<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('index.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">
<img src="{{ current_user.email|email_to_gravatar_url(size=100) }}" class="img-circle" alt="User Image"/>
</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.dashboard') }}"><i class="fa fa-dashboard"></i> <span>Dashboard</span></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> <span>New Domain</span></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_stats') }}"><i class="fa fa-info-circle"></i> <span>PDNS</span></a>
</li>
<li class="{{ 'active' if active_page == 'admin_history' else '' }}">
<a href="{{ url_for('admin.history') }}"><i class="fa fa-calendar"></i> <span>History</span></a>
</li>
<li class="{{ 'active' if active_page == 'admin_domain_template' else '' }}">
<a href="{{ url_for('admin.templates') }}"><i class="fa fa-clone"></i> <span>Domain Templates</span></a>
</li>
<li class="{{ 'active' if active_page == 'admin_accounts' else '' }}">
<a href="{{ url_for('admin.manage_account') }}"><i class="fa fa-industry"></i> <span>Accounts</span></a>
</li>
<li class="{{ 'active' if active_page == 'admin_users' else '' }}">
<a href="{{ url_for('admin.manage_user') }}"><i class="fa fa-users"></i> <span>Users</span></a>
</li>
<li class="{{ 'treeview active' if active_page == 'admin_settings' else 'treeview' }}">
<a href="#">
<i class="fa fa-cog"></i> <span>Settings</span>
<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> <span>Basic</span></a></li>
<li><a href="{{ url_for('admin.setting_records') }}"><i class="fa fa-circle-o"></i> <span>Records</span></a></li>
{% if current_user.role.name == 'Administrator' %}
<li><a href="{{ url_for('admin.setting_pdns') }}"><i class="fa fa-circle-o"></i> <span>PDNS</a></li>
<li><a href="{{ url_for('admin.setting_authentication') }}"><i class="fa fa-circle-o"></i> <span>Authentication</span></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.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

@@ -0,0 +1,378 @@
{% extends "base.html" %}
{% set active_page = "dashboard" %}
{% block title %}<title>Dashboard - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
Dashboard
<small>Info</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
<li class="active">Dashboard</li>
</ol>
</section>
{% endblock %}
{% block content %}
<!-- Main content -->
<section class="content">
{% if current_user.role.name in ['Administrator', 'Operator'] %}
<div class="row">
<div class="col-xs-3">
<div class="box">
<div class="box-header">
<h3 class="box-title">Statistics</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-lg-6">
<!-- small box -->
<div class="small-box bg-aqua">
<div class="inner">
<h3>{{ domain_count }}</h3>
<p>{% if domain_count > 1 %}Domains{% else %}Domain{% endif %}</p>
</div>
<div class="icon">
<i class="fa fa-book"></i>
</div>
</div>
</div>
<div class="col-lg-6">
<a href="{{ url_for('admin.manage_user') }}">
<div class="small-box bg-green">
<div class="inner">
<h3>{{ user_num }}</h3>
<p>{% if user_num > 1 %}Users{% else %}User{% endif %}</p>
</div>
<div class="icon">
<i class="fa fa-users"></i>
</div>
</div>
</a>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<a href="{{ url_for('admin.history') }}">
<div class="small-box bg-green">
<div class="inner">
<h3>{{ history_number }}</h3>
<p>{% if history_number > 1 %}Histories{% else %}History{% endif %}</p>
</div>
<div class="icon">
<i class="fa fa-calendar"></i>
</div>
</div>
</a>
</div>
<div class="col-lg-6">
<a href="{{ url_for('admin.pdns_stats') }}">
<div class="small-box bg-green">
<div class="inner">
<h3><span style="font-size: 18px">{{ uptime|display_second_to_time }}</span></h3>
<p>Uptime</p>
</div>
<div class="icon">
<i class="fa fa-clock-o"></i>
</div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
<div class="col-xs-9">
<div class="box">
<div class="box-header">
<h3 class="box-title">Recent History</h3>
</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">
<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|replace("[]","None") }}'>
Info&nbsp;<i class="fa fa-info"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</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-->
</section>
<!-- /.content -->
{% endblock %}
{% block extrascripts %}
<script>
PDNS_VERSION = '{{ SETTING.get("pdns_version") }}';
//SYBPATCH START//
function setUpDomainList(id ,url){
$(id).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,
"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//
// set up history data table
$("#tbl_history").DataTable({
"paging" : false,
"lengthChange" : false,
"searching" : false,
"ordering" : false,
"info" : false,
"autoWidth" : false,
"columnDefs": [
{
"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');
});
$(document.body).on('click', '.refresh-bg-button', function() {
var modal = $("#modal_bg_reload");
modal.modal('show');
reload_domains($SCRIPT_ROOT + '/dashboard/domains-updater');
});
$(document.body).on("click", ".button_template", function (e) {
var modal = $("#modal_template");
var domain = $(this).prop('id');
var form = " <label for=\"template_name\">Template name</label> \
<input type=\"text\" class=\"form-control\" name=\"template_name\" id=\"template_name\" placeholder=\"Enter a valid template name (required)\"> \
<label for=\"template_description\">Template description</label> \
<input type=\"text\" class=\"form-control\" name=\"template_description\" id=\"template_description\" placeholder=\"Enter a template description (optional)\"> \
<input id=\"domain\" name=\"domain\" type=\"hidden\" value=\""+domain+"\"> \
";
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
var data = {'_csrf_token': '{{ csrf_token() }}'};
data['name'] = modal.find('#template_name').val();
data['description'] = modal.find('#template_description').val();
data['domain'] = modal.find('#domain').val();
applyChanges(data, $SCRIPT_ROOT + "{{ url_for('admin.create_template_from_zone') }}", true);
modal.modal('hide');
})
modal.find('#button_close').click(function() {
modal.modal('hide');
})
modal.modal('show');
});
{% if current_user.role.name in ['Administrator', 'Operator'] or not SETTING.get('dnssec_admins_only') %}
$(document.body).on("click", ".button_dnssec", function() {
var domain = $(this).prop('id');
getdnssec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec', domain);
});
$(document.body).on("click", ".button_dnssec_enable", function() {
var domain = $(this).prop('id');
enable_dns_sec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec/enable', '{{ csrf_token() }}');
});
$(document.body).on("click", ".button_dnssec_disable", function() {
var domain = $(this).prop('id');
enable_dns_sec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec/disable', '{{ csrf_token() }}');
});
{% endif %}
</script>
{% endblock %}
{% block modals %}
<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>
<div class="modal fade modal-primary" id="modal_template">
<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">Clone to template</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-left"
id="button_close" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-flat btn-primary" id="button_save">Save</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
<div class="modal fade" id="modal_dnssec_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">DNSSEC</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" id="modal_bg_reload">
<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">Sync domains from nameserver</h4>
</div>
<div class="modal-body">
<div class="overlay">
<div id="modal_bg_reload_content"><i class="fa fa-refresh fa-spin"></i> Update in progress ..</div>
</div>
</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>
{% endblock %}

View File

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

View File

@@ -0,0 +1,569 @@
{% extends "base.html" %}
{% block title %}<title>{{ domain.name }} - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<section class="content-header">
<h1>
Manage domain: <b>{{ domain.name }}</b>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i
class="fa fa-dashboard"></i> Home</a></li>
<li>Domain</li>
<li class="active">{{ domain.name }}</li>
</ol>
</section>
{% endblock %}
{% block content %}
<section class="content">
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-body">
{% if domain.type != 'Slave' %}
<button type="button" class="btn btn-flat btn-primary pull-left button_add_record" id="{{ domain.name }}">
Add Record&nbsp;<i class="fa fa-plus"></i>
</button>
<button type="button" class="btn btn-flat btn-primary pull-right button_apply_changes" id="{{ domain.name }}" value="{{ domain.serial }}">
Apply Changes&nbsp;<i class="fa fa-floppy-o"></i>
</button>
{% else %}
<button type="button" class="btn btn-flat btn-primary pull-left button_update_from_master" id="{{ domain.name }}">
Update from Master&nbsp;<i class="fa fa-download"></i>
</button>
{% endif %}
</div>
<div class="box-body">
<table id="tbl_records" class="table table-bordered table-striped">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Status</th>
<th>TTL</th>
<th>Data</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{% for record in records %}
<tr class="odd row_record" id="{{ domain.name }}">
<td>
{{ (record.name,domain.name)|display_record_name }}
</td>
<td>
{{ record.type }}
</td>
<td>
{{ record.status }}
</td>
<td>
{{ record.ttl }}
</td>
<td>
{{ 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>&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 %}
{% 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>
</td>
{% endif %}
</td>
<!-- hidden column that we can sort on -->
<td>1</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- /.box-body -->
</div>
<!-- /.box -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</section>
{% endblock %}
{% block extrascripts %}
<script>
// superglobals
window.records_allow_edit = {{ editable_records|tojson }};
window.ttl_options = {{ ttl_options|tojson }};
window.nEditing = null;
window.nNew = false;
// set up user data table
$("#tbl_records").DataTable({
"paging" : true,
"lengthChange" : true,
"searching" : true,
"ordering" : true,
"info" : true,
"autoWidth" : false,
{% if SETTING.get('default_record_table_size')|string in ['5','15','20'] %}
"lengthMenu": [ [5, 15, 20, -1],
[5, 15, 20, "All"]],
{% else %}
"lengthMenu": [ [5, 15, 20, {{ SETTING.get('default_record_table_size') }}, -1],
[5, 15, 20, {{ SETTING.get('default_record_table_size') }}, "All"]],
{% endif %}
"pageLength": {{ SETTING.get('default_record_table_size') }},
"language": {
"lengthMenu": " _MENU_ records"
},
"retrieve" : true,
"columnDefs": [
{
type: 'natural',
targets: [0, 4]
},
{
// hidden column so that we can add new records on top
// regardless of whatever sorting is done
visible: false,
targets: [ 7 ]
},
{
className: "length-break",
targets: [ 4 ]
}
],
"orderFixed": [[7, 'asc']]
});
// handle delete button
$(document.body).on("click", ".button_delete", function(e) {
e.stopPropagation();
var modal = $("#modal_delete");
var table = $("#tbl_records").DataTable();
var record = $(this).prop('id');
var nRow = $(this).parents('tr')[0];
var info = "Are you sure you want to delete " + record + "?";
modal.find('.modal-body p').text(info);
modal.modal('show');
$("#button_delete_confirm").unbind().one('click', function(e) {
table.row(nRow).remove().draw();
modal.modal('hide');
});
$("#button_delete_cancel").unbind().one('click', function(e) {
modal.modal('hide');
});
});
// handle edit button and record click
$(document.body).on("click", ".button_edit{% if quick_edit %}, .row_record{% endif %}", function(e) {
e.stopPropagation();
if ($(this).is('tr')) {
var nRow = $(this)[0];
} else {
var nRow = $(this).parents('tr')[0];
}
var table = $("#tbl_records").DataTable();
if (nEditing == nRow) {
/* click on row already being edited, do nothing */
} else if (nEditing !== null && nEditing != nRow && nNew == false) {
/* Currently editing - but not this row - restore the old before continuing to edit mode */
restoreRow(table, nEditing);
editRow(table, nRow);
nEditing = nRow;
} else if (nNew == true) {
/* adding a new row, delete it and start editing */
table.row(nEditing).remove().draw();
nNew = false;
editRow(table, nRow);
nEditing = nRow;
} else {
/* No edit in progress - let's start one */
editRow(table, nRow);
nEditing = nRow;
}
});
// handle apply changes button
$(document.body).on("click",".button_apply_changes", function() {
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 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() }}'};
applyRecordChanges(data, domain);
modal.modal('hide');
})
modal.modal('show');
});
// handle add record button
$(document.body).on("click", ".button_add_record", function (e) {
if (nNew || nEditing) {
var modal = $("#modal_error");
modal.find('.modal-body p').text("Previous record not saved. Please save it before adding more record.");
modal.modal('show');
return;
}
// clear search first
$("#tbl_records").DataTable().search('').columns().search('').draw();
// add new row
var default_type = records_allow_edit[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;
nNew = true;
});
//handle cancel button
$(document.body).on("click", ".button_cancel", function (e) {
e.stopPropagation();
var oTable = $("#tbl_records").DataTable();
if (nNew) {
oTable.row(nEditing).remove().draw();
nEditing = null;
nNew = false;
} else {
restoreRow(oTable, nEditing);
nEditing = null;
}
});
//handle save button
$(document.body).on("click", ".button_save", function (e) {
e.stopPropagation();
var table = $("#tbl_records").DataTable();
saveRow(table, nEditing);
nEditing = null;
nNew = false;
});
//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');
});
{% if SETTING.get('record_helper') %}
//handle wacky record types
$(document.body).on("focus", "#current_edit_record_data", function (e) {
var record_type = $(this).parents("tr").find('#record_type').val();
var record_data = $(this);
if (record_type == "CAA") {
var modal = $("#modal_custom_record");
if (record_data.val() == "") {
var form = " <label for=\"caa_flag\">CAA Flag</label> \
<input type=\"text\" class=\"form-control\" name=\"caa_flag\" id=\"caa_flag\" placeholder=\"0\"> \
<label for=\"caa_tag\">CAA Tag</label> \
<input type=\"text\" class=\"form-control\" name=\"caa_tag\" id=\"caa_tag\" placeholder=\"issue\"> \
<label for=\"caa_value\">CAA Value</label> \
<input type=\"text\" class=\"form-control\" name=\"caa_value\" id=\"caa_value\" placeholder=\"eg. letsencrypt.org\"> \
";
} else {
var parts = record_data.val().split(" ");
var form = " <label for=\"caa_flag\">CAA Flag</label> \
<input type=\"text\" class=\"form-control\" name=\"caa_flag\" id=\"caa_flag\" placeholder=\"0\" value=\"" + parts[0] + "\"> \
<label for=\"caa_tag\">CAA Tag</label> \
<input type=\"text\" class=\"form-control\" name=\"caa_tag\" id=\"caa_tag\" placeholder=\"issue\" value=\"" + parts[1] + "\"> \
<label for=\"caa_value\">CAA Value</label> \
<input type=\"text\" class=\"form-control\" name=\"caa_value\" id=\"caa_value\" placeholder=\"eg. letsencrypt.org\" value=\"" + parts[2] + "\"> \
";
}
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
caa_flag = modal.find('#caa_flag').val();
caa_tag = modal.find('#caa_tag').val();
caa_value = modal.find('#caa_value').val();
data = caa_flag + " " + caa_tag + " " + '"' + caa_value + '"';
record_data.val(data);
modal.modal('hide');
})
modal.modal('show');
} else if (record_type == "MX") {
var modal = $("#modal_custom_record");
if (record_data.val() == "") {
var form = " <label for=\"mx_priority\">MX Priority</label> \
<input type=\"text\" class=\"form-control\" name=\"mx_priority\" id=\"mx_priority\" placeholder=\"eg. 10\"> \
<label for=\"mx_server\">MX Server</label> \
<input type=\"text\" class=\"form-control\" name=\"mx_server\" id=\"mx_server\" placeholder=\"eg. postfix.example.com\"> \
";
} else {
var parts = record_data.val().split(" ");
var form = " <label for=\"mx_priority\">MX Priority</label> \
<input type=\"text\" class=\"form-control\" name=\"mx_priority\" id=\"mx_priority\" placeholder=\"eg. 10\" value=\"" + parts[0] + "\"> \
<label for=\"mx_server\">MX Server</label> \
<input type=\"text\" class=\"form-control\" name=\"mx_server\" id=\"mx_server\" placeholder=\"eg. postfix.example.com\" value=\"" + parts[1] + "\"> \
";
}
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
mx_server = modal.find('#mx_server').val();
mx_priority = modal.find('#mx_priority').val();
data = mx_priority + " " + mx_server;
record_data.val(data);
modal.modal('hide');
})
modal.modal('show');
} else if (record_type == "SRV") {
var modal = $("#modal_custom_record");
if (record_data.val() == "") {
var form = " <label for=\"srv_priority\">SRV Priority</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_priority\" id=\"srv_priority\" placeholder=\"0\"> \
<label for=\"srv_weight\">SRV Weight</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_weight\" id=\"srv_weight\" placeholder=\"10\"> \
<label for=\"srv_port\">SRV Port</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_port\" id=\"srv_port\" placeholder=\"5060\"> \
<label for=\"srv_target\">SRV Target</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_target\" id=\"srv_target\" placeholder=\"sip.example.com\"> \
";
} else {
var parts = record_data.val().split(" ");
var form = " <label for=\"srv_priority\">SRV Priority</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_priority\" id=\"srv_priority\" placeholder=\"0\" value=\"" + parts[0] + "\"> \
<label for=\"srv_weight\">SRV Weight</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_weight\" id=\"srv_weight\" placeholder=\"10\" value=\"" + parts[1] + "\"> \
<label for=\"srv_port\">SRV Port</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_port\" id=\"srv_port\" placeholder=\"5060\" value=\"" + parts[2] + "\"> \
<label for=\"srv_target\">SRV Target</label> \
<input type=\"text\" class=\"form-control\" name=\"srv_target\" id=\"srv_target\" placeholder=\"sip.example.com\" value=\"" + parts[3] + "\"> \
";
}
modal.find('.modal-body p').html(form);
modal.find('#button_save').click(function() {
srv_priority = modal.find('#srv_priority').val();
srv_weight = modal.find('#srv_weight').val();
srv_port = modal.find('#srv_port').val();
srv_target = modal.find('#srv_target').val();
data = srv_priority + " " + srv_weight + " " + srv_port + " " + srv_target;
record_data.val(data);
modal.modal('hide');
})
modal.modal('show');
} else if (record_type == "SOA") {
var modal = $("#modal_custom_record");
if (record_data.val() == "") {
var form = " <label for=\"soa_primaryns\">Primary Name Server</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_primaryns\" id=\"soa_primaryns\" placeholder=\"ns1.example.com\"> \
<label for=\"soa_adminemail\">Primary Contact</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_adminemail\" id=\"soa_adminemail\" placeholder=\"admin.example.com\"> \
<label for=\"soa_serial\">Serial</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_serial\" id=\"soa_serial\" placeholder=\"2016010101\"> \
<label for=\"soa_zonerefresh\">Zone refresh timer</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_zonerefresh\" id=\"soa_zonerefresh\" placeholder=\"86400\"> \
<label for=\"soa_failedzonerefresh\">Failed refresh retry timer</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_failedzonerefresh\" id=\"soa_failedzonerefresh\" placeholder=\"7200\"> \
<label for=\"soa_zoneexpiry\">Zone expiry timer</label> \
<input type=\"text\" class=\"form-control\" name=\"soa_zoneexpiry\" id=\"soa_zoneexpiry\" placeholder=\"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();
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');
});
modal.modal('show');
}
});
{% endif %}
</script>
{% endblock %}
{% block modals %}
<div class="modal fade modal-warning" id="modal_delete">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Confirmation</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-left" id="button_delete_cancel"
data-dismiss="modal">Close</button>
<button type="button" class="btn btn-flat btn-danger" id="button_delete_confirm">Delete</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<div class="modal fade modal-primary" id="modal_apply_changes">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Confirmation</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-left"
data-dismiss="modal">Close</button>
<button type="button" class="btn btn-flat btn-primary" id="button_apply_confirm">Apply</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<div class="modal fade modal-primary" id="modal_custom_record">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Custom Record</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-primary" id="button_save">Save</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
{% endblock %}

View File

@@ -0,0 +1,180 @@
{% 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.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li><a href="{{ url_for('dashboard.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') }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<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.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,284 @@
{% 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.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.setting', domain_name=domain.name) }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<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) }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<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) }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<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
},
'_csrf_token': '{{ csrf_token() }}'
};
applyChanges(postdata, $SCRIPT_ROOT + '/domain/' + domain + '/manage-setting', 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
},
'_csrf_token': '{{ csrf_token() }}'
};
applyChanges(postdata, $SCRIPT_ROOT + '/domain/' + domain + '/manage-setting', 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 () {
$.post($SCRIPT_ROOT + '/domain/setting/' + domain + '/delete', {
'_csrf_token': '{{ csrf_token() }}'
}, function () {
window.location.href = '{{ url_for('dashboard.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

@@ -0,0 +1 @@
{{ response }}

View File

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}<title>PowerDNS-Admin - 400 Error</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
400
<small>Error</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li>400</li>
</ol>
</section>
{% endblock %}
{% block content %}
<!-- Main content -->
<section class="content">
<div class="error-page">
<h2 class="headline text-yellow">400</h2>
<div class="error-content">
<h3>
<i class="fa fa-warning text-yellow"></i> Oops! Bad request
</h3>
<p>
The server refused to process your request and return a 400 error.
You may <a href="{{ url_for('dashboard.dashboard') }}">return to the dashboard</a>.
</p>
</div>
<!-- /.error-content -->
</div>
<!-- /.error-page -->
</section>
<!-- /.content -->
{% endblock %}

View File

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}<title>PowerDNS-Admin - 403 Error</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
403
<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>
</ol>
</section>
{% endblock %}
{% block content %}
<!-- Main content -->
<section class="content">
<div class="error-page">
<h2 class="headline text-yellow">403</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>.
</p>
</div>
<!-- /.error-content -->
</div>
<!-- /.error-page -->
</section>
<!-- /.content -->
{% endblock %}

View File

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}<title>PowerDNS-Admin - 404 Error</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
404
<small>Error</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li>404</li>
</ol>
</section>
{% endblock %}
{% block content %}
<!-- Main content -->
<section class="content">
<div class="error-page">
<h2 class="headline text-yellow">404</h2>
<div class="error-content">
<h3>
<i class="fa fa-warning text-yellow"></i> Oops! You're lost
</h3>
<p>
The page you requested could not be found.
You may <a href="{{ url_for('dashboard.dashboard') }}">return to the dashboard</a>.
</p>
</div>
<!-- /.error-content -->
</div>
<!-- /.error-page -->
</section>
<!-- /.content -->
{% endblock %}

View File

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}<title>PowerDNS-Admin - 500 Error</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
500
<small>Error</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li>500</li>
</ol>
</section>
{% endblock %}
{% block content %}
<!-- Main content -->
<section class="content">
<div class="error-page">
<h2 class="headline text-yellow">500</h2>
<div class="error-content">
<h3>
<i class="fa fa-warning text-yellow"></i> Oops! Something went wrong
</h3>
<p>
Try again later.
You may <a href="{{ url_for('dashboard.dashboard') }}">return to the dashboard</a>.
</p>
</div>
<!-- /.error-content -->
</div>
<!-- /.error-page -->
</section>
<!-- /.content -->
{% endblock %}

View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block title %}<title>PowerDNS-Admin - SAML Authentication Error</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
<h1>
SAML
<small>Error</small>
</h1>
<ol class="breadcrumb">
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
<li>SAML</li>
</ol>
</section>
{% endblock %}
{% block content %}
<!-- Main content -->
<section class="content">
<div class="error-page">
<div>
<h1 class="headline text-yellow" style="font-size:46px;">SAML Authentication Error</h1></div><br/><br/>
<div class="error-content">
<h3>
<i class="fa fa-warning text-yellow"></i> Oops! Something went wrong
</h3><br>
<p>
Login failed.<br>
Error(s) when processing SAML Response:<br>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
You may <a href="{{ url_for('dashboard.dashboard') }}">return to the dashboard</a>.
</p>
</div>
<!-- /.error-content -->
</div>
<!-- /.error-page -->
</section>
<!-- /.content -->
{% endblock %}

View File

@@ -0,0 +1,147 @@
<!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.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">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<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" name="remember"> 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') or SETTING.get('oidc_oauth_enabled') %}
<div class="social-auth-links text-center">
<p>- OR -</p>
{% if SETTING.get('oidc_oauth_enabled') %}
<a href="{{ url_for('index.oidc_login') }}" class="btn btn-block btn-social btn-openid btn-flat"><i
class="fa fa-openid"></i> Sign in using
OpenID Connect</a>
{% endif %}
{% if SETTING.get('github_oauth_enabled') %}
<a href="{{ url_for('index.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('index.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('index.saml_login') }}">SAML login</a>
{% endif %}
{% if SETTING.get('signup_enabled') %}
<br>
<a href="{{ url_for('index.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,43 @@
<!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,109 @@
<!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.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('index.register') }}" method="post" data-toggle="validator">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<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('index.login') }}';
})
});
</script>
</body>
</html>

View File

@@ -0,0 +1,125 @@
{% 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('admin.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('admin.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('admin.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('admin.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>
<button type="button" class="btn btn-flat btn-danger button_delete" id="{{template.name}}">
Delete&nbsp;<i class="fa fa-trash"></i>
</button>
</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
});
// handle delete button
$(document.body).on("click", ".button_delete", function (e) {
var template = $(this).prop('id');
$.post($SCRIPT_ROOT + '/admin/template/' + template + '/delete', {
'_csrf_token': '{{ csrf_token() }}'
}, function () {
window.location.href = '{{ url_for('admin.templates') }}';
});
});
</script>
{% endblock %}
{% block modals %}
{% endblock %}

View File

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

View File

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

View File

@@ -0,0 +1,156 @@
{% extends "base.html" %}
{% block title %}<title>My Profile - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- 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>
<li class="active">My Profile</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">Edit my profile{% if session['authentication_type'] != 'LOCAL' %} [Disabled -
Authenticated externally]{% endif %}</h3>
</div>
<div class="box-body">
<!-- Custom Tabs -->
<div class="nav-tabs-custom" id="tabs">
<ul class="nav nav-tabs">
<li class="active"><a href="#tabs-personal" data-toggle="tab">Personal
Info</a></li>
{% if session['authentication_type'] == 'LOCAL' %}
<li><a href="#tabs-password" data-toggle="tab">Change Password</a></li>
{% endif %}
{% if session['authentication_type'] in ['LOCAL', 'LDAP'] %}
<li><a href="#tabs-authentication" data-toggle="tab">Authentication</a></li>
{% endif %}
</ul>
<div class="tab-content">
<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 %}>
</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 %}>
</div>
<div class="form-group">
<label for="email">E-mail</label> <input type="text" class="form-control"
name="email" id="email" placeholder="{{ current_user.email }}"
{% if 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>
{% if session['authentication_type'] == 'LOCAL' %}
<div class="tab-pane" id="tabs-password">
{% if not current_user.password %}
Your account password is managed via LDAP which isn't supported to change here.
{% else %}
<form action="{{ user_profile }}" method="post">
<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" />
</div>
<div class="form-group">
<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>
</div>
</form>
{% endif %}
</div>
{% endif %}
<!-- {% if session['authentication_type'] in ['LOCAL', 'LDAP'] %} -->
<div class="tab-pane" id="tabs-authentication">
<form action="{{ user_profile }}" method="post">
<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 %}>
<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>
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
nobody can capture it.</i></strong></font>
</div>
{% endif %}
</div>
</form>
</div>
<!-- {% endif %} -->
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block extrascripts %}
<!-- TODO: add password and password confirmation comparison check -->
<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')
});
// initialize pretty checkboxes
$('.otp_toggle').iCheck({
checkboxClass: 'icheckbox_square-blue',
increaseArea: '20%'
});
// handle checkbox toggling
$('.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() }}'
};
applyChanges(postdata, $SCRIPT_ROOT + '/user/profile', false, true);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,32 @@
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.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']