mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2024-11-09 15:10:27 +00:00
Resolve the conflicts for #228
This commit is contained in:
commit
17a892b18d
5
.gitignore
vendored
5
.gitignore
vendored
@ -23,9 +23,14 @@ nosetests.xml
|
||||
flask
|
||||
config.py
|
||||
logfile.log
|
||||
settings.json
|
||||
advanced_settings.json
|
||||
idp.crt
|
||||
log.txt
|
||||
|
||||
db_repository/*
|
||||
upload/avatar/*
|
||||
tmp/*
|
||||
.ropeproject
|
||||
.sonarlint/*
|
||||
pdns.db
|
||||
|
12
.travis.yml
Normal file
12
.travis.yml
Normal file
@ -0,0 +1,12 @@
|
||||
language: python
|
||||
python:
|
||||
- "3.5.2"
|
||||
before_install:
|
||||
- 'travis_retry sudo apt-get update'
|
||||
- 'travis_retry sudo apt-get install python3-dev libxml2-dev libxmlsec1-dev'
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
before_script:
|
||||
- mv config_template.py config.py
|
||||
script:
|
||||
- sh run_travis.sh
|
11
Dockerfile
11
Dockerfile
@ -6,12 +6,21 @@ ENV ENVIRONMENT=${ENVIRONMENT}
|
||||
WORKDIR /powerdns-admin
|
||||
|
||||
RUN apt-get update -y
|
||||
RUN apt-get install -y python3-pip python3-dev libmysqlclient-dev supervisor
|
||||
RUN apt-get install -y python3-pip python3-dev supervisor
|
||||
|
||||
# lib for building mysql db driver
|
||||
RUN apt-get install -y libmysqlclient-dev
|
||||
|
||||
# lib for buiding ldap and ssl-based application
|
||||
RUN apt-get install -y libsasl2-dev libldap2-dev libssl-dev
|
||||
|
||||
# lib for building python3-saml
|
||||
RUN apt-get install -y libxml2-dev libxslt1-dev libxmlsec1-dev libffi-dev pkg-config
|
||||
|
||||
COPY ./requirements.txt /powerdns-admin/requirements.txt
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
ADD ./supervisord.conf /etc/supervisord.conf
|
||||
ADD . /powerdns-admin/
|
||||
COPY ./configs/${ENVIRONMENT}.py /powerdns-admin/config.py
|
||||
|
||||
|
@ -8,6 +8,9 @@ A PowerDNS web interface with advanced features.
|
||||
- User access management based on domain
|
||||
- User activity logging
|
||||
- Local DB / LDAP / Active Directory user authentication
|
||||
- Support SAML authentication
|
||||
- Google oauth authentication
|
||||
- Github oauth authentication
|
||||
- Support Two-factor authentication (TOTP)
|
||||
- Dashboard and pdns service statistics
|
||||
- DynDNS 2 protocol support
|
||||
|
@ -11,7 +11,6 @@ login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
|
||||
def enable_github_oauth(GITHUB_ENABLE):
|
||||
if not GITHUB_ENABLE:
|
||||
return None, None
|
||||
@ -89,5 +88,9 @@ def enable_google_oauth(GOOGLE_ENABLE):
|
||||
|
||||
google = enable_google_oauth(app.config.get('GOOGLE_OAUTH_ENABLE'))
|
||||
|
||||
|
||||
from app import views, models
|
||||
|
||||
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()
|
||||
|
48
app/lib/certutil.py
Normal file
48
app/lib/certutil.py
Normal 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))
|
116
app/lib/utils.py
116
app/lib/utils.py
@ -7,6 +7,44 @@ import hashlib
|
||||
from app import app
|
||||
from distutils.version import StrictVersion
|
||||
from urllib.parse import urlparse
|
||||
from datetime import datetime, timedelta
|
||||
from threading import Thread
|
||||
|
||||
from .certutil import *
|
||||
|
||||
if app.config['SAML_ENABLED']:
|
||||
from onelogin.saml2.auth import OneLogin_Saml2_Auth
|
||||
from onelogin.saml2.utils import OneLogin_Saml2_Utils
|
||||
from onelogin.saml2.settings import OneLogin_Saml2_Settings
|
||||
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
|
||||
idp_timestamp = datetime(1970, 1, 1)
|
||||
idp_data = None
|
||||
idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'])
|
||||
if idp_data is None:
|
||||
print('SAML: IDP Metadata initial load failed')
|
||||
exit(-1)
|
||||
idp_timestamp = datetime.now()
|
||||
|
||||
|
||||
def get_idp_data():
|
||||
global idp_data, idp_timestamp
|
||||
lifetime = timedelta(minutes=app.config['SAML_METADATA_CACHE_LIFETIME'])
|
||||
if idp_timestamp+lifetime < datetime.now():
|
||||
background_thread = Thread(target=retreive_idp_data)
|
||||
background_thread.start()
|
||||
return idp_data
|
||||
|
||||
|
||||
def retreive_idp_data():
|
||||
global idp_data, idp_timestamp
|
||||
new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'])
|
||||
if new_idp_data is not None:
|
||||
idp_data = new_idp_data
|
||||
idp_timestamp = datetime.now()
|
||||
print("SAML: IDP Metadata successfully retreived from: " + app.config['SAML_METADATA_URL'])
|
||||
else:
|
||||
print("SAML: IDP Metadata could not be retreived")
|
||||
|
||||
|
||||
if 'TIMEOUT' in app.config.keys():
|
||||
TIMEOUT = app.config['TIMEOUT']
|
||||
@ -62,7 +100,8 @@ def fetch_remote(remote_url, method='GET', data=None, accept=None, params=None,
|
||||
|
||||
|
||||
def fetch_json(remote_url, method='GET', data=None, params=None, headers=None):
|
||||
r = fetch_remote(remote_url, method=method, data=data, params=params, headers=headers, accept='application/json; q=1')
|
||||
r = fetch_remote(remote_url, method=method, data=data, params=params, headers=headers,
|
||||
accept='application/json; q=1')
|
||||
|
||||
if method == "DELETE":
|
||||
return True
|
||||
@ -159,3 +198,78 @@ def email_to_gravatar_url(email="", size=100):
|
||||
"""
|
||||
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.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'] is 'on':
|
||||
own_url = 'https://'
|
||||
else:
|
||||
own_url = 'http://'
|
||||
own_url += req['http_host']
|
||||
metadata = get_idp_data()
|
||||
settings = {}
|
||||
settings['sp'] = {}
|
||||
settings['sp']['NameIDFormat'] = idp_data['sp']['NameIDFormat']
|
||||
settings['sp']['entityId'] = app.config['SAML_SP_ENTITY_ID']
|
||||
cert = open(CERT_FILE, "r").readlines()
|
||||
key = open(KEY_FILE, "r").readlines()
|
||||
settings['sp']['privateKey'] = "".join(key)
|
||||
settings['sp']['x509cert'] = "".join(cert)
|
||||
settings['sp']['assertionConsumerService'] = {}
|
||||
settings['sp']['assertionConsumerService']['binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
||||
settings['sp']['assertionConsumerService']['url'] = own_url+'/saml/authorized'
|
||||
settings['sp']['attributeConsumingService'] = {}
|
||||
settings['sp']['singleLogoutService'] = {}
|
||||
settings['sp']['singleLogoutService']['binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||
settings['sp']['singleLogoutService']['url'] = own_url+'/saml/sls'
|
||||
settings['idp'] = metadata['idp']
|
||||
settings['strict'] = True
|
||||
settings['debug'] = app.config['SAML_DEBUG']
|
||||
settings['security'] = {}
|
||||
settings['security']['digestAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
|
||||
settings['security']['metadataCacheDuration'] = None
|
||||
settings['security']['metadataValidUntil'] = None
|
||||
settings['security']['requestedAuthnContext'] = True
|
||||
settings['security']['signatureAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
|
||||
settings['security']['wantAssertionsEncrypted'] = False
|
||||
settings['security']['wantAttributeStatement'] = True
|
||||
settings['security']['wantNameId'] = True
|
||||
settings['security']['authnRequestsSigned'] = app.config['SAML_SIGN_REQUEST']
|
||||
settings['security']['logoutRequestSigned'] = app.config['SAML_SIGN_REQUEST']
|
||||
settings['security']['logoutResponseSigned'] = app.config['SAML_SIGN_REQUEST']
|
||||
settings['security']['nameIdEncrypted'] = False
|
||||
settings['security']['signMetadata'] = True
|
||||
settings['security']['wantAssertionsSigned'] = True
|
||||
settings['security']['wantMessagesSigned'] = True
|
||||
settings['security']['wantNameIdEncrypted'] = False
|
||||
settings['contactPerson'] = {}
|
||||
settings['contactPerson']['support'] = {}
|
||||
settings['contactPerson']['support']['emailAddress'] = app.config['SAML_SP_CONTACT_NAME']
|
||||
settings['contactPerson']['support']['givenName'] = app.config['SAML_SP_CONTACT_MAIL']
|
||||
settings['contactPerson']['technical'] = {}
|
||||
settings['contactPerson']['technical']['emailAddress'] = app.config['SAML_SP_CONTACT_NAME']
|
||||
settings['contactPerson']['technical']['givenName'] = app.config['SAML_SP_CONTACT_MAIL']
|
||||
settings['organization'] = {}
|
||||
settings['organization']['en-US'] = {}
|
||||
settings['organization']['en-US']['displayname'] = 'PowerDNS-Admin'
|
||||
settings['organization']['en-US']['name'] = 'PowerDNS-Admin'
|
||||
settings['organization']['en-US']['url'] = own_url
|
||||
auth = OneLogin_Saml2_Auth(req, settings)
|
||||
return auth
|
@ -140,7 +140,9 @@ class User(db.Model):
|
||||
|
||||
def check_password(self, hashed_password):
|
||||
# Check hased password. Useing 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))
|
||||
@ -276,7 +278,7 @@ class User(db.Model):
|
||||
self.set_admin(isadmin)
|
||||
self.update_profile()
|
||||
return True
|
||||
|
||||
else:
|
||||
logging.error('Unsupported authentication method')
|
||||
return False
|
||||
|
||||
@ -653,6 +655,38 @@ class Domain(db.Model):
|
||||
logging.debug(traceback.print_exc())
|
||||
return {'status': 'error', 'msg': 'Cannot add this domain.'}
|
||||
|
||||
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'] = PDNS_API_KEY
|
||||
if soa_edit_api == 'OFF':
|
||||
post_data = {
|
||||
"soa_edit_api": None,
|
||||
"kind": domain.type
|
||||
}
|
||||
else:
|
||||
post_data = {
|
||||
"soa_edit_api": soa_edit_api,
|
||||
"kind": domain.type
|
||||
}
|
||||
try:
|
||||
jdata = utils.fetch_json(
|
||||
urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain.name)), headers=headers,
|
||||
method='PUT', data=post_data)
|
||||
if 'error' in jdata.keys():
|
||||
logging.error(jdata['error'])
|
||||
return {'status': 'error', 'msg': jdata['error']}
|
||||
else:
|
||||
logging.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:
|
||||
logging.debug(e)
|
||||
logging.debug(traceback.format_exc())
|
||||
logging.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,
|
||||
@ -799,6 +833,50 @@ class Domain(db.Model):
|
||||
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'] = PDNS_API_KEY
|
||||
post_data = {
|
||||
"keytype": "ksk",
|
||||
"active": True
|
||||
}
|
||||
try:
|
||||
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + '/servers/localhost/zones/{0}/cryptokeys'.format(domain.name)), headers=headers, method='POST',data=post_data)
|
||||
if 'error' in jdata:
|
||||
return {'status': 'error', 'msg': 'DNSSEC is not enabled for this domain', 'jdata' : jdata}
|
||||
else:
|
||||
return {'status': 'ok'}
|
||||
except:
|
||||
logging.error(traceback.print_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'] = PDNS_API_KEY
|
||||
url = '/servers/localhost/zones/{0}/cryptokeys/{1}'.format(domain.name, key_id)
|
||||
|
||||
try:
|
||||
jdata = utils.fetch_json(urljoin(PDNS_STATS_URL, API_EXTENDED_URL + url), headers=headers, method='DELETE')
|
||||
if 'error' in jdata:
|
||||
return {'status': 'error', 'msg': 'DNSSEC is not disabled for this domain', 'jdata' : jdata}
|
||||
else:
|
||||
return {'status': 'ok'}
|
||||
except:
|
||||
return {'status': 'error', 'msg': 'There was something wrong, please contact administrator','id': key_id, 'url': url}
|
||||
else:
|
||||
return {'status': 'error', 'msg': 'This domain doesnot exist'}
|
||||
|
||||
class DomainUser(db.Model):
|
||||
__tablename__ = 'domain_user'
|
||||
|
@ -1,3 +1,5 @@
|
||||
var dnssecKeyList = []
|
||||
|
||||
function applyChanges(data, url, showResult, refreshPage) {
|
||||
var success = false;
|
||||
$.ajax({
|
||||
@ -116,7 +118,22 @@ function SelectElement(elementID, valueToSelect)
|
||||
element.value = valueToSelect;
|
||||
}
|
||||
|
||||
function getdnssec(url){
|
||||
function enable_dns_sec(url) {
|
||||
$.getJSON(url, 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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getdnssec(url, domain){
|
||||
|
||||
$.getJSON(url, function(data) {
|
||||
var modal = $("#modal_dnssec_info");
|
||||
@ -127,6 +144,18 @@ function getdnssec(url){
|
||||
else {
|
||||
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>'+
|
||||
@ -145,6 +174,7 @@ function getdnssec(url){
|
||||
dnssec_msg += '</form>';
|
||||
}
|
||||
}
|
||||
}
|
||||
modal.find('.modal-body p').html(dnssec_msg);
|
||||
}
|
||||
modal.modal('show');
|
||||
|
@ -154,6 +154,7 @@
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
PDNS_VERSION = '{{ pdns_version }}'
|
||||
// set up history data table
|
||||
$("#tbl_history").DataTable({
|
||||
"paging" : false,
|
||||
@ -214,6 +215,23 @@
|
||||
|
||||
modal.modal('show');
|
||||
});
|
||||
|
||||
$(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');
|
||||
|
||||
});
|
||||
|
||||
$(document.body).on("click", ".button_dnssec_disable", function() {
|
||||
var domain = $(this).prop('id');
|
||||
enable_dns_sec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec/disable');
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block modals %}
|
||||
@ -262,4 +280,28 @@
|
||||
</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">×</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 -->
|
||||
{% endblock %}
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
{% macro dnssec(domain) %}
|
||||
{% if domain.dnssec %}
|
||||
<td><span class="label label-success"><i class="fa fa-lock-alt"></i> Enabled</span></td>
|
||||
<td><span style="cursor:pointer" class="label label-success button_dnssec" id="{{ domain.name }}"><i class="fa fa-lock"></i> Enabled</span></td>
|
||||
{% else %}
|
||||
<td><span class="label label-primary"><i class="fa fa-unlock-alt"></i> Disabled</span></td>
|
||||
<td><span style="cursor:pointer" class="label label-primary button_dnssec" id="{{ domain.name }}"><i class="fa fa-unlock-alt"></i> Disabled</span></td>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
@ -82,7 +82,7 @@
|
||||
<button type="button" class="btn btn-flat btn-warning"> <i class="fa fa-exclamation-circle"></i> </button>
|
||||
</td>
|
||||
<td width="6%">
|
||||
<button type="button" class="btn btn-flat btn-warning"> <i class="fa fa-exclamation-circle"></i> </button>
|
||||
<button type="button" class="btn btn-flat btn-warning""> <i class="fa fa-exclamation-circle"></i> </button>
|
||||
</td>
|
||||
{% endif %}
|
||||
</td>
|
||||
@ -104,6 +104,7 @@
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
PDNS_VERSION = '{{ pdns_version }}'
|
||||
// superglobals
|
||||
window.records_allow_edit = {{ editable_records|tojson }};
|
||||
window.nEditing = null;
|
||||
|
@ -1,6 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}<title>DNS Control Panel - Domain Management</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>
|
||||
@ -86,6 +102,57 @@
|
||||
</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 when and how the SOA serial number will be updated after a change is made to the domain.</p>
|
||||
<ul>
|
||||
<li>
|
||||
(OFF) - Not set
|
||||
</li>
|
||||
<li>
|
||||
INCEPTION-INCREMENT - Uses YYYYMMDDSS format for SOA serial numbers. If the SOA serial from the backend is within two days after inception, it gets incremented by two (the backend should keep SS below 98).
|
||||
</li>
|
||||
<li>
|
||||
INCEPTION - Sets the SOA serial to the last inception time in YYYYMMDD01 format. Uses localtime to find the day for inception time. <strong>Not recomended.</strong>
|
||||
</li>
|
||||
<li>
|
||||
INCREMENT-WEEK - Sets the SOA serial to the number of weeks since the epoch, which is the last inception time in weeks. <strong>Not recomended.</strong>
|
||||
</li>
|
||||
<li>
|
||||
INCREMENT-WEEKS - Increments the serial with the number of weeks since the UNIX epoch. This should work in every setup; but the result won't look like YYYYMMDDSS anymore.
|
||||
</li>
|
||||
<li>
|
||||
EPOCH - Sets the SOA serial to the number of seconds since the epoch.
|
||||
</li>
|
||||
<li>
|
||||
INCEPTION-EPOCH - Sets the new SOA serial number to the maximum of the old SOA serial number, and age in seconds of the last inception.
|
||||
</li>
|
||||
</ul>
|
||||
<b>New SOA-EDIT-API Setting:</b>
|
||||
<form method="post" action="{{ url_for('domain_change_soa_edit_api', domain_name=domain.name) }}">
|
||||
<select name="soa_edit_api" class="form-control" style="width:15em;">
|
||||
<option selected value="0">- Unchanged -</option>
|
||||
<option>OFF</option>
|
||||
<option>INCEPTION-INCREMENT</option>
|
||||
<option>INCEPTION</option>
|
||||
<option>INCREMENT-WEEK</option>
|
||||
<option>INCREMENT-WEEKS</option>
|
||||
<option>EPOCH</option>
|
||||
<option>INCEPTION-EPOCH</option>
|
||||
</select><br/>
|
||||
<button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api">
|
||||
<i class="fa fa-check"></i> 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">
|
||||
|
45
app/templates/errors/SAML.html
Normal file
45
app/templates/errors/SAML.html
Normal file
@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}<title>DNS Control Panel - 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') }}"><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') }}">return to the dashboard</a>.
|
||||
</p>
|
||||
</div>
|
||||
<!-- /.error-content -->
|
||||
</div>
|
||||
<!-- /.error-page -->
|
||||
</section>
|
||||
<!-- /.content -->
|
||||
{% endblock %}
|
@ -101,11 +101,16 @@
|
||||
{% if google_enabled %}
|
||||
<a href="{{ url_for('google_login') }}">Google oauth login</a>
|
||||
{% endif %}
|
||||
{% if saml_enabled %}
|
||||
<br>
|
||||
<a href="{{ url_for('saml_login') }}">SAML login</a>
|
||||
{% endif %}
|
||||
{% if github_enabled %}
|
||||
<br>
|
||||
<a href="{{ url_for('github_login') }}">Github oauth login</a>
|
||||
{% endif %}
|
||||
<br>
|
||||
{% if signup_enabled %}
|
||||
<br>
|
||||
<a href="{{ url_for('register') }}" class="text-center">Create an account </a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@
|
||||
<div class="col-lg-12">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Edit my profile</h3>
|
||||
<h3 class="box-title">Edit my profile{% if external_account %} [Disabled - Authenticated externally]{% endif %}</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<!-- Custom Tabs -->
|
||||
@ -29,10 +29,10 @@
|
||||
Info</a></li>
|
||||
<li><a href="#tabs-avatar" data-toggle="tab">Change
|
||||
Avatar</a></li>
|
||||
<li><a href="#tabs-password" data-toggle="tab">Change
|
||||
{% if not external_account %}<li><a href="#tabs-password" data-toggle="tab">Change
|
||||
Password</a></li>
|
||||
<li><a href="#tabs-authentication" data-toggle="tab">Authentication
|
||||
</a></li>
|
||||
</a></li>{% endif %}>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="tabs-personal">
|
||||
@ -40,21 +40,21 @@
|
||||
<div class="form-group">
|
||||
<label for="firstname">First Name</label> <input type="text"
|
||||
class="form-control" name="firstname" id="firstname"
|
||||
placeholder="{{ current_user.firstname }}">
|
||||
placeholder="{{ current_user.firstname }}" {% if external_account %}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 }}">
|
||||
placeholder="{{ current_user.lastname }}" {% if external_account %}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 }}">
|
||||
</div>
|
||||
placeholder="{{ current_user.email }}" {% if external_account %}disabled{% endif %}>
|
||||
</div>{% if not external_account %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-flat btn-primary">Submit</button>
|
||||
</div>
|
||||
</div>{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="tabs-avatar">
|
||||
@ -69,25 +69,25 @@
|
||||
else %} <img
|
||||
src="{{ current_user.email|email_to_gravatar_url(size=200) }}"
|
||||
alt="" /> {% endif %}
|
||||
</div>
|
||||
</div>{% if not external_account %}
|
||||
<div>
|
||||
<label for="file">Select image</label> <input type="file"
|
||||
id="file" name="file">
|
||||
</div>
|
||||
</div>
|
||||
</div>{% endif %}
|
||||
</div>{% if not external_account %}
|
||||
<div>
|
||||
<span class="label label-danger">NOTE! </span> <span> Only
|
||||
supports <strong>.PNG, .JPG, .JPEG</strong>. The best size
|
||||
to use is <strong>200x200</strong>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>{% endif %}
|
||||
</div>{% if not external_account %}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-flat btn-primary">Submit</button>
|
||||
</div>
|
||||
</div>{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane" id="tabs-password">
|
||||
{% if not external_account %}<div class="tab-pane" id="tabs-password">
|
||||
{% if not current_user.password %} Your account password is
|
||||
managed via LDAP which isn't supported to change here. {% else
|
||||
%}
|
||||
@ -95,15 +95,15 @@
|
||||
<div class="form-group">
|
||||
<label for="password">New Password</label> <input
|
||||
type="password" class="form-control" name="password"
|
||||
id="newpassword" />
|
||||
id="newpassword" {% if external_account %}disabled{% endif %} />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="rpassword">Re-type New Password</label> <input
|
||||
type="password" class="form-control" name="rpassword"
|
||||
id="rpassword" />
|
||||
id="rpassword" {% if external_account %}disabled{% endif %} />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-flat btn-primary">Change
|
||||
<button type="submit" class="btn btn-flat btn-primary" {% if external_account %}disabled{% endif %}>Change
|
||||
password</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -112,7 +112,7 @@
|
||||
<div class="tab-pane" id="tabs-authentication">
|
||||
<form action="{{ user_profile }}" method="post">
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="otp_toggle" class="otp_toggle" {% if current_user.otp_secret %}checked{% endif %}>
|
||||
<input type="checkbox" id="otp_toggle" class="otp_toggle" {% if current_user.otp_secret %}checked{% endif %} {% if external_account %}disabled{% endif %}>
|
||||
<label for="otp_toggle">Enable Two Factor Authentication</label>
|
||||
{% if current_user.otp_secret %}
|
||||
<div id="token_information">
|
||||
@ -124,7 +124,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
234
app/views.py
234
app/views.py
@ -22,6 +22,10 @@ from app.lib import utils
|
||||
from app.lib.log import logger
|
||||
from app.decorators import admin_role_required, can_access_domain
|
||||
|
||||
if app.config['SAML_ENABLED']:
|
||||
from onelogin.saml2.auth import OneLogin_Saml2_Auth
|
||||
from onelogin.saml2.utils import OneLogin_Saml2_Utils
|
||||
|
||||
# LOG CONFIG
|
||||
logging = logger('MODEL', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config()
|
||||
|
||||
@ -184,6 +188,70 @@ def github_login():
|
||||
return abort(400)
|
||||
return github.authorize(callback=url_for('authorized', _external=True))
|
||||
|
||||
@app.route('/saml/login')
|
||||
def saml_login():
|
||||
if not app.config.get('SAML_ENABLED'):
|
||||
return 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))
|
||||
|
||||
@app.route('/saml/metadata')
|
||||
def saml_metadata():
|
||||
if not app.config.get('SAML_ENABLED'):
|
||||
return 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
|
||||
|
||||
@app.route('/saml/authorized', methods=['GET', 'POST'])
|
||||
def saml_authorized():
|
||||
errors = []
|
||||
if not app.config.get('SAML_ENABLED'):
|
||||
return 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']))
|
||||
user = User.query.filter_by(username=session['samlNameId'].lower()).first()
|
||||
if not user:
|
||||
# create user
|
||||
user = User(username=session['samlNameId'],
|
||||
plain_text_password = None,
|
||||
email=session['samlNameId'])
|
||||
user.create_local_user()
|
||||
session['user_id'] = user.id
|
||||
if session['samlUserdata'].has_key("email"):
|
||||
user.email = session['samlUserdata']["email"][0].lower()
|
||||
if session['samlUserdata'].has_key("givenname"):
|
||||
user.firstname = session['samlUserdata']["givenname"][0]
|
||||
if session['samlUserdata'].has_key("surname"):
|
||||
user.lastname = session['samlUserdata']["surname"][0]
|
||||
user.plain_text_password = None
|
||||
user.update_profile()
|
||||
session['external_auth'] = True
|
||||
login_user(user, remember=False)
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
return render_template('errors/SAML.html', errors=errors)
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
@login_manager.unauthorized_handler
|
||||
@ -191,9 +259,10 @@ def login():
|
||||
LOGIN_TITLE = app.config['LOGIN_TITLE'] if 'LOGIN_TITLE' in app.config.keys() else ''
|
||||
BASIC_ENABLED = app.config['BASIC_ENABLED']
|
||||
SIGNUP_ENABLED = app.config['SIGNUP_ENABLED']
|
||||
LDAP_ENABLE = app.config.get('LDAP_ENABLE')
|
||||
LDAP_ENABLED = app.config.get('LDAP_ENABLED')
|
||||
GITHUB_ENABLE = app.config.get('GITHUB_OAUTH_ENABLE')
|
||||
GOOGLE_ENABLE = app.config.get('GOOGLE_OAUTH_ENABLE')
|
||||
SAML_ENABLED = app.config.get('SAML_ENABLED')
|
||||
|
||||
if g.user is not None and current_user.is_authenticated:
|
||||
return redirect(url_for('dashboard'))
|
||||
@ -209,7 +278,7 @@ def login():
|
||||
user = User(username=email,
|
||||
firstname=first_name,
|
||||
lastname=surname,
|
||||
plain_text_password=gen_salt(7),
|
||||
plain_text_password=None,
|
||||
email=email)
|
||||
|
||||
result = user.create_local_user()
|
||||
@ -219,6 +288,7 @@ def login():
|
||||
|
||||
session['user_id'] = user.id
|
||||
login_user(user, remember = False)
|
||||
session['external_auth'] = True
|
||||
return redirect(url_for('index'))
|
||||
|
||||
if 'github_token' in session:
|
||||
@ -228,7 +298,7 @@ def login():
|
||||
if not user:
|
||||
# create user
|
||||
user = User(username=user_info['name'],
|
||||
plain_text_password=gen_salt(7),
|
||||
plain_text_password=None,
|
||||
email=user_info['email'])
|
||||
|
||||
result = user.create_local_user()
|
||||
@ -237,6 +307,7 @@ def login():
|
||||
return redirect(url_for('login'))
|
||||
|
||||
session['user_id'] = user.id
|
||||
session['external_auth'] = True
|
||||
login_user(user, remember = False)
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@ -244,7 +315,8 @@ def login():
|
||||
return render_template('login.html',
|
||||
github_enabled=GITHUB_ENABLE,
|
||||
google_enabled=GOOGLE_ENABLE,
|
||||
ldap_enabled=LDAP_ENABLE, login_title=LOGIN_TITLE,
|
||||
saml_enabled=SAML_ENABLED,
|
||||
ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE,
|
||||
basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
|
||||
|
||||
# process login
|
||||
@ -270,18 +342,37 @@ def login():
|
||||
try:
|
||||
auth = user.is_validate(method=auth_method)
|
||||
if auth == False:
|
||||
return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLE, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
|
||||
return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLED,
|
||||
login_title=LOGIN_TITLE,
|
||||
basic_enabled=BASIC_ENABLED,
|
||||
signup_enabled=SIGNUP_ENABLED,
|
||||
github_enabled=GITHUB_ENABLE,
|
||||
saml_enabled=SAML_ENABLED)
|
||||
except Exception as e:
|
||||
return render_template('login.html', error=e, ldap_enabled=LDAP_ENABLE, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
|
||||
return render_template('login.html', error=e, ldap_enabled=LDAP_ENABLED, login_title=LOGIN_TITLE,
|
||||
basic_enabled=BASIC_ENABLED,
|
||||
signup_enabled=SIGNUP_ENABLED,
|
||||
github_enabled=GITHUB_ENABLE,
|
||||
saml_enabled=SAML_ENABLED)
|
||||
|
||||
# check if user enabled OPT authentication
|
||||
if user.otp_secret:
|
||||
if otp_token:
|
||||
good_token = user.verify_totp(otp_token)
|
||||
if not good_token:
|
||||
return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLE, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
|
||||
return render_template('login.html', error='Invalid credentials', ldap_enabled=LDAP_ENABLED,
|
||||
login_title=LOGIN_TITLE,
|
||||
basic_enabled=BASIC_ENABLED,
|
||||
signup_enabled=SIGNUP_ENABLED,
|
||||
github_enabled=GITHUB_ENABLE,
|
||||
saml_enabled=SAML_ENABLED)
|
||||
else:
|
||||
return render_template('login.html', error='Token required', ldap_enabled=LDAP_ENABLE, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
|
||||
return render_template('login.html', error='Token required', ldap_enabled=LDAP_ENABLED,
|
||||
login_title=LOGIN_TITLE,
|
||||
basic_enabled=BASIC_ENABLED,
|
||||
signup_enabled=SIGNUP_ENABLED,
|
||||
github_enabled = GITHUB_ENABLE,
|
||||
saml_enabled = SAML_ENABLED)
|
||||
|
||||
login_user(user, remember = remember_me)
|
||||
return redirect(request.args.get('next') or url_for('index'))
|
||||
@ -297,22 +388,53 @@ def login():
|
||||
|
||||
try:
|
||||
result = user.create_local_user()
|
||||
if result['status'] == True:
|
||||
return render_template('login.html', username=username, password=password, ldap_enabled=LDAP_ENABLE, login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED)
|
||||
if result == True:
|
||||
return render_template('login.html', username=username, password=password, ldap_enabled=LDAP_ENABLED,
|
||||
login_title=LOGIN_TITLE, basic_enabled=BASIC_ENABLED, signup_enabled=SIGNUP_ENABLED,
|
||||
github_enabled=GITHUB_ENABLE,saml_enabled=SAML_ENABLED)
|
||||
else:
|
||||
return render_template('register.html', error=result['msg'])
|
||||
except Exception as e:
|
||||
return render_template('register.html', error=e)
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
def clear_session():
|
||||
session.pop('user_id', None)
|
||||
session.pop('github_token', None)
|
||||
session.pop('google_token', None)
|
||||
session.clear()
|
||||
logout_user()
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
if app.config.get('SAML_ENABLED') and 'samlSessionIndex' in session and app.config.get('SAML_LOGOUT'):
|
||||
req = utils.prepare_flask_request(request)
|
||||
auth = utils.init_saml_auth(req)
|
||||
if app.config.get('SAML_LOGOUT_URL'):
|
||||
return redirect(auth.logout(name_id_format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
return_to = app.config.get('SAML_LOGOUT_URL'),
|
||||
session_index = session['samlSessionIndex'], name_id=session['samlNameId']))
|
||||
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('login'))
|
||||
|
||||
@app.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 app.config.get('SAML_LOGOUT_URL') is not None:
|
||||
return redirect(app.config.get('SAML_LOGOUT_URL'))
|
||||
else:
|
||||
return redirect(url_for('login'))
|
||||
else:
|
||||
return render_template('errors/SAML.html', errors=errors)
|
||||
|
||||
@app.route('/dashboard', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@ -330,7 +452,7 @@ def dashboard():
|
||||
uptime = list([uptime for uptime in statistics if uptime['name'] == 'uptime'])[0]['value']
|
||||
else:
|
||||
uptime = 0
|
||||
return render_template('dashboard.html', domain_count=domain_count, users=users, history_number=history_number, uptime=uptime, histories=history)
|
||||
return render_template('dashboard.html', domain_count=domain_count, users=users, history_number=history_number, uptime=uptime, histories=history,pdns_version=app.config['PDNS_VERSION'])
|
||||
|
||||
|
||||
@app.route('/dashboard-domains', methods=['GET'])
|
||||
@ -407,7 +529,9 @@ def dashboard_domains():
|
||||
def domain(domain_name):
|
||||
r = Record()
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if domain:
|
||||
if not domain:
|
||||
return redirect(url_for('error', code=404))
|
||||
|
||||
# query domain info from PowerDNS API
|
||||
zone_info = r.get_record_data(domain.name)
|
||||
if zone_info:
|
||||
@ -424,18 +548,21 @@ def domain(domain_name):
|
||||
for subrecord in jr['records']:
|
||||
record = Record(name=jr['name'], type=jr['type'], status='Disabled' if subrecord['disabled'] else 'Active', ttl=jr['ttl'], data=subrecord['content'])
|
||||
records.append(record)
|
||||
else:
|
||||
for jr in jrecords:
|
||||
if jr['type'] in app.config['RECORDS_ALLOW_EDIT']:
|
||||
record = Record(name=jr['name'], type=jr['type'], status='Disabled' if jr['disabled'] else 'Active', ttl=jr['ttl'], data=jr['content'])
|
||||
records.append(record)
|
||||
if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name):
|
||||
editable_records = app.config['RECORDS_ALLOW_EDIT']
|
||||
else:
|
||||
editable_records = app.config['REVERSE_ALLOW_EDIT']
|
||||
return render_template('domain.html', domain=domain, records=records, editable_records=editable_records)
|
||||
else:
|
||||
return redirect(url_for('error', code=404))
|
||||
for jr in jrecords:
|
||||
if jr['type'] in app.config['RECORDS_ALLOW_EDIT']:
|
||||
record = Record(name=jr['name'], type=jr['type'], status='Disabled' if jr['disabled'] else 'Active', ttl=jr['ttl'], data=jr['content'])
|
||||
records.append(record)
|
||||
if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name):
|
||||
editable_records = app.config['FORWARD_RECORDS_ALLOW_EDIT']
|
||||
else:
|
||||
editable_records = app.config['REVERSE_RECORDS_ALLOW_EDIT']
|
||||
return render_template('domain.html', domain=domain, records=records, editable_records=editable_records,pdns_version=app.config['PDNS_VERSION'])
|
||||
|
||||
|
||||
@app.route('/admin/domain/add', methods=['GET', 'POST'])
|
||||
@ -539,6 +666,30 @@ def domain_management(domain_name):
|
||||
return redirect(url_for('domain_management', domain_name=domain_name))
|
||||
|
||||
|
||||
@app.route('/admin/domain/<path:domain_name>/change_soa_setting', methods=['POST'])
|
||||
@login_required
|
||||
@admin_role_required
|
||||
def domain_change_soa_edit_api(domain_name):
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if not domain:
|
||||
return redirect(url_for('error', code=404))
|
||||
new_setting = request.form.get('soa_edit_api')
|
||||
if new_setting == None:
|
||||
return redirect(url_for('error', code=500))
|
||||
if new_setting == '0':
|
||||
return redirect(url_for('domain_management', 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()
|
||||
d = Domain(name=domain_name)
|
||||
domain_user_ids = d.get_user()
|
||||
return render_template('domain_management.html', domain=domain, users=users, domain_user_ids=domain_user_ids, status=status)
|
||||
else:
|
||||
return redirect(url_for('error', code=500))
|
||||
|
||||
|
||||
@app.route('/domain/<path:domain_name>/apply', methods=['POST'], strict_slashes=False)
|
||||
@login_required
|
||||
@can_access_domain
|
||||
@ -602,14 +753,36 @@ def record_delete(domain_name, record_name, record_type):
|
||||
|
||||
|
||||
@app.route('/domain/<path:domain_name>/dnssec', methods=['GET'])
|
||||
@can_access_domain
|
||||
@login_required
|
||||
@can_access_domain
|
||||
def domain_dnssec(domain_name):
|
||||
domain = Domain()
|
||||
dnssec = domain.get_domain_dnssec(domain_name)
|
||||
return make_response(jsonify(dnssec), 200)
|
||||
|
||||
|
||||
@app.route('/domain/<path:domain_name>/dnssec/enable', methods=['GET'])
|
||||
@login_required
|
||||
@can_access_domain
|
||||
def domain_dnssec_enable(domain_name):
|
||||
domain = Domain()
|
||||
dnssec = domain.enable_domain_dnssec(domain_name)
|
||||
return make_response(jsonify(dnssec), 200)
|
||||
|
||||
|
||||
@app.route('/domain/<path:domain_name>/dnssec/disable', methods=['GET'])
|
||||
@login_required
|
||||
@can_access_domain
|
||||
def domain_dnssec_disable(domain_name):
|
||||
domain = Domain()
|
||||
dnssec = domain.get_domain_dnssec(domain_name)
|
||||
|
||||
for key in dnssec['dnssec']:
|
||||
response = domain.delete_dnssec_key(domain_name,key['id']);
|
||||
|
||||
return make_response(jsonify( { 'status': 'ok', 'msg': 'DNSSEC removed.' } ))
|
||||
|
||||
|
||||
@app.route('/domain/<path:domain_name>/managesetting', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@admin_role_required
|
||||
@ -751,7 +924,7 @@ def create_template_from_zone():
|
||||
return make_response(jsonify({'status': 'error', 'msg': 'Error when applying new changes'}), 500)
|
||||
|
||||
|
||||
@app.route('/template/<string:template>/edit', methods=['GET'])
|
||||
@app.route('/template/<path:template>/edit', methods=['GET'])
|
||||
@login_required
|
||||
@admin_role_required
|
||||
def edit_template(template):
|
||||
@ -771,7 +944,7 @@ def edit_template(template):
|
||||
return redirect(url_for('templates'))
|
||||
|
||||
|
||||
@app.route('/template/<string:template>/apply', methods=['POST'], strict_slashes=False)
|
||||
@app.route('/template/<path:template>/apply', methods=['POST'], strict_slashes=False)
|
||||
@login_required
|
||||
def apply_records(template):
|
||||
try:
|
||||
@ -801,7 +974,7 @@ def apply_records(template):
|
||||
return make_response(jsonify({'status': 'error', 'msg': 'Error when applying new changes'}), 500)
|
||||
|
||||
|
||||
@app.route('/template/<string:template>/delete', methods=['GET'])
|
||||
@app.route('/template/<path:template>/delete', methods=['GET'])
|
||||
@login_required
|
||||
@admin_role_required
|
||||
def delete_template(template):
|
||||
@ -976,8 +1149,11 @@ def admin_settings_edit(setting):
|
||||
@app.route('/user/profile', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def user_profile():
|
||||
if request.method == 'GET':
|
||||
return render_template('user_profile.html')
|
||||
external_account = False
|
||||
if session.has_key('external_auth'):
|
||||
external_account = session['external_auth']
|
||||
if request.method == 'GET' or external_account:
|
||||
return render_template('user_profile.html', external_account=external_account)
|
||||
if request.method == 'POST':
|
||||
# get new profile info
|
||||
firstname = request.form['firstname'] if 'firstname' in request.form else ''
|
||||
@ -1011,7 +1187,7 @@ def user_profile():
|
||||
user = User(username=current_user.username, plain_text_password=new_password, firstname=firstname, lastname=lastname, email=email, avatar=save_file_name, reload_info=False)
|
||||
user.update_profile()
|
||||
|
||||
return render_template('user_profile.html')
|
||||
return render_template('user_profile.html', external_account=external_account)
|
||||
|
||||
|
||||
@app.route('/user/avatar/<path:filename>')
|
||||
@ -1065,12 +1241,12 @@ def dyndns_update():
|
||||
domain = None
|
||||
domain_segments = hostname.split('.')
|
||||
for index in range(len(domain_segments)):
|
||||
domain_segments.pop(0)
|
||||
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)
|
||||
|
@ -28,15 +28,15 @@ SQLA_DB_HOST = 'mysqlhostorip'
|
||||
SQLA_DB_NAME = 'powerdnsadmin'
|
||||
|
||||
#MySQL
|
||||
SQLALCHEMY_DATABASE_URI = 'mysql://'+SQLA_DB_USER+':'\
|
||||
+SQLA_DB_PASSWORD+'@'+SQLA_DB_HOST+'/'+SQLA_DB_NAME
|
||||
#SQLALCHEMY_DATABASE_URI = 'mysql://'+SQLA_DB_USER+':'\
|
||||
# +SQLA_DB_PASSWORD+'@'+SQLA_DB_HOST+'/'+SQLA_DB_NAME
|
||||
#SQLite
|
||||
#SQLALCHEMY_DATABASE_URI = 'sqlite:////path/to/your/pdns.db'
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///pdns.db'
|
||||
SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||
|
||||
# LDAP CONFIG
|
||||
LDAP_ENABLE = False
|
||||
LDAP_ENABLED = False
|
||||
LDAP_TYPE = 'ldap'
|
||||
LDAP_URI = 'ldaps://your-ldap-server:636'
|
||||
# with LDAP_BIND_TYPE you can specify 'direct' or 'search' to use user credentials
|
||||
@ -45,6 +45,9 @@ LDAP_BIND_TYPE= 'direct' # direct or search
|
||||
LDAP_USERNAME = 'cn=dnsuser,ou=users,ou=services,dc=duykhanh,dc=me'
|
||||
LDAP_PASSWORD = 'dnsuser'
|
||||
LDAP_SEARCH_BASE = 'ou=System Admins,ou=People,dc=duykhanh,dc=me'
|
||||
LDAP_GROUP_SECURITY = False
|
||||
LDAP_ADMIN_GROUP = 'CN=PowerDNS-Admin Admin,OU=Custom,DC=ivan,DC=local'
|
||||
LDAP_USER_GROUP = 'CN=PowerDNS-Admin User,OU=Custom,DC=ivan,DC=local'
|
||||
# Additional options only if LDAP_TYPE=ldap
|
||||
LDAP_USERNAMEFIELD = 'uid'
|
||||
LDAP_FILTER = '(objectClass=inetorgperson)'
|
||||
@ -73,6 +76,7 @@ GITHUB_OAUTH_URL = 'http://127.0.0.1:9191/api/v3/'
|
||||
GITHUB_OAUTH_TOKEN = 'http://127.0.0.1:9191/oauth/token'
|
||||
GITHUB_OAUTH_AUTHORIZE = 'http://127.0.0.1:9191/oauth/authorize'
|
||||
|
||||
|
||||
# Google OAuth
|
||||
GOOGLE_OAUTH_ENABLE = False
|
||||
GOOGLE_OAUTH_CLIENT_ID = ' '
|
||||
@ -85,6 +89,28 @@ GOOGLE_TOKEN_PARAMS = {
|
||||
GOOGLE_AUTHORIZE_URL='https://accounts.google.com/o/oauth2/auth'
|
||||
GOOGLE_BASE_URL='https://www.googleapis.com/oauth2/v1/'
|
||||
|
||||
# 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_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'
|
||||
|
||||
#Default Auth
|
||||
BASIC_ENABLED = True
|
||||
SIGNUP_ENABLED = True
|
||||
@ -95,10 +121,9 @@ PDNS_API_KEY = 'you never know'
|
||||
PDNS_VERSION = '4.1.1'
|
||||
|
||||
# RECORDS ALLOWED TO EDIT
|
||||
RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'PTR', 'SPF', 'SRV', 'TXT', 'NS']
|
||||
|
||||
# RECORDS ALLOWED TO EDIT FOR REVERSE DOMAINS
|
||||
REVERSE_ALLOW_EDIT = ['PTR', 'NS']
|
||||
RECORDS_ALLOW_EDIT = ['SOA', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'PTR', 'SPF', 'SRV', 'TXT', 'LOC', 'NS', 'PTR']
|
||||
FORWARD_RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'PTR', 'SPF', 'SRV', 'TXT', 'LOC' 'NS']
|
||||
REVERSE_RECORDS_ALLOW_EDIT = ['TXT', 'LOC', 'NS', 'PTR']
|
||||
|
||||
# EXPERIMENTAL FEATURES
|
||||
PRETTY_IPV6_PTR = False
|
||||
|
@ -30,7 +30,7 @@ SIGNUP_ENABLED = True
|
||||
|
||||
|
||||
# LDAP CONFIG
|
||||
LDAP_ENABLE = False
|
||||
LDAP_ENABLED = False
|
||||
LDAP_TYPE = 'ldap'
|
||||
LDAP_URI = 'ldaps://your-ldap-server:636'
|
||||
# with LDAP_BIND_TYPE you can specify 'direct' or 'search' to use user credentials
|
||||
@ -82,6 +82,29 @@ GOOGLE_AUTHORIZE_URL='https://accounts.google.com/o/oauth2/auth'
|
||||
GOOGLE_BASE_URL='https://www.googleapis.com/oauth2/v1/'
|
||||
|
||||
|
||||
# SAML AUTHENTICATION
|
||||
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_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'
|
||||
|
||||
|
||||
# POWERDNS CONFIG
|
||||
PDNS_STATS_URL = 'http://192.168.100.100:8081/'
|
||||
PDNS_API_KEY = 'changeme'
|
||||
|
29
docker/DOCKER.md
Normal file
29
docker/DOCKER.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Docker support
|
||||
This is a updated version of the current docker support.
|
||||
Container support is only for development purposes and should not be used in production without your own modificatins.
|
||||
|
||||
It's not needed to reload the container after you make changes in your current branch.
|
||||
|
||||
Images are currently not available in docker hub or other repository, so you have to build them yourself.
|
||||
|
||||
After a successful launch PowerDNS-Admin is reachable at http://localhost:9393
|
||||
|
||||
PowerDNS runs op port localhost udp/5353
|
||||
|
||||
|
||||
## Basic commands:
|
||||
### Build images
|
||||
cd to this directory
|
||||
|
||||
```# ./build-images.sh```
|
||||
|
||||
### Run containers
|
||||
Build the images before you run this command.
|
||||
|
||||
```# docker-compose up```
|
||||
|
||||
### Stop containers
|
||||
```# docker-compose stop```
|
||||
|
||||
### Remove containers
|
||||
```# docker-compose rm```
|
42
docker/PowerDNS-Admin/Dockerfile
Normal file
42
docker/PowerDNS-Admin/Dockerfile
Normal file
@ -0,0 +1,42 @@
|
||||
# PowerDNS-Admin
|
||||
# Original from:
|
||||
# https://github.com/ngoduykhanh/PowerDNS-Admin
|
||||
#
|
||||
# Initial image by winggundamth(/powerdns-mysql:trusty)
|
||||
#
|
||||
#
|
||||
FROM alpine
|
||||
MAINTAINER Jeroen Boonstra <jeroen [at] provider.nl>
|
||||
|
||||
ENV APP_USER=web APP_NAME=powerdns-admin
|
||||
ENV APP_PATH=/home/$APP_USER/$APP_NAME
|
||||
|
||||
|
||||
RUN apk add --update \
|
||||
sudo \
|
||||
python \
|
||||
libxml2 \
|
||||
xmlsec \
|
||||
git \
|
||||
python-dev \
|
||||
py-pip \
|
||||
build-base \
|
||||
libxml2-dev \
|
||||
xmlsec-dev \
|
||||
libffi-dev \
|
||||
openldap-dev \
|
||||
&& adduser -S web
|
||||
|
||||
RUN sudo -u $APP_USER -H git clone --depth=1 \
|
||||
https://github.com/thomasDOTde/PowerDNS-Admin $APP_PATH
|
||||
|
||||
RUN pip install -r $APP_PATH/requirements.txt
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
|
||||
USER $APP_USER
|
||||
WORKDIR $APP_PATH
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["python", "run.py"]
|
||||
EXPOSE 9393
|
||||
VOLUME ["/var/log"]
|
12
docker/PowerDNS-Admin/docker-entrypoint.sh
Executable file
12
docker/PowerDNS-Admin/docker-entrypoint.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$WAITFOR_DB" -a ! -f "$APP_PATH/config.py" ]; then
|
||||
cp "$APP_PATH/config_template_docker.py" "$APP_PATH/config.py"
|
||||
fi
|
||||
|
||||
cd $APP_PATH && python create_db.py
|
||||
|
||||
# Start PowerDNS Admin
|
||||
exec "$@"
|
40
docker/PowerDNS-MySQL/Dockerfile
Normal file
40
docker/PowerDNS-MySQL/Dockerfile
Normal file
@ -0,0 +1,40 @@
|
||||
# PowerDNS Authoritative Server with MySQL backend
|
||||
# https://www.powerdns.com
|
||||
#
|
||||
# The PowerDNS Authoritative Server is the only solution that enables
|
||||
# authoritative DNS service from all major databases, including but not limited
|
||||
# to MySQL, PostgreSQL, SQLite3, Oracle, Sybase, Microsoft SQL Server, LDAP and
|
||||
# plain text files.
|
||||
|
||||
FROM winggundamth/ubuntu-base:trusty
|
||||
MAINTAINER Jirayut Nimsaeng <w [at] winginfotech.net>
|
||||
ENV FROM_BASE=trusty-20160503.1
|
||||
|
||||
# 1) Add PowerDNS repository https://repo.powerdns.com
|
||||
# 2) Install PowerDNS server
|
||||
# 3) Clean to reduce Docker image size
|
||||
ARG APT_CACHER_NG
|
||||
COPY build-files /build-files
|
||||
RUN [ -n "$APT_CACHER_NG" ] && \
|
||||
echo "Acquire::http::Proxy \"$APT_CACHER_NG\";" \
|
||||
> /etc/apt/apt.conf.d/11proxy || true; \
|
||||
apt-get update && \
|
||||
apt-get install -y curl && \
|
||||
curl https://repo.powerdns.com/FD380FBB-pub.asc | apt-key add - && \
|
||||
echo 'deb [arch=amd64] http://repo.powerdns.com/ubuntu trusty-auth-40 main' \
|
||||
> /etc/apt/sources.list.d/pdns-$(lsb_release -cs).list && \
|
||||
mv /build-files/pdns-pin /etc/apt/preferences.d/pdns && \
|
||||
apt-get update && \
|
||||
apt-get install -y pdns-server pdns-backend-mysql mysql-client && \
|
||||
mv /build-files/pdns.mysql.conf /etc/powerdns/pdns.d/pdns.mysql.conf && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /etc/apt/apt.conf.d/11proxy /build-files \
|
||||
/etc/powerdns/pdns.d/pdns.simplebind.conf
|
||||
|
||||
# 1) Copy Docker entrypoint script
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
EXPOSE 53/udp 53 8081
|
||||
VOLUME ["/var/log", "/etc/powerdns"]
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["/usr/sbin/pdns_server", "--guardian=yes"]
|
3
docker/PowerDNS-MySQL/build-files/pdns-pin
Normal file
3
docker/PowerDNS-MySQL/build-files/pdns-pin
Normal file
@ -0,0 +1,3 @@
|
||||
Package: pdns-*
|
||||
Pin: origin repo.powerdns.com
|
||||
Pin-Priority: 600
|
6
docker/PowerDNS-MySQL/build-files/pdns.mysql.conf
Normal file
6
docker/PowerDNS-MySQL/build-files/pdns.mysql.conf
Normal file
@ -0,0 +1,6 @@
|
||||
launch+=gmysql
|
||||
gmysql-port=3306
|
||||
gmysql-host=172.17.0.1
|
||||
gmysql-password=CHANGEME
|
||||
gmysql-user=powerdns
|
||||
gmysql-dbname=powerdns
|
89
docker/PowerDNS-MySQL/docker-entrypoint.sh
Executable file
89
docker/PowerDNS-MySQL/docker-entrypoint.sh
Executable file
@ -0,0 +1,89 @@
|
||||
#!/bin/sh
|
||||
# Author: Jirayut 'Dear' Nimsaeng
|
||||
#
|
||||
set -e
|
||||
|
||||
PDNS_CONF_PATH="/etc/powerdns/pdns.conf"
|
||||
PDNS_MYSQL_CONF_PATH="/etc/powerdns/pdns.d/pdns.mysql.conf"
|
||||
PDNS_MYSQL_HOST="localhost"
|
||||
PDNS_MYSQL_PORT="3306"
|
||||
PDNS_MYSQL_USERNAME="powerdns"
|
||||
PDNS_MYSQL_PASSWORD="$PDNS_DB_PASSWORD"
|
||||
PDNS_MYSQL_DBNAME="powerdns"
|
||||
|
||||
if [ -z "$PDNS_DB_PASSWORD" ]; then
|
||||
echo 'ERROR: PDNS_DB_PASSWORD environment variable not found'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configure variables
|
||||
if [ "$PDNS_DB_HOST" ]; then
|
||||
PDNS_MYSQL_HOST="$PDNS_DB_HOST"
|
||||
fi
|
||||
if [ "$PDNS_DB_PORT" ]; then
|
||||
PDNS_MYSQL_PORT="$PDNS_DB_PORT"
|
||||
fi
|
||||
if [ "$PDNS_DB_USERNAME" ]; then
|
||||
PDNS_MYSQL_USERNAME="$PDNS_DB_USERNAME"
|
||||
fi
|
||||
if [ "$PDNS_DB_NAME" ]; then
|
||||
PDNS_MYSQL_DBNAME="$PDNS_DB_NAME"
|
||||
fi
|
||||
|
||||
# Configure mysql backend
|
||||
sed -i \
|
||||
-e "s/^gmysql-host=.*/gmysql-host=$PDNS_MYSQL_HOST/g" \
|
||||
-e "s/^gmysql-port=.*/gmysql-port=$PDNS_MYSQL_PORT/g" \
|
||||
-e "s/^gmysql-user=.*/gmysql-user=$PDNS_MYSQL_USERNAME/g" \
|
||||
-e "s/^gmysql-password=.*/gmysql-password=$PDNS_MYSQL_PASSWORD/g" \
|
||||
-e "s/^gmysql-dbname=.*/gmysql-dbname=$PDNS_MYSQL_DBNAME/g" \
|
||||
$PDNS_MYSQL_CONF_PATH
|
||||
|
||||
if [ "$PDNS_SLAVE" != "1" ]; then
|
||||
# Configure to be master
|
||||
sed -i \
|
||||
-e "s/^#\?\smaster=.*/master=yes/g" \
|
||||
-e "s/^#\?\sslave=.*/slave=no/g" \
|
||||
$PDNS_CONF_PATH
|
||||
else
|
||||
# Configure to be slave
|
||||
sed -i \
|
||||
-e "s/^#\?\smaster=.*/master=no/g" \
|
||||
-e "s/^#\?\sslave=.*/slave=yes/g" \
|
||||
$PDNS_CONF_PATH
|
||||
fi
|
||||
|
||||
if [ "$PDNS_API_KEY" ]; then
|
||||
# Enable API
|
||||
sed -i \
|
||||
-e "s/^#\?\sapi=.*/api=yes/g" \
|
||||
-e "s!^#\?\sapi-logfile=.*!api-logfile=/dev/stdout!g" \
|
||||
-e "s/^#\?\sapi-key=.*/api-key=$PDNS_API_KEY/g" \
|
||||
-e "s/^#\?\swebserver=.*/webserver=yes/g" \
|
||||
-e "s/^#\?\swebserver-address=.*/webserver-address=0.0.0.0/g" \
|
||||
$PDNS_CONF_PATH
|
||||
fi
|
||||
|
||||
if [ "$PDNS_WEBSERVER_ALLOW_FROM" ]; then
|
||||
sed -i \
|
||||
"s/^#\?\swebserver-allow-from=.*/webserver-allow-from=$PDNS_WEBSERVER_ALLOW_FROM/g" \
|
||||
$PDNS_CONF_PATH
|
||||
fi
|
||||
|
||||
|
||||
MYSQL_COMMAND="mysql -h $PDNS_MYSQL_HOST -P $PDNS_MYSQL_PORT -u $PDNS_MYSQL_USERNAME -p$PDNS_MYSQL_PASSWORD"
|
||||
|
||||
until $MYSQL_COMMAND -e ";" ; do
|
||||
>&2 echo "MySQL is unavailable - sleeping"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
>&2 echo "MySQL is up - initial database if not exists"
|
||||
MYSQL_CHECK_IF_HAS_TABLE="SELECT COUNT(DISTINCT table_name) FROM information_schema.columns WHERE table_schema = '$PDNS_MYSQL_DBNAME';"
|
||||
MYSQL_NUM_TABLE=$($MYSQL_COMMAND --batch --skip-column-names -e "$MYSQL_CHECK_IF_HAS_TABLE")
|
||||
if [ "$MYSQL_NUM_TABLE" -eq 0 ]; then
|
||||
$MYSQL_COMMAND -D $PDNS_MYSQL_DBNAME < /usr/share/doc/pdns-backend-mysql/schema.mysql.sql
|
||||
fi
|
||||
|
||||
# Start PowerDNS
|
||||
exec "$@"
|
10
docker/build-images.sh
Executable file
10
docker/build-images.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
IMAGES=(PowerDNS-MySQL PowerDNS-Admin)
|
||||
for IMAGE in "${IMAGES[@]}"
|
||||
do
|
||||
echo building $(basename $IMAGE | tr '[A-Z]' '[a-z]')
|
||||
cd $IMAGE
|
||||
docker build -t $(basename $IMAGE | tr '[A-Z]' '[a-z]') .
|
||||
cd ..
|
||||
done
|
50
docker/docker-compose.yml
Normal file
50
docker/docker-compose.yml
Normal file
@ -0,0 +1,50 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
powerdns-authoritative:
|
||||
image: powerdns-mysql
|
||||
hostname: powerdns-authoritative
|
||||
depends_on:
|
||||
- powerdns-authoritative-mariadb
|
||||
links:
|
||||
- powerdns-authoritative-mariadb:mysqldb
|
||||
ports:
|
||||
- 5553:53/udp
|
||||
- 8081:8081
|
||||
environment:
|
||||
- PDNS_DB_HOST=mysqldb
|
||||
- PDNS_DB_USERNAME=root
|
||||
- PDNS_DB_NAME=powerdns
|
||||
- PDNS_DB_PASSWORD=PowerDNSPassword
|
||||
- PDNS_API_KEY=PowerDNSAPIKey
|
||||
|
||||
powerdns-authoritative-mariadb:
|
||||
image: mariadb:10.1.15
|
||||
hostname: powerdns-authoritative-mariadb
|
||||
environment:
|
||||
- MYSQL_DATABASE=powerdns
|
||||
- MYSQL_ROOT_PASSWORD=PowerDNSPassword
|
||||
|
||||
powerdns-admin:
|
||||
image: powerdns-admin
|
||||
hostname: powerdns-admin
|
||||
depends_on:
|
||||
- powerdns-admin-mariadb
|
||||
- powerdns-authoritative
|
||||
links:
|
||||
- powerdns-admin-mariadb:mysqldb
|
||||
- powerdns-authoritative:powerdns-server
|
||||
volumes:
|
||||
- ../:/home/web/powerdns-admin
|
||||
ports:
|
||||
- 9393:9393
|
||||
environment:
|
||||
- WAITFOR_DB=60
|
||||
|
||||
powerdns-admin-mariadb:
|
||||
image: mariadb:10.1.15
|
||||
hostname: powerdns-admin-mariadb
|
||||
environment:
|
||||
- MYSQL_DATABASE=powerdns-admin
|
||||
- MYSQL_ROOT_PASSWORD=PowerDNSAdminPassword
|
@ -14,3 +14,6 @@ pyotp==2.2.6
|
||||
qrcode==6.0
|
||||
dnspython==1.15.0
|
||||
gunicorn==19.7.1
|
||||
python3-saml
|
||||
pyOpenSSL>=0.15
|
||||
pytz>=2017.3
|
||||
|
2
run.py
2
run.py
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
from app import app
|
||||
from config import PORT
|
||||
|
||||
|
3
run_travis.sh
Normal file
3
run_travis.sh
Normal file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
python run.py&
|
||||
nosetests --with-coverage
|
Loading…
Reference in New Issue
Block a user