mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2025-06-15 04:26:05 +00:00
Refactoring the code
- Use Flask blueprint - Split model and views into smaller parts - Bug fixes - API adjustment
This commit is contained in:
0
powerdnsadmin/lib/__init__.py
Normal file
0
powerdnsadmin/lib/__init__.py
Normal file
48
powerdnsadmin/lib/certutil.py
Normal file
48
powerdnsadmin/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))
|
82
powerdnsadmin/lib/errors.py
Normal file
82
powerdnsadmin/lib/errors.py
Normal 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
|
39
powerdnsadmin/lib/helper.py
Normal file
39
powerdnsadmin/lib/helper.py
Normal 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
|
27
powerdnsadmin/lib/schema.py
Normal file
27
powerdnsadmin/lib/schema.py
Normal 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
337
powerdnsadmin/lib/utils.py
Normal 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"]
|
Reference in New Issue
Block a user