Overhauled app settings implementation to remove redundancy of definitions. Additionally, re-factored settings initialization code to allow for every setting to be defined by environment variable for both bare metal and Docker container deployments.

This commit is contained in:
Matt Scott 2023-04-14 07:12:02 -04:00
parent 9ddfde02b8
commit c842d09195
No known key found for this signature in database
GPG Key ID: A9A0AFFC0E079001
8 changed files with 734 additions and 704 deletions

View File

@ -1,151 +1,2 @@
# import everything from environment variables
import os
import sys
import json
# Defaults for Docker image
BIND_ADDRESS = '0.0.0.0'
PORT = 80 PORT = 80
SERVER_EXTERNAL_SSL = os.getenv('SERVER_EXTERNAL_SSL', True) SQLALCHEMY_DATABASE_URI = 'sqlite:////data/powerdns-admin.db'
SQLALCHEMY_DATABASE_URI = 'sqlite:////data/powerdns-admin.db'
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_HTTPONLY = True
SESSION_TYPE = 'sqlalchemy'
legal_envvars = (
'SECRET_KEY',
'OIDC_OAUTH_ENABLED',
'OIDC_OAUTH_KEY',
'OIDC_OAUTH_SECRET',
'OIDC_OAUTH_API_URL',
'OIDC_OAUTH_TOKEN_URL',
'OIDC_OAUTH_AUTHORIZE_URL',
'OIDC_OAUTH_METADATA_URL',
'OIDC_OAUTH_LOGOUT_URL',
'OIDC_OAUTH_SCOPE',
'OIDC_OAUTH_USERNAME',
'OIDC_OAUTH_FIRSTNAME',
'OIDC_OAUTH_LAST_NAME',
'OIDC_OAUTH_EMAIL',
'BIND_ADDRESS',
'PORT',
'SERVER_EXTERNAL_SSL',
'LOG_LEVEL',
'SALT',
'SQLALCHEMY_TRACK_MODIFICATIONS',
'SQLALCHEMY_DATABASE_URI',
'SQLALCHEMY_ENGINE_OPTIONS',
'MAIL_SERVER',
'MAIL_PORT',
'MAIL_DEBUG',
'MAIL_USE_TLS',
'MAIL_USE_SSL',
'MAIL_USERNAME',
'MAIL_PASSWORD',
'MAIL_DEFAULT_SENDER',
'SAML_ENABLED',
'SAML_DEBUG',
'SAML_PATH',
'SAML_METADATA_URL',
'SAML_METADATA_CACHE_LIFETIME',
'SAML_IDP_SSO_BINDING',
'SAML_IDP_ENTITY_ID',
'SAML_NAMEID_FORMAT',
'SAML_ATTRIBUTE_EMAIL',
'SAML_ATTRIBUTE_GIVENNAME',
'SAML_ATTRIBUTE_SURNAME',
'SAML_ATTRIBUTE_NAME',
'SAML_ATTRIBUTE_USERNAME',
'SAML_ATTRIBUTE_ADMIN',
'SAML_ATTRIBUTE_GROUP',
'SAML_GROUP_ADMIN_NAME',
'SAML_GROUP_TO_ACCOUNT_MAPPING',
'SAML_ATTRIBUTE_ACCOUNT',
'SAML_SP_ENTITY_ID',
'SAML_SP_CONTACT_NAME',
'SAML_SP_CONTACT_MAIL',
'SAML_SIGN_REQUEST',
'SAML_WANT_MESSAGE_SIGNED',
'SAML_LOGOUT',
'SAML_LOGOUT_URL',
'SAML_ASSERTION_ENCRYPTED',
'REMOTE_USER_LOGOUT_URL',
'REMOTE_USER_COOKIES',
'SIGNUP_ENABLED',
'LOCAL_DB_ENABLED',
'LDAP_ENABLED',
'SAML_CERT',
'SAML_KEY',
'SESSION_TYPE',
'SESSION_COOKIE_SECURE',
'CSRF_COOKIE_SECURE',
'CAPTCHA_ENABLE',
)
legal_envvars_int = ('PORT', 'MAIL_PORT', 'SAML_METADATA_CACHE_LIFETIME')
legal_envvars_bool = (
'SQLALCHEMY_TRACK_MODIFICATIONS',
'HSTS_ENABLED',
'MAIL_DEBUG',
'MAIL_USE_TLS',
'MAIL_USE_SSL',
'OIDC_OAUTH_ENABLED',
'SAML_ENABLED',
'SAML_DEBUG',
'SAML_SIGN_REQUEST',
'SAML_WANT_MESSAGE_SIGNED',
'SAML_LOGOUT',
'SAML_ASSERTION_ENCRYPTED',
'REMOTE_USER_ENABLED',
'SIGNUP_ENABLED',
'LOCAL_DB_ENABLED',
'LDAP_ENABLED',
'SESSION_COOKIE_SECURE',
'CSRF_COOKIE_SECURE',
'CAPTCHA_ENABLE',
'SERVER_EXTERNAL_SSL',
)
legal_envvars_dict = (
'SQLALCHEMY_ENGINE_OPTIONS',
)
def str2bool(v):
return v.lower() in ("true", "yes", "1")
def dictfromstr(v, ret):
try:
return json.loads(ret)
except Exception as e:
print('Cannot parse json {} for variable {}'.format(ret, v))
print(e)
raise ValueError
for v in legal_envvars:
ret = None
# _FILE suffix will allow to read value from file, useful for Docker containers.
# secrets feature
if v + '_FILE' in os.environ:
if v in os.environ:
raise AttributeError(
"Both {} and {} are set but are exclusive.".format(
v, v + '_FILE'))
with open(os.environ[v + '_FILE']) as f:
ret = f.read()
f.close()
elif v in os.environ:
ret = os.environ[v]
if ret is not None:
if v in legal_envvars_bool:
ret = str2bool(ret)
if v in legal_envvars_int:
ret = int(ret)
if v in legal_envvars_dict:
ret = dictfromstr(v, ret)
sys.modules[__name__].__dict__[v] = ret

View File

@ -4,11 +4,11 @@ from flask import Flask
from flask_mail import Mail from flask_mail import Mail
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
from flask_session import Session from flask_session import Session
from .lib import utils from .lib import utils
def create_app(config=None): def create_app(config=None):
from powerdnsadmin.lib.settings import AppSettings
from . import models, routes, services from . import models, routes, services
from .assets import assets from .assets import assets
app = Flask(__name__) app = Flask(__name__)
@ -50,6 +50,9 @@ def create_app(config=None):
elif config.endswith('.py'): elif config.endswith('.py'):
app.config.from_pyfile(config) app.config.from_pyfile(config)
# Load any settings defined with environment variables
AppSettings.load_environment(app)
# HSTS # HSTS
if app.config.get('HSTS_ENABLED'): if app.config.get('HSTS_ENABLED'):
from flask_sslify import SSLify from flask_sslify import SSLify

View File

@ -13,6 +13,7 @@ def admin_role_required(f):
""" """
Grant access if user is in Administrator role Grant access if user is in Administrator role
""" """
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if current_user.role.name != 'Administrator': if current_user.role.name != 'Administrator':
@ -26,6 +27,7 @@ def operator_role_required(f):
""" """
Grant access if user is in Operator role or higher Grant access if user is in Operator role or higher
""" """
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if current_user.role.name not in ['Administrator', 'Operator']: if current_user.role.name not in ['Administrator', 'Operator']:
@ -39,6 +41,7 @@ def history_access_required(f):
""" """
Grant access if user is in Operator role or higher, or Users can view history Grant access if user is in Operator role or higher, or Users can view history
""" """
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if current_user.role.name not in [ if current_user.role.name not in [
@ -57,6 +60,7 @@ def can_access_domain(f):
- user is in granted Account, or - user is in granted Account, or
- user is in granted Domain - user is in granted Domain
""" """
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if current_user.role.name not in ['Administrator', 'Operator']: if current_user.role.name not in ['Administrator', 'Operator']:
@ -83,10 +87,11 @@ def can_configure_dnssec(f):
- user is in Operator role or higher, or - user is in Operator role or higher, or
- dnssec_admins_only is off - dnssec_admins_only is off
""" """
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if current_user.role.name not in [ if current_user.role.name not in [
'Administrator', 'Operator' 'Administrator', 'Operator'
] and Setting().get('dnssec_admins_only'): ] and Setting().get('dnssec_admins_only'):
abort(403) abort(403)
@ -94,16 +99,18 @@ def can_configure_dnssec(f):
return decorated_function return decorated_function
def can_remove_domain(f): def can_remove_domain(f):
""" """
Grant access if: Grant access if:
- user is in Operator role or higher, or - user is in Operator role or higher, or
- allow_user_remove_domain is on - allow_user_remove_domain is on
""" """
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if current_user.role.name not in [ if current_user.role.name not in [
'Administrator', 'Operator' 'Administrator', 'Operator'
] and not Setting().get('allow_user_remove_domain'): ] and not Setting().get('allow_user_remove_domain'):
abort(403) abort(403)
return f(*args, **kwargs) return f(*args, **kwargs)
@ -111,17 +118,17 @@ def can_remove_domain(f):
return decorated_function return decorated_function
def can_create_domain(f): def can_create_domain(f):
""" """
Grant access if: Grant access if:
- user is in Operator role or higher, or - user is in Operator role or higher, or
- allow_user_create_domain is on - allow_user_create_domain is on
""" """
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if current_user.role.name not in [ if current_user.role.name not in [
'Administrator', 'Operator' 'Administrator', 'Operator'
] and not Setting().get('allow_user_create_domain'): ] and not Setting().get('allow_user_create_domain'):
abort(403) abort(403)
return f(*args, **kwargs) return f(*args, **kwargs)
@ -144,11 +151,12 @@ def api_basic_auth(f):
# Remove "Basic " from the header value # Remove "Basic " from the header value
auth_header = auth_header[6:] auth_header = auth_header[6:]
auth_components = []
try: try:
auth_header = str(base64.b64decode(auth_header), 'utf-8') auth_header = str(base64.b64decode(auth_header), 'utf-8')
# NK: We use auth_components here as we don't know if we'll have a :, we split it maximum 1 times to grab the # NK: We use auth_components here as we don't know if we'll have a colon,
# username, the rest of the string would be the password. # we split it maximum 1 times to grab the username, the rest of the string would be the password.
auth_components = auth_header.split(':', maxsplit=1) auth_components = auth_header.split(':', maxsplit=1)
except (binascii.Error, UnicodeDecodeError) as e: except (binascii.Error, UnicodeDecodeError) as e:
current_app.logger.error( current_app.logger.error(
@ -211,16 +219,19 @@ def callback_if_request_body_contains_key(callback, http_methods=[], keys=[]):
If request body contains one or more of specified keys, call If request body contains one or more of specified keys, call
:param callback :param callback
""" """
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
check_current_http_method = not http_methods or request.method in http_methods check_current_http_method = not http_methods or request.method in http_methods
if (check_current_http_method and if (check_current_http_method and
set(request.get_json(force=True).keys()).intersection(set(keys)) set(request.get_json(force=True).keys()).intersection(set(keys))
): ):
callback(*args, **kwargs) callback(*args, **kwargs)
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function return decorated_function
return decorator return decorator
@ -246,16 +257,18 @@ def api_role_can(action, roles=None, allow_self=False):
except: except:
username = None username = None
if ( if (
(current_user.role.name in roles) or (current_user.role.name in roles) or
(allow_self and user_id and current_user.id == user_id) or (allow_self and user_id and current_user.id == user_id) or
(allow_self and username and current_user.username == username) (allow_self and username and current_user.username == username)
): ):
return f(*args, **kwargs) return f(*args, **kwargs)
msg = ( msg = (
"User {} with role {} does not have enough privileges to {}" "User {} with role {} does not have enough privileges to {}"
).format(current_user.username, current_user.role.name, action) ).format(current_user.username, current_user.role.name, action)
raise NotEnoughPrivileges(message=msg) raise NotEnoughPrivileges(message=msg)
return decorated_function return decorated_function
return decorator return decorator
@ -265,15 +278,16 @@ def api_can_create_domain(f):
- user is in Operator role or higher, or - user is in Operator role or higher, or
- allow_user_create_domain is on - allow_user_create_domain is on
""" """
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if current_user.role.name not in [ if current_user.role.name not in [
'Administrator', 'Operator' 'Administrator', 'Operator'
] and not Setting().get('allow_user_create_domain'): ] and not Setting().get('allow_user_create_domain'):
msg = "User {0} does not have enough privileges to create zone" msg = "User {0} does not have enough privileges to create zone"
current_app.logger.error(msg.format(current_user.username)) current_app.logger.error(msg.format(current_user.username))
raise NotEnoughPrivileges() raise NotEnoughPrivileges()
if Setting().get('deny_domain_override'): if Setting().get('deny_domain_override'):
req = request.get_json(force=True) req = request.get_json(force=True)
domain = Domain() domain = Domain()
@ -294,10 +308,11 @@ def apikey_can_create_domain(f):
- deny_domain_override is off or - deny_domain_override is off or
- override_domain is true (from request) - override_domain is true (from request)
""" """
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if g.apikey.role.name not in [ if g.apikey.role.name not in [
'Administrator', 'Operator' 'Administrator', 'Operator'
] and not Setting().get('allow_user_create_domain'): ] and not Setting().get('allow_user_create_domain'):
msg = "ApiKey #{0} does not have enough privileges to create zone" msg = "ApiKey #{0} does not have enough privileges to create zone"
current_app.logger.error(msg.format(g.apikey.id)) current_app.logger.error(msg.format(g.apikey.id))
@ -320,20 +335,23 @@ def apikey_can_remove_domain(http_methods=[]):
- user is in Operator role or higher, or - user is in Operator role or higher, or
- allow_user_remove_domain is on - allow_user_remove_domain is on
""" """
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
check_current_http_method = not http_methods or request.method in http_methods check_current_http_method = not http_methods or request.method in http_methods
if (check_current_http_method and if (check_current_http_method and
g.apikey.role.name not in ['Administrator', 'Operator'] and g.apikey.role.name not in ['Administrator', 'Operator'] and
not Setting().get('allow_user_remove_domain') not Setting().get('allow_user_remove_domain')
): ):
msg = "ApiKey #{0} does not have enough privileges to remove zone" msg = "ApiKey #{0} does not have enough privileges to remove zone"
current_app.logger.error(msg.format(g.apikey.id)) current_app.logger.error(msg.format(g.apikey.id))
raise NotEnoughPrivileges() raise NotEnoughPrivileges()
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function return decorated_function
return decorator return decorator
@ -341,6 +359,7 @@ def apikey_is_admin(f):
""" """
Grant access if user is in Administrator role Grant access if user is in Administrator role
""" """
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if g.apikey.role.name != 'Administrator': if g.apikey.role.name != 'Administrator':
@ -358,6 +377,7 @@ def apikey_can_access_domain(f):
- user has Operator role or higher, or - user has Operator role or higher, or
- user has explicitly been granted access to domain - user has explicitly been granted access to domain
""" """
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if g.apikey.role.name not in ['Administrator', 'Operator']: if g.apikey.role.name not in ['Administrator', 'Operator']:
@ -382,22 +402,26 @@ def apikey_can_configure_dnssec(http_methods=[]):
- user is in Operator role or higher, or - user is in Operator role or higher, or
- dnssec_admins_only is off - dnssec_admins_only is off
""" """
def decorator(f=None): def decorator(f=None):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
check_current_http_method = not http_methods or request.method in http_methods check_current_http_method = not http_methods or request.method in http_methods
if (check_current_http_method and if (check_current_http_method and
g.apikey.role.name not in ['Administrator', 'Operator'] and g.apikey.role.name not in ['Administrator', 'Operator'] and
Setting().get('dnssec_admins_only') Setting().get('dnssec_admins_only')
): ):
msg = "ApiKey #{0} does not have enough privileges to configure dnssec" msg = "ApiKey #{0} does not have enough privileges to configure dnssec"
current_app.logger.error(msg.format(g.apikey.id)) current_app.logger.error(msg.format(g.apikey.id))
raise DomainAccessForbidden(message=msg) raise DomainAccessForbidden(message=msg)
return f(*args, **kwargs) if f else None return f(*args, **kwargs) if f else None
return decorated_function return decorated_function
return decorator return decorator
def allowed_record_types(f): def allowed_record_types(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
@ -423,6 +447,7 @@ def allowed_record_types(f):
return decorated_function return decorated_function
def allowed_record_ttl(f): def allowed_record_ttl(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
@ -431,12 +456,12 @@ def allowed_record_ttl(f):
if request.method == 'GET': if request.method == 'GET':
return f(*args, **kwargs) return f(*args, **kwargs)
if g.apikey.role.name in ['Administrator', 'Operator']: if g.apikey.role.name in ['Administrator', 'Operator']:
return f(*args, **kwargs) return f(*args, **kwargs)
allowed_ttls = Setting().get_ttl_options() allowed_ttls = Setting().get_ttl_options()
allowed_numeric_ttls = [ ttl[0] for ttl in allowed_ttls ] allowed_numeric_ttls = [ttl[0] for ttl in allowed_ttls]
content = request.get_json() content = request.get_json()
try: try:
for record in content['rrsets']: for record in content['rrsets']:
@ -497,6 +522,7 @@ def dyndns_login_required(f):
return decorated_function return decorated_function
def apikey_or_basic_auth(f): def apikey_or_basic_auth(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
@ -505,4 +531,5 @@ def apikey_or_basic_auth(f):
return apikey_auth(f)(*args, **kwargs) return apikey_auth(f)(*args, **kwargs)
else: else:
return api_basic_auth(f)(*args, **kwargs) return api_basic_auth(f)(*args, **kwargs)
return decorated_function return decorated_function

View File

@ -1,44 +1,32 @@
import os import os
import urllib.parse
basedir = os.path.abspath(os.path.dirname(__file__)) basedir = os.path.abspath(os.path.dirname(__file__))
### BASIC APP CONFIG
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
BIND_ADDRESS = '0.0.0.0' BIND_ADDRESS = '0.0.0.0'
PORT = 9191
HSTS_ENABLED = False
SERVER_EXTERNAL_SSL = os.getenv('SERVER_EXTERNAL_SSL', True)
SESSION_TYPE = 'sqlalchemy'
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_HTTPONLY = True
#CAPTCHA Config
CAPTCHA_ENABLE = True CAPTCHA_ENABLE = True
CAPTCHA_LENGTH = 6
CAPTCHA_WIDTH = 160
CAPTCHA_HEIGHT = 60 CAPTCHA_HEIGHT = 60
CAPTCHA_LENGTH = 6
CAPTCHA_SESSION_KEY = 'captcha_image' CAPTCHA_SESSION_KEY = 'captcha_image'
CAPTCHA_WIDTH = 160
### DATABASE CONFIG CSRF_COOKIE_HTTPONLY = True
SQLA_DB_USER = 'pda' HSTS_ENABLED = False
SQLA_DB_PASSWORD = 'changeme' PORT = 9191
SQLA_DB_HOST = '127.0.0.1' SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
SQLA_DB_NAME = 'pda' SAML_ASSERTION_ENCRYPTED = True
SAML_ENABLED = False
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
SERVER_EXTERNAL_SSL = os.getenv('SERVER_EXTERNAL_SSL', True)
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_TYPE = 'sqlalchemy'
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
SQLALCHEMY_TRACK_MODIFICATIONS = True SQLALCHEMY_TRACK_MODIFICATIONS = True
# SQLA_DB_USER = 'pda'
### DATABASE - MySQL # SQLA_DB_PASSWORD = 'changeme'
# SQLA_DB_HOST = '127.0.0.1'
# SQLA_DB_NAME = 'pda'
# SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format( # SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format(
# urllib.parse.quote_plus(SQLA_DB_USER), # urllib.parse.quote_plus(SQLA_DB_USER),
# urllib.parse.quote_plus(SQLA_DB_PASSWORD), # urllib.parse.quote_plus(SQLA_DB_PASSWORD),
# SQLA_DB_HOST, # SQLA_DB_HOST,
# SQLA_DB_NAME # SQLA_DB_NAME
# ) # )
### DATABASE - SQLite
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
# SAML Authnetication
SAML_ENABLED = False
SAML_ASSERTION_ENCRYPTED = True

View File

@ -0,0 +1,629 @@
import os
from pathlib import Path
basedir = os.path.abspath(Path(os.path.dirname(__file__)).parent)
class AppSettings(object):
defaults = {
# Flask Settings
'bind_address': '0.0.0.0',
'csrf_cookie_secure': False,
'log_level': 'WARNING',
'port': 9191,
'salt': '$2b$12$yLUMTIfl21FKJQpTkRQXCu',
'secret_key': 'e951e5a1f4b94151b360f47edf596dd2',
'session_cookie_secure': False,
'session_type': 'sqlalchemy',
'sqlalchemy_track_modifications': True,
'sqlalchemy_database_uri': 'sqlite:///' + os.path.join(basedir, 'pdns.db'),
'sqlalchemy_engine_options': {},
# General Settings
'captcha_enable': True,
'captcha_height': 60,
'captcha_length': 6,
'captcha_session_key': 'captcha_image',
'captcha_width': 160,
'mail_server': 'localhost',
'mail_port': 25,
'mail_debug': False,
'mail_use_ssl': False,
'mail_use_tls': False,
'mail_username': '',
'mail_password': '',
'mail_default_sender': '',
'remote_user_enabled': False,
'remote_user_cookies': [],
'remote_user_logout_url': '',
'server_external_ssl': True,
'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,
'allow_user_remove_domain': False,
'allow_user_view_history': False,
'custom_history_header': '',
'delete_sso_accounts': False,
'bg_domain_updates': False,
'enable_api_rr_history': True,
'preserve_history': False,
'site_name': 'PowerDNS-Admin',
'site_url': 'http://localhost:9191',
'session_timeout': 10,
'warn_session_timeout': True,
'pdns_api_url': '',
'pdns_api_key': '',
'pdns_api_timeout': 30,
'pdns_version': '4.1.1',
'verify_ssl_connections': True,
'verify_user_email': False,
'enforce_api_ttl': False,
'ttl_options': '1 minute,5 minutes,30 minutes,60 minutes,24 hours',
'otp_field_enabled': True,
'custom_css': '',
'otp_force': False,
'max_history_records': 1000,
'deny_domain_override': False,
'account_name_extra_chars': False,
'gravatar_enabled': False,
# Local Authentication Settings
'local_db_enabled': True,
'signup_enabled': True,
'pwd_enforce_characters': False,
'pwd_min_len': 10,
'pwd_min_lowercase': 3,
'pwd_min_uppercase': 2,
'pwd_min_digits': 2,
'pwd_min_special': 1,
'pwd_enforce_complexity': False,
'pwd_min_complexity': 11,
# LDAP Authentication Settings
'ldap_enabled': False,
'ldap_type': 'ldap',
'ldap_uri': '',
'ldap_base_dn': '',
'ldap_admin_username': '',
'ldap_admin_password': '',
'ldap_domain': '',
'ldap_filter_basic': '',
'ldap_filter_username': '',
'ldap_filter_group': '',
'ldap_filter_groupname': '',
'ldap_sg_enabled': False,
'ldap_admin_group': '',
'ldap_operator_group': '',
'ldap_user_group': '',
'autoprovisioning': False,
'autoprovisioning_attribute': '',
'urn_value': '',
'purge': False,
# Google OAuth Settings
'google_oauth_enabled': False,
'google_oauth_client_id': '',
'google_oauth_client_secret': '',
'google_oauth_scope': 'openid email profile',
'google_base_url': 'https://www.googleapis.com/oauth2/v3/',
'google_oauth_auto_configure': True,
'google_oauth_metadata_url': 'https://accounts.google.com/.well-known/openid-configuration',
'google_token_url': 'https://oauth2.googleapis.com/token',
'google_authorize_url': 'https://accounts.google.com/o/oauth2/v2/auth',
# GitHub OAuth Settings
'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_auto_configure': False,
'github_oauth_metadata_url': '',
'github_oauth_token_url': 'https://github.com/login/oauth/access_token',
'github_oauth_authorize_url': 'https://github.com/login/oauth/authorize',
# Azure OAuth Settings
'azure_oauth_enabled': False,
'azure_oauth_key': '',
'azure_oauth_secret': '',
'azure_oauth_scope': 'User.Read openid email profile',
'azure_oauth_api_url': 'https://graph.microsoft.com/v1.0/',
'azure_oauth_auto_configure': True,
'azure_oauth_metadata_url': '',
'azure_oauth_token_url': '',
'azure_oauth_authorize_url': '',
'azure_sg_enabled': False,
'azure_admin_group': '',
'azure_operator_group': '',
'azure_user_group': '',
'azure_group_accounts_enabled': False,
'azure_group_accounts_name': 'displayName',
'azure_group_accounts_name_re': '',
'azure_group_accounts_description': 'description',
'azure_group_accounts_description_re': '',
# OIDC OAuth Settings
'oidc_oauth_enabled': False,
'oidc_oauth_key': '',
'oidc_oauth_secret': '',
'oidc_oauth_scope': 'email',
'oidc_oauth_api_url': '',
'oidc_oauth_auto_configure': True,
'oidc_oauth_metadata_url': '',
'oidc_oauth_token_url': '',
'oidc_oauth_authorize_url': '',
'oidc_oauth_logout_url': '',
'oidc_oauth_username': 'preferred_username',
'oidc_oauth_email': 'email',
'oidc_oauth_firstname': 'given_name',
'oidc_oauth_last_name': 'family_name',
'oidc_oauth_account_name_property': '',
'oidc_oauth_account_description_property': '',
# SAML Authentication Settings
'saml_enabled': False,
'saml_debug': False,
'saml_path': os.path.join(basedir, 'saml'),
'saml_metadata_url': None,
'saml_metadata_cache_lifetime': 1,
'saml_idp_sso_binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
'saml_idp_entity_id': None,
'saml_nameid_format': None,
'saml_attribute_account': None,
'saml_attribute_email': 'email',
'saml_attribute_givenname': 'givenname',
'saml_attribute_surname': 'surname',
'saml_attribute_name': None,
'saml_attribute_username': None,
'saml_attribute_admin': None,
'saml_attribute_group': None,
'saml_group_admin_name': None,
'saml_group_operator_name': None,
'saml_group_to_account_mapping': None,
'saml_sp_entity_id': None,
'saml_sp_contact_name': None,
'saml_sp_contact_mail': None,
'saml_sign_request': False,
'saml_want_message_signed': True,
'saml_logout': True,
'saml_logout_url': None,
'saml_assertion_encrypted': True,
'saml_cert': None,
'saml_key': None,
# Zone Record Settings
'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
},
}
types = {
# Flask Settings
'bind_address': str,
'csrf_cookie_secure': bool,
'log_level': str,
'port': int,
'salt': str,
'secret_key': str,
'session_cookie_secure': bool,
'session_type': str,
'sqlalchemy_track_modifications': bool,
'sqlalchemy_database_uri': str,
'sqlalchemy_engine_options': dict,
# General Settings
'captcha_enable': bool,
'captcha_height': int,
'captcha_length': int,
'captcha_session_key': str,
'captcha_width': int,
'mail_server': str,
'mail_port': int,
'mail_debug': bool,
'mail_use_ssl': bool,
'mail_use_tls': bool,
'mail_username': str,
'mail_password': str,
'mail_default_sender': str,
'remote_user_enabled': bool,
'remote_user_cookies': list,
'remote_user_logout_url': str,
'maintenance': bool,
'fullscreen_layout': bool,
'record_helper': bool,
'login_ldap_first': bool,
'default_record_table_size': int,
'default_domain_table_size': int,
'auto_ptr': bool,
'record_quick_edit': bool,
'pretty_ipv6_ptr': bool,
'dnssec_admins_only': bool,
'allow_user_create_domain': bool,
'allow_user_remove_domain': bool,
'allow_user_view_history': bool,
'custom_history_header': str,
'delete_sso_accounts': bool,
'bg_domain_updates': bool,
'enable_api_rr_history': bool,
'preserve_history': bool,
'site_name': str,
'site_url': str,
'session_timeout': int,
'warn_session_timeout': bool,
'pdns_api_url': str,
'pdns_api_key': str,
'pdns_api_timeout': int,
'pdns_version': str,
'verify_ssl_connections': bool,
'verify_user_email': bool,
'enforce_api_ttl': bool,
'ttl_options': str,
'otp_field_enabled': bool,
'custom_css': str,
'otp_force': bool,
'max_history_records': int,
'deny_domain_override': bool,
'account_name_extra_chars': bool,
'gravatar_enabled': bool,
'forward_records_allow_edit': dict,
'reverse_records_allow_edit': dict,
# Local Authentication Settings
'local_db_enabled': bool,
'signup_enabled': bool,
'pwd_enforce_characters': bool,
'pwd_min_len': int,
'pwd_min_lowercase': int,
'pwd_min_uppercase': int,
'pwd_min_digits': int,
'pwd_min_special': int,
'pwd_enforce_complexity': bool,
'pwd_min_complexity': int,
# LDAP Authentication Settings
'ldap_enabled': bool,
'ldap_type': str,
'ldap_uri': str,
'ldap_base_dn': str,
'ldap_admin_username': str,
'ldap_admin_password': str,
'ldap_domain': str,
'ldap_filter_basic': str,
'ldap_filter_username': str,
'ldap_filter_group': str,
'ldap_filter_groupname': str,
'ldap_sg_enabled': bool,
'ldap_admin_group': str,
'ldap_operator_group': str,
'ldap_user_group': str,
'autoprovisioning': bool,
'autoprovisioning_attribute': str,
'urn_value': str,
'purge': bool,
# Google OAuth Settings
'google_oauth_enabled': bool,
'google_oauth_client_id': str,
'google_oauth_client_secret': str,
'google_oauth_scope': str,
'google_base_url': str,
'google_oauth_auto_configure': bool,
'google_oauth_metadata_url': str,
'google_token_url': str,
'google_authorize_url': str,
# GitHub OAuth Settings
'github_oauth_enabled': bool,
'github_oauth_key': str,
'github_oauth_secret': str,
'github_oauth_scope': str,
'github_oauth_api_url': str,
'github_oauth_auto_configure': bool,
'github_oauth_metadata_url': str,
'github_oauth_token_url': str,
'github_oauth_authorize_url': str,
# Azure OAuth Settings
'azure_oauth_enabled': bool,
'azure_oauth_key': str,
'azure_oauth_secret': str,
'azure_oauth_scope': str,
'azure_oauth_api_url': str,
'azure_oauth_auto_configure': bool,
'azure_oauth_metadata_url': str,
'azure_oauth_token_url': str,
'azure_oauth_authorize_url': str,
'azure_sg_enabled': bool,
'azure_admin_group': str,
'azure_operator_group': str,
'azure_user_group': str,
'azure_group_accounts_enabled': bool,
'azure_group_accounts_name': str,
'azure_group_accounts_name_re': str,
'azure_group_accounts_description': str,
'azure_group_accounts_description_re': str,
# OIDC OAuth Settings
'oidc_oauth_enabled': bool,
'oidc_oauth_key': str,
'oidc_oauth_secret': str,
'oidc_oauth_scope': str,
'oidc_oauth_api_url': str,
'oidc_oauth_auto_configure': bool,
'oidc_oauth_metadata_url': str,
'oidc_oauth_token_url': str,
'oidc_oauth_authorize_url': str,
'oidc_oauth_logout_url': str,
'oidc_oauth_username': str,
'oidc_oauth_email': str,
'oidc_oauth_firstname': str,
'oidc_oauth_last_name': str,
'oidc_oauth_account_name_property': str,
'oidc_oauth_account_description_property': str,
# SAML Authentication Settings
'saml_enabled': bool,
'saml_debug': bool,
'saml_path': str,
'saml_metadata_url': str,
'saml_metadata_cache_lifetime': int,
'saml_idp_sso_binding': str,
'saml_idp_entity_id': str,
'saml_nameid_format': str,
'saml_attribute_account': str,
'saml_attribute_email': str,
'saml_attribute_givenname': str,
'saml_attribute_surname': str,
'saml_attribute_name': str,
'saml_attribute_username': str,
'saml_attribute_admin': str,
'saml_attribute_group': str,
'saml_group_admin_name': str,
'saml_group_operator_name': str,
'saml_group_to_account_mapping': str,
'saml_sp_entity_id': str,
'saml_sp_contact_name': str,
'saml_sp_contact_mail': str,
'saml_sign_request': bool,
'saml_want_message_signed': bool,
'saml_logout': bool,
'saml_logout_url': str,
'saml_assertion_encrypted': bool,
'saml_cert': str,
'saml_key': str,
}
groups = {
'authentication': [
# Local Authentication Settings
'local_db_enabled',
'signup_enabled',
'pwd_enforce_characters',
'pwd_min_len',
'pwd_min_lowercase',
'pwd_min_uppercase',
'pwd_min_digits',
'pwd_min_special',
'pwd_enforce_complexity',
'pwd_min_complexity',
# LDAP Authentication Settings
'ldap_enabled',
'ldap_type',
'ldap_uri',
'ldap_base_dn',
'ldap_admin_username',
'ldap_admin_password',
'ldap_domain',
'ldap_filter_basic',
'ldap_filter_username',
'ldap_filter_group',
'ldap_filter_groupname',
'ldap_sg_enabled',
'ldap_admin_group',
'ldap_operator_group',
'ldap_user_group',
'autoprovisioning',
'autoprovisioning_attribute',
'urn_value',
'purge',
# Google OAuth Settings
'google_oauth_enabled',
'google_oauth_client_id',
'google_oauth_client_secret',
'google_oauth_scope',
'google_base_url',
'google_oauth_auto_configure',
'google_oauth_metadata_url',
'google_token_url',
'google_authorize_url',
# GitHub OAuth Settings
'github_oauth_enabled',
'github_oauth_key',
'github_oauth_secret',
'github_oauth_scope',
'github_oauth_api_url',
'github_oauth_auto_configure',
'github_oauth_metadata_url',
'github_oauth_token_url',
'github_oauth_authorize_url',
# Azure OAuth Settings
'azure_oauth_enabled',
'azure_oauth_key',
'azure_oauth_secret',
'azure_oauth_scope',
'azure_oauth_api_url',
'azure_oauth_auto_configure',
'azure_oauth_metadata_url',
'azure_oauth_token_url',
'azure_oauth_authorize_url',
'azure_sg_enabled',
'azure_admin_group',
'azure_operator_group',
'azure_user_group',
'azure_group_accounts_enabled',
'azure_group_accounts_name',
'azure_group_accounts_name_re',
'azure_group_accounts_description',
'azure_group_accounts_description_re',
# OIDC OAuth Settings
'oidc_oauth_enabled',
'oidc_oauth_key',
'oidc_oauth_secret',
'oidc_oauth_scope',
'oidc_oauth_api_url',
'oidc_oauth_auto_configure',
'oidc_oauth_metadata_url',
'oidc_oauth_token_url',
'oidc_oauth_authorize_url',
'oidc_oauth_logout_url',
'oidc_oauth_username',
'oidc_oauth_email',
'oidc_oauth_firstname',
'oidc_oauth_last_name',
'oidc_oauth_account_name_property',
'oidc_oauth_account_description_property',
]
}
@staticmethod
def convert_type(name, value):
import json
from json import JSONDecodeError
if name in AppSettings.types:
var_type = AppSettings.types[name]
# Handle boolean values
if var_type == bool and isinstance(value, str):
if value.lower() in ['True', 'true', '1'] or value is True:
return True
else:
return False
# Handle float values
if var_type == float:
return float(value)
# Handle integer values
if var_type == int:
return int(value)
if (var_type == dict or var_type == list) and isinstance(value, str) and len(value) > 0:
try:
return json.loads(value)
except JSONDecodeError as e:
raise ValueError('Cannot parse json {} for variable {}'.format(value, name))
if var_type == str:
return str(value)
return value
@staticmethod
def load_environment(app):
""" Load app settings from environment variables when defined. """
import os
for var_name, default_value in AppSettings.defaults.items():
env_name = var_name.upper()
current_value = None
if env_name + '_FILE' in os.environ:
if env_name in os.environ:
raise AttributeError(
"Both {} and {} are set but are exclusive.".format(
env_name, env_name + '_FILE'))
with open(os.environ[env_name + '_FILE']) as f:
current_value = f.read()
f.close()
elif env_name in os.environ:
current_value = os.environ[env_name]
if current_value is not None:
app.config[env_name] = AppSettings.convert_type(var_name, current_value)

View File

@ -1,450 +1,19 @@
import sys import sys
import traceback import traceback
import pytimeparse import pytimeparse
from ast import literal_eval from ast import literal_eval
from distutils.util import strtobool
from flask import current_app from flask import current_app
from .base import db from .base import db
from powerdnsadmin.lib.settings import AppSettings
class Setting(db.Model): class Setting(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, index=True) name = db.Column(db.String(64), unique=True, index=True)
value = db.Column(db.Text()) value = db.Column(db.Text())
types = {
'maintenance': bool,
'fullscreen_layout': bool,
'record_helper': bool,
'login_ldap_first': bool,
'default_record_table_size': int,
'default_domain_table_size': int,
'auto_ptr': bool,
'record_quick_edit': bool,
'pretty_ipv6_ptr': bool,
'dnssec_admins_only': bool,
'allow_user_create_domain': bool,
'allow_user_remove_domain': bool,
'allow_user_view_history': bool,
'custom_history_header': str,
'delete_sso_accounts': bool,
'bg_domain_updates': bool,
'enable_api_rr_history': bool,
'preserve_history': bool,
'site_name': str,
'site_url': str,
'session_timeout': int,
'warn_session_timeout': bool,
'pdns_api_url': str,
'pdns_api_key': str,
'pdns_api_timeout': int,
'pdns_version': str,
'verify_ssl_connections': bool,
'verify_user_email': bool,
'enforce_api_ttl': bool,
'ttl_options': str,
'otp_field_enabled': bool,
'custom_css': str,
'otp_force': bool,
'max_history_records': int,
'deny_domain_override': bool,
'account_name_extra_chars': bool,
'gravatar_enabled': bool,
'forward_records_allow_edit': dict,
'reverse_records_allow_edit': dict,
'local_db_enabled': bool,
'signup_enabled': bool,
'pwd_enforce_characters': bool,
'pwd_min_len': int,
'pwd_min_lowercase': int,
'pwd_min_uppercase': int,
'pwd_min_digits': int,
'pwd_min_special': int,
'pwd_enforce_complexity': bool,
'pwd_min_complexity': int,
'ldap_enabled': bool,
'ldap_type': str,
'ldap_uri': str,
'ldap_base_dn': str,
'ldap_admin_username': str,
'ldap_admin_password': str,
'ldap_domain': str,
'ldap_filter_basic': str,
'ldap_filter_username': str,
'ldap_filter_group': str,
'ldap_filter_groupname': str,
'ldap_sg_enabled': bool,
'ldap_admin_group': str,
'ldap_operator_group': str,
'ldap_user_group': str,
'autoprovisioning': bool,
'autoprovisioning_attribute': str,
'urn_value': str,
'purge': bool,
'google_oauth_enabled': bool,
'google_oauth_client_id': str,
'google_oauth_client_secret': str,
'google_oauth_scope': str,
'google_base_url': str,
'google_oauth_auto_configure': bool,
'google_oauth_metadata_url': str,
'google_token_url': str,
'google_authorize_url': str,
'github_oauth_enabled': bool,
'github_oauth_key': str,
'github_oauth_secret': str,
'github_oauth_scope': str,
'github_oauth_api_url': str,
'github_oauth_auto_configure': bool,
'github_oauth_metadata_url': str,
'github_oauth_token_url': str,
'github_oauth_authorize_url': str,
'azure_oauth_enabled': bool,
'azure_oauth_key': str,
'azure_oauth_secret': str,
'azure_oauth_scope': str,
'azure_oauth_api_url': str,
'azure_oauth_auto_configure': bool,
'azure_oauth_metadata_url': str,
'azure_oauth_token_url': str,
'azure_oauth_authorize_url': str,
'azure_sg_enabled': bool,
'azure_admin_group': str,
'azure_operator_group': str,
'azure_user_group': str,
'azure_group_accounts_enabled': bool,
'azure_group_accounts_name': str,
'azure_group_accounts_name_re': str,
'azure_group_accounts_description': str,
'azure_group_accounts_description_re': str,
'oidc_oauth_enabled': bool,
'oidc_oauth_key': str,
'oidc_oauth_secret': str,
'oidc_oauth_scope': str,
'oidc_oauth_api_url': str,
'oidc_oauth_auto_configure': bool,
'oidc_oauth_metadata_url': str,
'oidc_oauth_token_url': str,
'oidc_oauth_authorize_url': str,
'oidc_oauth_logout_url': str,
'oidc_oauth_username': str,
'oidc_oauth_email': str,
'oidc_oauth_firstname': str,
'oidc_oauth_last_name': str,
'oidc_oauth_account_name_property': str,
'oidc_oauth_account_description_property': str,
}
defaults = {
# General Settings
'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,
'allow_user_remove_domain': False,
'allow_user_view_history': False,
'custom_history_header': '',
'delete_sso_accounts': False,
'bg_domain_updates': False,
'enable_api_rr_history': True,
'preserve_history': False,
'site_name': 'PowerDNS-Admin',
'site_url': 'http://localhost:9191',
'session_timeout': 10,
'warn_session_timeout': True,
'pdns_api_url': '',
'pdns_api_key': '',
'pdns_api_timeout': 30,
'pdns_version': '4.1.1',
'verify_ssl_connections': True,
'verify_user_email': False,
'enforce_api_ttl': False,
'ttl_options': '1 minute,5 minutes,30 minutes,60 minutes,24 hours',
'otp_field_enabled': True,
'custom_css': '',
'otp_force': False,
'max_history_records': 1000,
'deny_domain_override': False,
'account_name_extra_chars': False,
'gravatar_enabled': False,
# Local Authentication Settings ZONE_TYPE_FORWARD = 'forward'
'local_db_enabled': True, ZONE_TYPE_REVERSE = 'reverse'
'signup_enabled': True,
'pwd_enforce_characters': False,
'pwd_min_len': 10,
'pwd_min_lowercase': 3,
'pwd_min_uppercase': 2,
'pwd_min_digits': 2,
'pwd_min_special': 1,
'pwd_enforce_complexity': False,
'pwd_min_complexity': 11,
# LDAP Authentication Settings
'ldap_enabled': False,
'ldap_type': 'ldap',
'ldap_uri': '',
'ldap_base_dn': '',
'ldap_admin_username': '',
'ldap_admin_password': '',
'ldap_domain': '',
'ldap_filter_basic': '',
'ldap_filter_username': '',
'ldap_filter_group': '',
'ldap_filter_groupname': '',
'ldap_sg_enabled': False,
'ldap_admin_group': '',
'ldap_operator_group': '',
'ldap_user_group': '',
'autoprovisioning': False,
'autoprovisioning_attribute': '',
'urn_value': '',
'purge': False,
# Google OAuth2 Settings
'google_oauth_enabled': False,
'google_oauth_client_id': '',
'google_oauth_client_secret': '',
'google_oauth_scope': 'openid email profile',
'google_base_url': 'https://www.googleapis.com/oauth2/v3/',
'google_oauth_auto_configure': True,
'google_oauth_metadata_url': 'https://accounts.google.com/.well-known/openid-configuration',
'google_token_url': 'https://oauth2.googleapis.com/token',
'google_authorize_url': 'https://accounts.google.com/o/oauth2/v2/auth',
# GitHub OAuth2 Settings
'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_auto_configure': False,
'github_oauth_metadata_url': '',
'github_oauth_token_url': 'https://github.com/login/oauth/access_token',
'github_oauth_authorize_url': 'https://github.com/login/oauth/authorize',
# Azure OAuth2 Settings
'azure_oauth_enabled': False,
'azure_oauth_key': '',
'azure_oauth_secret': '',
'azure_oauth_scope': 'User.Read openid email profile',
'azure_oauth_api_url': 'https://graph.microsoft.com/v1.0/',
'azure_oauth_auto_configure': True,
'azure_oauth_metadata_url': '',
'azure_oauth_token_url': '',
'azure_oauth_authorize_url': '',
'azure_sg_enabled': False,
'azure_admin_group': '',
'azure_operator_group': '',
'azure_user_group': '',
'azure_group_accounts_enabled': False,
'azure_group_accounts_name': 'displayName',
'azure_group_accounts_name_re': '',
'azure_group_accounts_description': 'description',
'azure_group_accounts_description_re': '',
# OIDC OAuth2 Settings
'oidc_oauth_enabled': False,
'oidc_oauth_key': '',
'oidc_oauth_secret': '',
'oidc_oauth_scope': 'email',
'oidc_oauth_api_url': '',
'oidc_oauth_auto_configure': True,
'oidc_oauth_metadata_url': '',
'oidc_oauth_token_url': '',
'oidc_oauth_authorize_url': '',
'oidc_oauth_logout_url': '',
'oidc_oauth_username': 'preferred_username',
'oidc_oauth_email': 'email',
'oidc_oauth_firstname': 'given_name',
'oidc_oauth_last_name': 'family_name',
'oidc_oauth_account_name_property': '',
'oidc_oauth_account_description_property': '',
# Zone Record Settings
'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
},
}
groups = {
'authentication': [
# Local Authentication Settings
'local_db_enabled',
'signup_enabled',
'pwd_enforce_characters',
'pwd_min_len',
'pwd_min_lowercase',
'pwd_min_uppercase',
'pwd_min_digits',
'pwd_min_special',
'pwd_enforce_complexity',
'pwd_min_complexity',
# LDAP Authentication Settings
'ldap_enabled',
'ldap_type',
'ldap_uri',
'ldap_base_dn',
'ldap_admin_username',
'ldap_admin_password',
'ldap_domain',
'ldap_filter_basic',
'ldap_filter_username',
'ldap_filter_group',
'ldap_filter_groupname',
'ldap_sg_enabled',
'ldap_admin_group',
'ldap_operator_group',
'ldap_user_group',
'autoprovisioning',
'autoprovisioning_attribute',
'urn_value',
'purge',
# Google OAuth2 Settings
'google_oauth_enabled',
'google_oauth_client_id',
'google_oauth_client_secret',
'google_oauth_scope',
'google_base_url',
'google_oauth_auto_configure',
'google_oauth_metadata_url',
'google_token_url',
'google_authorize_url',
# GitHub OAuth2 Settings
'github_oauth_enabled',
'github_oauth_key',
'github_oauth_secret',
'github_oauth_scope',
'github_oauth_api_url',
'github_oauth_auto_configure',
'github_oauth_metadata_url',
'github_oauth_token_url',
'github_oauth_authorize_url',
# Azure OAuth2 Settings
'azure_oauth_enabled',
'azure_oauth_key',
'azure_oauth_secret',
'azure_oauth_scope',
'azure_oauth_api_url',
'azure_oauth_auto_configure',
'azure_oauth_metadata_url',
'azure_oauth_token_url',
'azure_oauth_authorize_url',
'azure_sg_enabled',
'azure_admin_group',
'azure_operator_group',
'azure_user_group',
'azure_group_accounts_enabled',
'azure_group_accounts_name',
'azure_group_accounts_name_re',
'azure_group_accounts_description',
'azure_group_accounts_description_re',
# OIDC OAuth2 Settings
'oidc_oauth_enabled',
'oidc_oauth_key',
'oidc_oauth_secret',
'oidc_oauth_scope',
'oidc_oauth_api_url',
'oidc_oauth_auto_configure',
'oidc_oauth_metadata_url',
'oidc_oauth_token_url',
'oidc_oauth_authorize_url',
'oidc_oauth_logout_url',
'oidc_oauth_username',
'oidc_oauth_email',
'oidc_oauth_firstname',
'oidc_oauth_last_name',
'oidc_oauth_account_name_property',
'oidc_oauth_account_description_property',
]
}
def __init__(self, id=None, name=None, value=None): def __init__(self, id=None, name=None, value=None):
self.id = id self.id = id
@ -457,44 +26,12 @@ class Setting(db.Model):
self.name = name self.name = name
self.value = value self.value = value
def convert_type(self, name, value):
import json
from json import JSONDecodeError
if name in self.types:
var_type = self.types[name]
# Handle boolean values
if var_type == bool:
if value == 'True' or value == 'true' or value == '1' or value is True:
return True
else:
return False
# Handle float values
if var_type == float:
return float(value)
# Handle integer values
if var_type == int:
return int(value)
if (var_type == dict or var_type == list) and isinstance(value, str) and len(value) > 0:
try:
return json.loads(value)
except JSONDecodeError as e:
pass
if var_type == str:
return str(value)
return value
def set_maintenance(self, mode): def set_maintenance(self, mode):
maintenance = Setting.query.filter( maintenance = Setting.query.filter(
Setting.name == 'maintenance').first() Setting.name == 'maintenance').first()
if maintenance is None: if maintenance is None:
value = self.defaults['maintenance'] value = AppSettings.defaults['maintenance']
maintenance = Setting(name='maintenance', value=str(value)) maintenance = Setting(name='maintenance', value=str(value))
db.session.add(maintenance) db.session.add(maintenance)
@ -516,7 +53,7 @@ class Setting(db.Model):
current_setting = Setting.query.filter(Setting.name == setting).first() current_setting = Setting.query.filter(Setting.name == setting).first()
if current_setting is None: if current_setting is None:
value = self.defaults[setting] value = AppSettings.defaults[setting]
current_setting = Setting(name=setting, value=str(value)) current_setting = Setting(name=setting, value=str(value))
db.session.add(current_setting) db.session.add(current_setting)
@ -541,21 +78,20 @@ class Setting(db.Model):
current_setting = Setting(name=setting, value=None) current_setting = Setting(name=setting, value=None)
db.session.add(current_setting) db.session.add(current_setting)
value = str(self.convert_type(setting, value)) value = str(AppSettings.convert_type(setting, value))
try: try:
current_setting.value = value current_setting.value = value
db.session.commit() db.session.commit()
return True return True
except Exception as e: except Exception as e:
current_app.logger.error('Cannot edit setting {0}. DETAIL: {1}'.format( current_app.logger.error('Cannot edit setting {0}. DETAIL: {1}'.format(setting, e))
setting, e))
current_app.logger.debug(traceback.format_exec()) current_app.logger.debug(traceback.format_exec())
db.session.rollback() db.session.rollback()
return False return False
def get(self, setting): def get(self, setting):
if setting in self.defaults: if setting in AppSettings.defaults:
if setting.upper() in current_app.config: if setting.upper() in current_app.config:
result = current_app.config[setting.upper()] result = current_app.config[setting.upper()]
@ -566,51 +102,45 @@ class Setting(db.Model):
if hasattr(result, 'value'): if hasattr(result, 'value'):
result = result.value result = result.value
return self.convert_type(setting, result) return AppSettings.convert_type(setting, result)
else: else:
return self.defaults[setting] return AppSettings.defaults[setting]
else: else:
current_app.logger.error('Unknown setting queried: {0}'.format(setting)) current_app.logger.error('Unknown setting queried: {0}'.format(setting))
def get_group(self, group): def get_group(self, group):
if not isinstance(group, list): if not isinstance(group, list):
group = self.groups[group] group = AppSettings.groups[group]
result = {} result = {}
records = self.query.all()
for record in records: for var_name, default_value in AppSettings.defaults.items():
if record.name in group: if var_name in group:
result[record.name] = self.convert_type(record.name, record.value) result[var_name] = self.get(var_name)
return result return result
def get_records_allow_to_edit(self): def get_records_allow_to_edit(self):
return list( return list(
set(self.get_forward_records_allow_to_edit() + set(self.get_supported_record_types(self.ZONE_TYPE_FORWARD) +
self.get_reverse_records_allow_to_edit())) self.get_supported_record_types(self.ZONE_TYPE_REVERSE)))
def get_forward_records_allow_to_edit(self): def get_supported_record_types(self, zone_type):
records = self.get('forward_records_allow_edit') setting_value = []
f_records = literal_eval(records) if isinstance(records,
str) else records if zone_type == self.ZONE_TYPE_FORWARD:
r_name = [r for r in f_records if f_records[r]] setting_value = self.get('forward_records_allow_edit')
# Sort alphabetically if python version is smaller than 3.6 elif zone_type == self.ZONE_TYPE_REVERSE:
if sys.version_info[0] < 3 or (sys.version_info[0] == 3 setting_value = self.get('reverse_records_allow_edit')
and sys.version_info[1] < 6):
r_name.sort() records = literal_eval(setting_value) if isinstance(setting_value, str) else setting_value
return r_name types = [r for r in records if records[r]]
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 # Sort alphabetically if python version is smaller than 3.6
if sys.version_info[0] < 3 or (sys.version_info[0] == 3 if sys.version_info[0] < 3 or (sys.version_info[0] == 3 and sys.version_info[1] < 6):
and sys.version_info[1] < 6): types.sort()
r_name.sort()
return r_name return types
def get_ttl_options(self): def get_ttl_options(self):
return [(pytimeparse.parse(ttl), ttl) return [(pytimeparse.parse(ttl), ttl)

View File

@ -1460,6 +1460,7 @@ def setting_pdns():
@login_required @login_required
@operator_role_required @operator_role_required
def setting_records(): def setting_records():
from powerdnsadmin.lib.settings import AppSettings
if request.method == 'GET': if request.method == 'GET':
_fr = Setting().get('forward_records_allow_edit') _fr = Setting().get('forward_records_allow_edit')
_rr = Setting().get('reverse_records_allow_edit') _rr = Setting().get('reverse_records_allow_edit')
@ -1472,7 +1473,7 @@ def setting_records():
elif request.method == 'POST': elif request.method == 'POST':
fr = {} fr = {}
rr = {} rr = {}
records = Setting().defaults['forward_records_allow_edit'] records = AppSettings.defaults['forward_records_allow_edit']
for r in records: for r in records:
fr[r] = True if request.form.get('fr_{0}'.format( fr[r] = True if request.form.get('fr_{0}'.format(
r.lower())) else False r.lower())) else False
@ -1517,6 +1518,7 @@ def setting_authentication():
@login_required @login_required
@admin_role_required @admin_role_required
def setting_authentication_api(): def setting_authentication_api():
from powerdnsadmin.lib.settings import AppSettings
result = {'status': 1, 'messages': [], 'data': {}} result = {'status': 1, 'messages': [], 'data': {}}
if request.form.get('commit') == '1': if request.form.get('commit') == '1':
@ -1524,7 +1526,7 @@ def setting_authentication_api():
data = json.loads(request.form.get('data')) data = json.loads(request.form.get('data'))
for key, value in data.items(): for key, value in data.items():
if key in model.groups['authentication']: if key in AppSettings.groups['authentication']:
model.set(key, value) model.set(key, value)
result['data'] = Setting().get_group('authentication') result['data'] = Setting().get_group('authentication')

View File

@ -72,9 +72,9 @@ def domain(domain_name):
quick_edit = Setting().get('record_quick_edit') quick_edit = Setting().get('record_quick_edit')
records_allow_to_edit = Setting().get_records_allow_to_edit() records_allow_to_edit = Setting().get_records_allow_to_edit()
forward_records_allow_to_edit = Setting( forward_records_allow_to_edit = Setting(
).get_forward_records_allow_to_edit() ).get_supported_record_types(Setting().ZONE_TYPE_FORWARD)
reverse_records_allow_to_edit = Setting( reverse_records_allow_to_edit = Setting(
).get_reverse_records_allow_to_edit() ).get_supported_record_types(Setting().ZONE_TYPE_REVERSE)
ttl_options = Setting().get_ttl_options() ttl_options = Setting().get_ttl_options()
records = [] records = []