Merge branch 'release/0.4.1'

This commit is contained in:
Matt Scott 2023-04-11 19:13:04 -04:00
commit a8895ffe7a
No known key found for this signature in database
GPG Key ID: A9A0AFFC0E079001
55 changed files with 3357 additions and 2406 deletions

View File

@ -4,8 +4,10 @@
name: MegaLinter
on:
workflow_dispatch:
push:
branches-ignore:
- "*"
- "dev"
- "main"
- "master"

View File

@ -27,6 +27,7 @@ CAPTCHA_SESSION_KEY = 'captcha_image'
SESSION_TYPE = 'sqlalchemy'
### DATABASE - MySQL
## Don't forget to uncomment the import in the top
#SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format(
# urllib.parse.quote_plus(SQLA_DB_USER),
# urllib.parse.quote_plus(SQLA_DB_PASSWORD),
@ -34,6 +35,15 @@ SESSION_TYPE = 'sqlalchemy'
# SQLA_DB_NAME
#)
### DATABASE - PostgreSQL
## Don't forget to uncomment the import in the top
#SQLALCHEMY_DATABASE_URI = 'postgres://{}:{}@{}/{}'.format(
# urllib.parse.quote_plus(SQLA_DB_USER),
# urllib.parse.quote_plus(SQLA_DB_PASSWORD),
# SQLA_DB_HOST,
# SQLA_DB_NAME
#)
### DATABASE - SQLite
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')

View File

@ -1,3 +1,8 @@
# import everything from environment variables
import os
import sys
import json
# Defaults for Docker image
BIND_ADDRESS = '0.0.0.0'
PORT = 80
@ -23,6 +28,7 @@ legal_envvars = (
'OIDC_OAUTH_EMAIL',
'BIND_ADDRESS',
'PORT',
'SERVER_EXTERNAL_SSL',
'LOG_LEVEL',
'SALT',
'SQLALCHEMY_TRACK_MODIFICATIONS',
@ -97,21 +103,18 @@ legal_envvars_bool = (
'SESSION_COOKIE_SECURE',
'CSRF_COOKIE_SECURE',
'CAPTCHA_ENABLE',
'SERVER_EXTERNAL_SSL',
)
legal_envvars_dict = (
'SQLALCHEMY_ENGINE_OPTIONS',
)
# import everything from environment variables
import os
import sys
import json
def str2bool(v):
return v.lower() in ("true", "yes", "1")
def dictfromstr(v,ret):
def dictfromstr(v, ret):
try:
return json.loads(ret)
except Exception as e:
@ -119,10 +122,11 @@ def dictfromstr(v,ret):
print(e)
raise ValueError
for v in legal_envvars:
ret = None
# _FILE suffix will allow to read value from file, usefull for Docker's
# _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:

View File

@ -11,6 +11,7 @@ RUN apt-get update -y \
libffi-dev \
libldap2-dev \
libmariadb-dev-compat \
libpq-dev \
libsasl2-dev \
libssl-dev \
libxml2-dev \

View File

@ -2,6 +2,7 @@ FROM alpine:3.17 AS builder
ARG BUILD_DEPENDENCIES="build-base \
libffi-dev \
libpq-dev \
libxml2-dev \
mariadb-connector-c-dev \
openldap-dev \

View File

@ -38,7 +38,7 @@
## Using PowerDNS-Admin
- Setting up a domain
- Setting up a zone
- Adding a record
- <whatever else>

View File

@ -11,7 +11,7 @@ Active Directory Setup - Tested with Windows Server 2012
5) Fill in the required info -
* LDAP URI - ldap://ip.of.your.domain.controller:389
* LDAP Base DN - dc=youdomain,dc=com
* LDAP Base DN - dc=yourdomain,dc=com
* Active Directory domain - yourdomain.com
* Basic filter - (objectCategory=person)
* the brackets here are **very important**

View File

@ -1,64 +1,65 @@
# Supported environment variables
| Variable | Description | Required | Default value |
| ---------| ----------- | -------- | ------------- |
| BIND_ADDRESS |
| CSRF_COOKIE_SECURE |
| SESSION_TYPE | null|filesystem|sqlalchemy | | filesystem |
| LDAP_ENABLED |
| LOCAL_DB_ENABLED |
| LOG_LEVEL |
| MAIL_DEBUG |
| MAIL_DEFAULT_SENDER |
| MAIL_PASSWORD |
| MAIL_PORT |
| MAIL_SERVER |
| MAIL_USERNAME |
| MAIL_USE_SSL |
| MAIL_USE_TLS |
| OFFLINE_MODE |
| OIDC_OAUTH_API_URL | | | |
| OIDC_OAUTH_AUTHORIZE_URL |
| OIDC_OAUTH_TOKEN_URL | | | |
| OIDC_OAUTH_METADATA_URL | | | |
| PORT |
| REMOTE_USER_COOKIES |
| REMOTE_USER_LOGOUT_URL |
| SALT |
| SAML_ASSERTION_ENCRYPTED |
| SAML_ATTRIBUTE_ACCOUNT |
| SAML_ATTRIBUTE_ADMIN |
| SAML_ATTRIBUTE_EMAIL |
| SAML_ATTRIBUTE_GIVENNAME |
| SAML_ATTRIBUTE_GROUP |
| SAML_ATTRIBUTE_NAME |
| SAML_ATTRIBUTE_SURNAME |
| SAML_ATTRIBUTE_USERNAME |
| SAML_CERT |
| SAML_DEBUG |
| SAML_ENABLED |
| SAML_GROUP_ADMIN_NAME |
| SAML_GROUP_TO_ACCOUNT_MAPPING |
| SAML_IDP_SSO_BINDING |
| SAML_IDP_ENTITY_ID |
| SAML_KEY |
| SAML_LOGOUT |
| SAML_LOGOUT_URL |
| SAML_METADATA_CACHE_LIFETIME |
| SAML_METADATA_URL |
| SAML_NAMEID_FORMAT |
| SAML_PATH |
| SAML_SIGN_REQUEST |
| SAML_SP_CONTACT_MAIL |
| SAML_SP_CONTACT_NAME |
| SAML_SP_ENTITY_ID |
| SAML_WANT_MESSAGE_SIGNED |
| SECRET_KEY | Flask secret key [^1] | Y | no default |
| SESSION_COOKIE_SECURE |
| SIGNUP_ENABLED |
| SQLALCHEMY_DATABASE_URI | SQL Alchemy URI to connect to database | N | no default |
| Variable | Description | Required | Default value |
|--------------------------------|--------------------------------------------------------------------------|------------|---------------|
| BIND_ADDRESS |
| CSRF_COOKIE_SECURE |
| SESSION_TYPE | null | filesystem | sqlalchemy | | filesystem |
| LDAP_ENABLED |
| LOCAL_DB_ENABLED |
| LOG_LEVEL |
| MAIL_DEBUG |
| MAIL_DEFAULT_SENDER |
| MAIL_PASSWORD |
| MAIL_PORT |
| MAIL_SERVER |
| MAIL_USERNAME |
| MAIL_USE_SSL |
| MAIL_USE_TLS |
| OFFLINE_MODE |
| OIDC_OAUTH_API_URL | | | |
| OIDC_OAUTH_AUTHORIZE_URL |
| OIDC_OAUTH_TOKEN_URL | | | |
| OIDC_OAUTH_METADATA_URL | | | |
| PORT |
| SERVER_EXTERNAL_SSL | Forceful override of URL schema detection when using the url_for method. | False | None |
| REMOTE_USER_COOKIES |
| REMOTE_USER_LOGOUT_URL |
| SALT |
| SAML_ASSERTION_ENCRYPTED |
| SAML_ATTRIBUTE_ACCOUNT |
| SAML_ATTRIBUTE_ADMIN |
| SAML_ATTRIBUTE_EMAIL |
| SAML_ATTRIBUTE_GIVENNAME |
| SAML_ATTRIBUTE_GROUP |
| SAML_ATTRIBUTE_NAME |
| SAML_ATTRIBUTE_SURNAME |
| SAML_ATTRIBUTE_USERNAME |
| SAML_CERT |
| SAML_DEBUG |
| SAML_ENABLED |
| SAML_GROUP_ADMIN_NAME |
| SAML_GROUP_TO_ACCOUNT_MAPPING |
| SAML_IDP_SSO_BINDING |
| SAML_IDP_ENTITY_ID |
| SAML_KEY |
| SAML_LOGOUT |
| SAML_LOGOUT_URL |
| SAML_METADATA_CACHE_LIFETIME |
| SAML_METADATA_URL |
| SAML_NAMEID_FORMAT |
| SAML_PATH |
| SAML_SIGN_REQUEST |
| SAML_SP_CONTACT_MAIL |
| SAML_SP_CONTACT_NAME |
| SAML_SP_ENTITY_ID |
| SAML_WANT_MESSAGE_SIGNED |
| SECRET_KEY | Flask secret key [^1] | Y | no default |
| SESSION_COOKIE_SECURE |
| SIGNUP_ENABLED |
| SQLALCHEMY_DATABASE_URI | SQL Alchemy URI to connect to database | N | no default |
| SQLALCHEMY_TRACK_MODIFICATIONS |
| SQLALCHEMY_ENGINE_OPTIONS | json string. e.g. '{"pool_recycle":600,"echo":1}' [^2] |
| SQLALCHEMY_ENGINE_OPTIONS | json string. e.g. '{"pool_recycle":600,"echo":1}' [^2] |
[^1]: Flask secret key (see https://flask.palletsprojects.com/en/1.1.x/config/#SECRET_KEY for how to generate)
[^2]: See Flask-SQLAlchemy Documentation for all engine options.

View File

@ -0,0 +1,17 @@
### PowerDNSAdmin basic settings
PowerDNSAdmin has many features and settings available to be turned either off or on.
In this docs those settings will be explain.
To find the settings in the the dashboard go to settings>basic.
allow_user_create_domain: This setting is used to allow users with the `user` role to create a domain, not possible by
default.
allow_user_remove_domain: Same as `allow_user_create_domain` but for removing a domain.
allow_user_view_history: Allow a user with the role `user` to view and access the history.
custom_history_header: This is a string type variable, when inputting an header name, if exists in the request it will
be in the created_by column in the history, if empty or not mentioned will default to the api_key description.
site_name: This will be the site name.

View File

@ -15,10 +15,9 @@ The below will create a database called powerdnsadmindb and a user of powerdnsad
```
$ sudo su - postgres
$ createuser powerdnsadmin
$ createdb powerdnsadmindb
$ createdb -E UTF8 -l en_US.UTF-8 -O powerdnsadmin -T template0 powerdnsadmindb 'The database for PowerDNS-Admin'
$ psql
postgres=# alter user powerdnsadmin with encrypted password 'powerdnsadmin';
postgres=# grant all privileges on database powerdnsadmindb to powerdnsadmin;
postgres=# ALTER ROLE powerdnsadmin WITH PASSWORD 'powerdnsadmin_password';
```
Note:
@ -51,18 +50,14 @@ On debian based systems these files are located in:
## Install required packages:
### Red-hat based systems:
TODO: confirm this is correct
```
sudo yum install postgresql-libs
```
### Debian based systems:
```
apt install libpq-dev python-dev
```
### Install python packages:
```
pip3 install psycopg2
apt install python3-psycopg2
```
## Known Issues:

View File

@ -7,19 +7,30 @@ First setup your database accordingly:
### Install required packages for building python libraries from requirements.txt file
For Debian 11 (bullseye) and above:
```bash
sudo apt install -y python3-dev git libsasl2-dev libldap2-dev libssl-dev libxml2-dev libxslt1-dev libxmlsec1-dev libffi-dev pkg-config apt-transport-https virtualenv build-essential curl
sudo apt install -y python3-dev git libsasl2-dev libldap2-dev python3-venv libmariadb-dev pkg-config build-essential curl libpq-dev
```
Older systems might also need the following:
```bash
sudo apt install -y libssl-dev libxml2-dev libxslt1-dev libxmlsec1-dev libffi-dev apt-transport-https virtualenv
```
### Install NodeJs
```bash
curl -sL https://deb.nodesource.com/setup_14.x | bash -
apt install -y nodejs
curl -sL https://deb.nodesource.com/setup_14.x | sudo bash -
sudo apt install -y nodejs
```
### Install yarn to build asset files
For Debian 11 (bullseye) and above:
```bash
curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/yarnkey.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update && sudo apt install -y yarn
```
For older Debian systems:
```bash
sudo curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list

View File

@ -23,7 +23,7 @@ def upgrade():
with op.batch_alter_table('user') as batch_op:
user = sa.sql.table('user', sa.sql.column('confirmed'))
batch_op.execute(user.update().values(confirmed=False))
batch_op.alter_column('confirmed', nullable=False)
batch_op.alter_column('confirmed', nullable=False, existing_type=sa.Boolean(), existing_nullable=True, existing_server_default=False)
def downgrade():

View File

@ -11,7 +11,9 @@
"jquery-sparkline": "^2.4.0",
"jquery-ui-dist": "^1.13.2",
"jquery.quicksearch": "^2.4.0",
"jquery-validation": "^1.19.5",
"jtimeout": "^3.2.0",
"knockout": "^3.5.1",
"multiselect": "^0.9.12"
},
"resolutions": {

View File

@ -20,6 +20,7 @@ js_login = Bundle(
'node_modules/jquery/dist/jquery.js',
'node_modules/bootstrap/dist/js/bootstrap.js',
'node_modules/icheck/icheck.js',
'node_modules/knockout/build/output/knockout-latest.js',
'custom/js/custom.js',
filters=(ConcatFilter, 'rjsmin'),
output='generated/login.js')
@ -47,6 +48,7 @@ js_main = Bundle(
'node_modules/datatables.net-bs4/js/dataTables.bootstrap4.js',
'node_modules/jquery-sparkline/jquery.sparkline.js',
'node_modules/jquery-slimscroll/jquery.slimscroll.js',
'node_modules/jquery-validation/dist/jquery.validate.js',
'node_modules/icheck/icheck.js',
'node_modules/fastclick/lib/fastclick.js',
'node_modules/moment/moment.js',
@ -55,6 +57,8 @@ js_main = Bundle(
'node_modules/datatables.net-plugins/sorting/natural.js',
'node_modules/jtimeout/src/jTimeout.js',
'node_modules/jquery.quicksearch/src/jquery.quicksearch.js',
'node_modules/knockout/build/output/knockout-latest.js',
'custom/js/app-authentication-settings-editor.js',
'custom/js/custom.js',
'node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.js',
filters=(ConcatFilter, 'rjsmin'),

View File

@ -133,50 +133,63 @@ def api_basic_auth(f):
@wraps(f)
def decorated_function(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if auth_header:
auth_header = auth_header.replace('Basic ', '', 1)
try:
auth_header = str(base64.b64decode(auth_header), 'utf-8')
username, password = auth_header.split(":")
except binascii.Error as e:
current_app.logger.error(
'Invalid base64-encoded of credential. Error {0}'.format(
e))
abort(401)
except TypeError as e:
current_app.logger.error('Error: {0}'.format(e))
abort(401)
user = User(username=username,
password=password,
plain_text_password=password)
try:
if Setting().get('verify_user_email') and user.email and not user.confirmed:
current_app.logger.warning(
'Basic authentication failed for user {} because of unverified email address'
.format(username))
abort(401)
auth_method = request.args.get('auth_method', 'LOCAL')
auth_method = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL'
auth = user.is_validate(method=auth_method,
src_ip=request.remote_addr)
if not auth:
current_app.logger.error('Checking user password failed')
abort(401)
else:
user = User.query.filter(User.username == username).first()
current_user = user # lgtm [py/unused-local-variable]
except Exception as e:
current_app.logger.error('Error: {0}'.format(e))
abort(401)
else:
if not auth_header:
current_app.logger.error('Error: Authorization header missing!')
abort(401)
if auth_header[:6] != "Basic ":
current_app.logger.error('Error: Unsupported authorization mechanism!')
abort(401)
# Remove "Basic " from the header value
auth_header = auth_header[6:]
try:
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
# username, the rest of the string would be the password.
auth_components = auth_header.split(':', maxsplit=1)
except (binascii.Error, UnicodeDecodeError) as e:
current_app.logger.error(
'Invalid base64-encoded of credential. Error {0}'.format(
e))
abort(401)
except TypeError as e:
current_app.logger.error('Error: {0}'.format(e))
abort(401)
# If we don't have two auth components (username, password), we can abort
if len(auth_components) != 2:
abort(401)
(username, password) = auth_components
user = User(username=username,
password=password,
plain_text_password=password)
try:
if Setting().get('verify_user_email') and user.email and not user.confirmed:
current_app.logger.warning(
'Basic authentication failed for user {} because of unverified email address'
.format(username))
abort(401)
auth_method = request.args.get('auth_method', 'LOCAL')
auth_method = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL'
auth = user.is_validate(method=auth_method, src_ip=request.remote_addr)
if not auth:
current_app.logger.error('Checking user password failed')
abort(401)
else:
user = User.query.filter(User.username == username).first()
current_user = user # lgtm [py/unused-local-variable]
except Exception as e:
current_app.logger.error('Error: {0}'.format(e))
abort(401)
return f(*args, **kwargs)
return decorated_function
@ -257,7 +270,7 @@ def api_can_create_domain(f):
if current_user.role.name not in [
'Administrator', 'Operator'
] and not Setting().get('allow_user_create_domain'):
msg = "User {0} does not have enough privileges to create domain"
msg = "User {0} does not have enough privileges to create zone"
current_app.logger.error(msg.format(current_user.username))
raise NotEnoughPrivileges()
@ -286,7 +299,7 @@ def apikey_can_create_domain(f):
if g.apikey.role.name not in [
'Administrator', 'Operator'
] and not Setting().get('allow_user_create_domain'):
msg = "ApiKey #{0} does not have enough privileges to create domain"
msg = "ApiKey #{0} does not have enough privileges to create zone"
current_app.logger.error(msg.format(g.apikey.id))
raise NotEnoughPrivileges()
@ -316,7 +329,7 @@ def apikey_can_remove_domain(http_methods=[]):
g.apikey.role.name not in ['Administrator', 'Operator'] and
not Setting().get('allow_user_remove_domain')
):
msg = "ApiKey #{0} does not have enough privileges to remove domain"
msg = "ApiKey #{0} does not have enough privileges to remove zone"
current_app.logger.error(msg.format(g.apikey.id))
raise NotEnoughPrivileges()
return f(*args, **kwargs)
@ -331,7 +344,7 @@ def apikey_is_admin(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.apikey.role.name != 'Administrator':
msg = "Apikey {0} does not have enough privileges to create domain"
msg = "Apikey {0} does not have enough privileges to create zone"
current_app.logger.error(msg.format(g.apikey.id))
raise NotEnoughPrivileges()
return f(*args, **kwargs)
@ -447,10 +460,8 @@ def apikey_auth(f):
if auth_header:
try:
apikey_val = str(base64.b64decode(auth_header), 'utf-8')
except binascii.Error as e:
current_app.logger.error(
'Invalid base64-encoded of credential. Error {0}'.format(
e))
except (binascii.Error, UnicodeDecodeError) as e:
current_app.logger.error('Invalid base64-encoded X-API-KEY. Error {0}'.format(e))
abort(401)
except TypeError as e:
current_app.logger.error('Error: {0}'.format(e))

View File

@ -8,6 +8,7 @@ SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
BIND_ADDRESS = '0.0.0.0'
PORT = 9191
HSTS_ENABLED = False
SERVER_EXTERNAL_SSL = None
SESSION_TYPE = 'sqlalchemy'
SESSION_COOKIE_SAMESITE = 'Lax'

View File

@ -21,7 +21,7 @@ class StructuredException(Exception):
class DomainNotExists(StructuredException):
status_code = 404
def __init__(self, name=None, message="Domain does not exist"):
def __init__(self, name=None, message="Zone does not exist"):
StructuredException.__init__(self)
self.message = message
self.name = name
@ -30,7 +30,7 @@ class DomainNotExists(StructuredException):
class DomainAlreadyExists(StructuredException):
status_code = 409
def __init__(self, name=None, message="Domain already exists"):
def __init__(self, name=None, message="Zone already exists"):
StructuredException.__init__(self)
self.message = message
self.name = name
@ -39,7 +39,7 @@ class DomainAlreadyExists(StructuredException):
class DomainAccessForbidden(StructuredException):
status_code = 403
def __init__(self, name=None, message="Domain access not allowed"):
def __init__(self, name=None, message="Zone access not allowed"):
StructuredException.__init__(self)
self.message = message
self.name = name
@ -47,7 +47,7 @@ class DomainAccessForbidden(StructuredException):
class DomainOverrideForbidden(StructuredException):
status_code = 409
def __init__(self, name=None, message="Domain override of record not allowed"):
def __init__(self, name=None, message="Zone override of record not allowed"):
StructuredException.__init__(self)
self.message = message
self.name = name
@ -67,7 +67,7 @@ class ApiKeyNotUsable(StructuredException):
def __init__(
self,
name=None,
message=("Api key must have domains or accounts"
message=("Api key must have zones or accounts"
" or an administrative role")):
StructuredException.__init__(self)
self.message = message

View File

@ -229,7 +229,7 @@ def ensure_list(l):
def pretty_domain_name(domain_name):
# Add a debugging statement to print out the domain name
print("Received domain name:", domain_name)
print("Received zone name:", domain_name)
# Check if the domain name is encoded using Punycode
if domain_name.endswith('.xn--'):
@ -238,9 +238,9 @@ def pretty_domain_name(domain_name):
domain_name = idna.decode(domain_name)
except Exception as e:
# If the decoding fails, raise an exception with more information
raise Exception('Cannot decode IDN domain: {}'.format(e))
raise Exception('Cannot decode IDN zone: {}'.format(e))
# Return the "pretty" version of the domain name
# Return the "pretty" version of the zone name
return domain_name

View File

@ -68,13 +68,13 @@ class Domain(db.Model):
return True
except Exception as e:
current_app.logger.error(
'Can not create setting {0} for domain {1}. {2}'.format(
'Can not create setting {0} for zone {1}. {2}'.format(
setting, self.name, e))
return False
def get_domain_info(self, domain_name):
"""
Get all domains which has in PowerDNS
Get all zones which has in PowerDNS
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
jdata = utils.fetch_json(urljoin(
@ -88,7 +88,7 @@ class Domain(db.Model):
def get_domains(self):
"""
Get all domains which has in PowerDNS
Get all zones which has in PowerDNS
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
jdata = utils.fetch_json(
@ -108,17 +108,17 @@ class Domain(db.Model):
return domain.id
except Exception as e:
current_app.logger.error(
'Domain does not exist. ERROR: {0}'.format(e))
'Zone does not exist. ERROR: {0}'.format(e))
return None
def search_idn_domains(self, search_string):
"""
Search for IDN domains using the provided search string.
Search for IDN zones using the provided search string.
"""
# Compile the regular expression pattern for matching IDN domain names
# Compile the regular expression pattern for matching IDN zone names
idn_pattern = re.compile(r'^xn--')
# Search for domain names that match the IDN pattern
# Search for zone names that match the IDN pattern
idn_domains = [
domain for domain in self.get_domains() if idn_pattern.match(domain)
]
@ -129,12 +129,12 @@ class Domain(db.Model):
def update(self):
"""
Fetch zones (domains) from PowerDNS and update into DB
Fetch zones (zones) from PowerDNS and update into DB
"""
db_domain = Domain.query.all()
list_db_domain = [d.name for d in db_domain]
dict_db_domain = dict((x.name, x) for x in db_domain)
current_app.logger.info("Found {} domains in PowerDNS-Admin".format(
current_app.logger.info("Found {} zones in PowerDNS-Admin".format(
len(list_db_domain)))
headers = {'X-API-Key': self.PDNS_API_KEY}
try:
@ -149,17 +149,17 @@ class Domain(db.Model):
"Found {} zones in PowerDNS server".format(len(list_jdomain)))
try:
# domains should remove from db since it doesn't exist in powerdns anymore
# zones should remove from db since it doesn't exist in powerdns anymore
should_removed_db_domain = list(
set(list_db_domain).difference(list_jdomain))
for domain_name in should_removed_db_domain:
self.delete_domain_from_pdnsadmin(domain_name, do_commit=False)
except Exception as e:
current_app.logger.error(
'Can not delete domain from DB. DETAIL: {0}'.format(e))
'Can not delete zone from DB. DETAIL: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
# update/add new domain
# update/add new zone
account_cache = {}
for data in jdata:
if 'account' in data:
@ -187,16 +187,16 @@ class Domain(db.Model):
self.add_domain_to_powerdns_admin(domain=data, do_commit=False)
db.session.commit()
current_app.logger.info('Update domain finished')
current_app.logger.info('Update zone finished')
return {
'status': 'ok',
'msg': 'Domain table has been updated successfully'
'msg': 'Zone table has been updated successfully'
}
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot update domain table. Error: {0}'.format(e))
return {'status': 'error', 'msg': 'Cannot update domain table'}
'Cannot update zone table. Error: {0}'.format(e))
return {'status': 'error', 'msg': 'Cannot update zone table'}
def update_pdns_admin_domain(self, domain, account_id, data, do_commit=True):
# existing domain, only update if something actually has changed
@ -218,11 +218,11 @@ class Domain(db.Model):
try:
if do_commit:
db.session.commit()
current_app.logger.info("Updated PDNS-Admin domain {0}".format(
current_app.logger.info("Updated PDNS-Admin zone {0}".format(
domain.name))
except Exception as e:
db.session.rollback()
current_app.logger.info("Rolled back Domain {0} {1}".format(
current_app.logger.info("Rolled back zone {0} {1}".format(
domain.name, e))
raise
@ -234,7 +234,7 @@ class Domain(db.Model):
domain_master_ips=[],
account_name=None):
"""
Add a domain to power dns
Add a zone to power dns
"""
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
@ -269,23 +269,23 @@ class Domain(db.Model):
if 'error' in jdata.keys():
current_app.logger.error(jdata['error'])
if jdata.get('http_code') == 409:
return {'status': 'error', 'msg': 'Domain already exists'}
return {'status': 'error', 'msg': 'Zone already exists'}
return {'status': 'error', 'msg': jdata['error']}
else:
current_app.logger.info(
'Added domain successfully to PowerDNS: {0}'.format(
'Added zone successfully to PowerDNS: {0}'.format(
domain_name))
self.add_domain_to_powerdns_admin(domain_dict=post_data)
return {'status': 'ok', 'msg': 'Added domain successfully'}
return {'status': 'ok', 'msg': 'Added zone successfully'}
except Exception as e:
current_app.logger.error('Cannot add domain {0} {1}'.format(
current_app.logger.error('Cannot add zone {0} {1}'.format(
domain_name, e))
current_app.logger.debug(traceback.format_exc())
return {'status': 'error', 'msg': 'Cannot add this domain.'}
return {'status': 'error', 'msg': 'Cannot add this zone.'}
def add_domain_to_powerdns_admin(self, domain=None, domain_dict=None, do_commit=True):
"""
Read Domain from PowerDNS and add into PDNS-Admin
Read zone from PowerDNS and add into PDNS-Admin
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
if not domain:
@ -299,7 +299,7 @@ class Domain(db.Model):
timeout=int(Setting().get('pdns_api_timeout')),
verify=Setting().get('verify_ssl_connections'))
except Exception as e:
current_app.logger.error('Can not read domain from PDNS')
current_app.logger.error('Can not read zone from PDNS')
current_app.logger.error(e)
current_app.logger.debug(traceback.format_exc())
@ -325,20 +325,20 @@ class Domain(db.Model):
if do_commit:
db.session.commit()
current_app.logger.info(
"Synced PowerDNS Domain to PDNS-Admin: {0}".format(d.name))
"Synced PowerDNS zone to PDNS-Admin: {0}".format(d.name))
return {
'status': 'ok',
'msg': 'Added Domain successfully to PowerDNS-Admin'
'msg': 'Added zone successfully to PowerDNS-Admin'
}
except Exception as e:
db.session.rollback()
current_app.logger.info("Rolled back Domain {0}".format(d.name))
current_app.logger.info("Rolled back zone {0}".format(d.name))
raise
def update_soa_setting(self, domain_name, soa_edit_api):
domain = Domain.query.filter(Domain.name == domain_name).first()
if not domain:
return {'status': 'error', 'msg': 'Domain does not exist.'}
return {'status': 'error', 'msg': 'Zone does not exist.'}
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
@ -365,7 +365,7 @@ class Domain(db.Model):
return {'status': 'error', 'msg': jdata['error']}
else:
current_app.logger.info(
'soa-edit-api changed for domain {0} successfully'.format(
'soa-edit-api changed for zone {0} successfully'.format(
domain_name))
return {
'status': 'ok',
@ -375,11 +375,11 @@ class Domain(db.Model):
current_app.logger.debug(e)
current_app.logger.debug(traceback.format_exc())
current_app.logger.error(
'Cannot change soa-edit-api for domain {0}'.format(
'Cannot change soa-edit-api for zone {0}'.format(
domain_name))
return {
'status': 'error',
'msg': 'Cannot change soa-edit-api for this domain.'
'msg': 'Cannot change soa-edit-api for this zone.'
}
def update_kind(self, domain_name, kind, masters=[]):
@ -388,7 +388,7 @@ class Domain(db.Model):
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if not domain:
return {'status': 'error', 'msg': 'Domain does not exist.'}
return {'status': 'error', 'msg': 'Znoe does not exist.'}
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
@ -409,26 +409,26 @@ class Domain(db.Model):
return {'status': 'error', 'msg': jdata['error']}
else:
current_app.logger.info(
'Update domain kind for {0} successfully'.format(
'Update zone kind for {0} successfully'.format(
domain_name))
return {
'status': 'ok',
'msg': 'Domain kind changed successfully'
'msg': 'Zone kind changed successfully'
}
except Exception as e:
current_app.logger.error(
'Cannot update kind for domain {0}. Error: {1}'.format(
'Cannot update kind for zone {0}. Error: {1}'.format(
domain_name, e))
current_app.logger.debug(traceback.format_exc())
return {
'status': 'error',
'msg': 'Cannot update kind for this domain.'
'msg': 'Cannot update kind for this zone.'
}
def create_reverse_domain(self, domain_name, domain_reverse_name):
"""
Check the existing reverse lookup domain,
Check the existing reverse lookup zone,
if not exists create a new one automatically
"""
domain_obj = Domain.query.filter(Domain.name == domain_name).first()
@ -448,7 +448,7 @@ class Domain(db.Model):
result = self.add(domain_reverse_name, 'Master', 'DEFAULT', [], [])
self.update()
if result['status'] == 'ok':
history = History(msg='Add reverse lookup domain {0}'.format(
history = History(msg='Add reverse lookup zone {0}'.format(
domain_reverse_name),
detail=json.dumps({
'domain_type': 'Master',
@ -459,7 +459,7 @@ class Domain(db.Model):
else:
return {
'status': 'error',
'msg': 'Adding reverse lookup domain failed'
'msg': 'Adding reverse lookup zone failed'
}
domain_user_ids = self.get_user()
if len(domain_user_ids) > 0:
@ -469,13 +469,13 @@ class Domain(db.Model):
'status':
'ok',
'msg':
'New reverse lookup domain created with granted privileges'
'New reverse lookup zone created with granted privileges'
}
return {
'status': 'ok',
'msg': 'New reverse lookup domain created without users'
'msg': 'New reverse lookup zone created without users'
}
return {'status': 'ok', 'msg': 'Reverse lookup domain already exists'}
return {'status': 'ok', 'msg': 'Reverse lookup zone already exists'}
def get_reverse_domain_name(self, reverse_host_address):
c = 1
@ -504,22 +504,22 @@ class Domain(db.Model):
def delete(self, domain_name):
"""
Delete a single domain name from powerdns
Delete a single zone name from powerdns
"""
try:
self.delete_domain_from_powerdns(domain_name)
self.delete_domain_from_pdnsadmin(domain_name)
return {'status': 'ok', 'msg': 'Delete domain successfully'}
return {'status': 'ok', 'msg': 'Delete zone successfully'}
except Exception as e:
current_app.logger.error(
'Cannot delete domain {0}'.format(domain_name))
'Cannot delete zone {0}'.format(domain_name))
current_app.logger.error(e)
current_app.logger.debug(traceback.format_exc())
return {'status': 'error', 'msg': 'Cannot delete domain'}
return {'status': 'error', 'msg': 'Cannot delete zone'}
def delete_domain_from_powerdns(self, domain_name):
"""
Delete a single domain name from powerdns
Delete a single zone name from powerdns
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
@ -531,12 +531,12 @@ class Domain(db.Model):
method='DELETE',
verify=Setting().get('verify_ssl_connections'))
current_app.logger.info(
'Deleted domain successfully from PowerDNS: {0}'.format(
'Deleted zone successfully from PowerDNS: {0}'.format(
domain_name))
return {'status': 'ok', 'msg': 'Delete domain successfully'}
return {'status': 'ok', 'msg': 'Delete zone successfully'}
def delete_domain_from_pdnsadmin(self, domain_name, do_commit=True):
# Revoke permission before deleting domain
# Revoke permission before deleting zone
domain = Domain.query.filter(Domain.name == domain_name).first()
domain_user = DomainUser.query.filter(
DomainUser.domain_id == domain.id)
@ -548,7 +548,7 @@ class Domain(db.Model):
domain_setting.delete()
domain.apikeys[:] = []
# Remove history for domain
# Remove history for zone
if not Setting().get('preserve_history'):
domain_history = History.query.filter(
History.domain_id == domain.id
@ -556,17 +556,17 @@ class Domain(db.Model):
if domain_history:
domain_history.delete()
# then remove domain
# then remove zone
Domain.query.filter(Domain.name == domain_name).delete()
if do_commit:
db.session.commit()
current_app.logger.info(
"Deleted domain successfully from pdnsADMIN: {}".format(
"Deleted zone successfully from pdnsADMIN: {}".format(
domain_name))
def get_user(self):
"""
Get users (id) who have access to this domain name
Get users (id) who have access to this zone name
"""
user_ids = []
query = db.session.query(
@ -596,7 +596,7 @@ class Domain(db.Model):
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot revoke user privileges on domain {0}. DETAIL: {1}'.
'Cannot revoke user privileges on zone {0}. DETAIL: {1}'.
format(self.name, e))
current_app.logger.debug(print(traceback.format_exc()))
@ -608,7 +608,7 @@ class Domain(db.Model):
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot grant user privileges to domain {0}. DETAIL: {1}'.
'Cannot grant user privileges to zone {0}. DETAIL: {1}'.
format(self.name, e))
current_app.logger.debug(print(traceback.format_exc()))
@ -625,7 +625,7 @@ class Domain(db.Model):
def add_user(self, user):
"""
Add a single user to Domain by User
Add a single user to zone by User
"""
try:
du = DomainUser(self.id, user.id)
@ -635,7 +635,7 @@ class Domain(db.Model):
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot add user privileges on domain {0}. DETAIL: {1}'.
'Cannot add user privileges on zone {0}. DETAIL: {1}'.
format(self.name, e))
return False
@ -667,11 +667,11 @@ class Domain(db.Model):
'There was something wrong, please contact administrator'
}
else:
return {'status': 'error', 'msg': 'This domain does not exist'}
return {'status': 'error', 'msg': 'This zone does not exist'}
def get_domain_dnssec(self, domain_name):
"""
Get domain DNSSEC information
Get zone DNSSEC information
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
@ -689,13 +689,13 @@ class Domain(db.Model):
if 'error' in jdata:
return {
'status': 'error',
'msg': 'DNSSEC is not enabled for this domain'
'msg': 'DNSSEC is not enabled for this zone'
}
else:
return {'status': 'ok', 'dnssec': jdata}
except Exception as e:
current_app.logger.error(
'Cannot get domain dnssec. DETAIL: {0}'.format(e))
'Cannot get zone dnssec. DETAIL: {0}'.format(e))
return {
'status':
'error',
@ -703,11 +703,11 @@ class Domain(db.Model):
'There was something wrong, please contact administrator'
}
else:
return {'status': 'error', 'msg': 'This domain does not exist'}
return {'status': 'error', 'msg': 'This zone does not exist'}
def enable_domain_dnssec(self, domain_name):
"""
Enable domain DNSSEC
Enable zone DNSSEC
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
@ -728,7 +728,7 @@ class Domain(db.Model):
return {
'status': 'error',
'msg':
'API-RECTIFY could not be enabled for this domain',
'API-RECTIFY could not be enabled for this zone',
'jdata': jdata
}
@ -749,7 +749,7 @@ class Domain(db.Model):
'status':
'error',
'msg':
'Cannot enable DNSSEC for this domain. Error: {0}'.
'Cannot enable DNSSEC for this zone. Error: {0}'.
format(jdata['error']),
'jdata':
jdata
@ -769,7 +769,7 @@ class Domain(db.Model):
}
else:
return {'status': 'error', 'msg': 'This domain does not exist'}
return {'status': 'error', 'msg': 'This zone does not exist'}
def delete_dnssec_key(self, domain_name, key_id):
"""
@ -794,13 +794,13 @@ class Domain(db.Model):
'status':
'error',
'msg':
'Cannot disable DNSSEC for this domain. Error: {0}'.
'Cannot disable DNSSEC for this zone. Error: {0}'.
format(jdata['error']),
'jdata':
jdata
}
# Disable API-RECTIFY for domain, AFTER deactivating DNSSEC
# Disable API-RECTIFY for zone, AFTER deactivating DNSSEC
post_data = {"api_rectify": False}
jdata = utils.fetch_json(
urljoin(
@ -815,7 +815,7 @@ class Domain(db.Model):
return {
'status': 'error',
'msg':
'API-RECTIFY could not be disabled for this domain',
'API-RECTIFY could not be disabled for this zone',
'jdata': jdata
}
@ -834,22 +834,22 @@ class Domain(db.Model):
}
else:
return {'status': 'error', 'msg': 'This domain does not exist'}
return {'status': 'error', 'msg': 'This zone does not exist'}
def assoc_account(self, account_id, update=True):
"""
Associate domain with a domain, specified by account id
Associate account with a zone, specified by account id
"""
domain_name = self.name
# Sanity check - domain name
if domain_name == "":
return {'status': False, 'msg': 'No domain name specified'}
return {'status': False, 'msg': 'No zone name specified'}
# read domain and check that it exists
domain = Domain.query.filter(Domain.name == domain_name).first()
if not domain:
return {'status': False, 'msg': 'Domain does not exist'}
return {'status': False, 'msg': 'Zone does not exist'}
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
@ -875,9 +875,9 @@ class Domain(db.Model):
else:
if update:
self.update()
msg_str = 'Account changed for domain {0} successfully'
msg_str = 'Account changed for zone {0} successfully'
current_app.logger.info(msg_str.format(domain_name))
history = History(msg='Update domain {0} associate account {1}'.format(domain.name, 'none' if account_name == '' else account_name),
history = History(msg='Update zone {0} associate account {1}'.format(domain.name, 'none' if account_name == '' else account_name),
detail = json.dumps({
'assoc_account': 'None' if account_name == '' else account_name,
'dissoc_account': 'None' if account_name_old == '' else account_name_old
@ -889,16 +889,16 @@ class Domain(db.Model):
except Exception as e:
current_app.logger.debug(e)
current_app.logger.debug(traceback.format_exc())
msg_str = 'Cannot change account for domain {0}'
msg_str = 'Cannot change account for zone {0}'
current_app.logger.error(msg_str.format(domain_name))
return {
'status': 'error',
'msg': 'Cannot change account for this domain.'
'msg': 'Cannot change account for this zone.'
}
def get_account(self):
"""
Get current account associated with this domain
Get current account associated with this zone
"""
domain = Domain.query.filter(Domain.name == self.name).first()
@ -907,7 +907,7 @@ class Domain(db.Model):
def is_valid_access(self, user_id):
"""
Check if the user is allowed to access this
domain name
zone name
"""
return db.session.query(Domain) \
.outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \
@ -919,8 +919,8 @@ class Domain(db.Model):
AccountUser.user_id == user_id
)).filter(Domain.id == self.id).first()
# Return None if this domain does not exist as record,
# Return the parent domain that hold the record if exist
# Return None if this zone does not exist as record,
# Return the parent zone that hold the record if exist
def is_overriding(self, domain_name):
upper_domain_name = '.'.join(domain_name.split('.')[1:])
while upper_domain_name != '':
@ -929,7 +929,7 @@ class Domain(db.Model):
if 'rrsets' in upper_domain:
for r in upper_domain['rrsets']:
if domain_name.rstrip('.') in r['name'].rstrip('.'):
current_app.logger.error('Domain already exists as a record: {} under domain: {}'.format(r['name'].rstrip('.'), upper_domain_name))
current_app.logger.error('Zone already exists as a record: {} under zone: {}'.format(r['name'].rstrip('.'), upper_domain_name))
return upper_domain_name
upper_domain_name = '.'.join(upper_domain_name.split('.')[1:])
return None

View File

@ -45,11 +45,11 @@ class DomainTemplate(db.Model):
return {'status': 'ok', 'msg': 'Template has been created'}
except Exception as e:
current_app.logger.error(
'Can not update domain template table. Error: {0}'.format(e))
'Can not update zone template table. Error: {0}'.format(e))
db.session.rollback()
return {
'status': 'error',
'msg': 'Can not update domain template table'
'msg': 'Can not update zone template table'
}
def delete_template(self):
@ -60,6 +60,6 @@ class DomainTemplate(db.Model):
return {'status': 'ok', 'msg': 'Template has been deleted'}
except Exception as e:
current_app.logger.error(
'Can not delete domain template. Error: {0}'.format(e))
'Can not delete zone template. Error: {0}'.format(e))
db.session.rollback()
return {'status': 'error', 'msg': 'Can not delete domain template'}
return {'status': 'error', 'msg': 'Can not delete zone template'}

View File

@ -39,9 +39,9 @@ class DomainTemplateRecord(db.Model):
db.session.commit()
except Exception as e:
current_app.logger.error(
'Can not update domain template table. Error: {0}'.format(e))
'Can not update zone template table. Error: {0}'.format(e))
db.session.rollback()
return {
'status': 'error',
'msg': 'Can not update domain template table'
'msg': 'Can not update zone template table'
}

View File

@ -46,7 +46,7 @@ class Record(object):
def get_rrsets(self, domain):
"""
Query domain's rrsets via PDNS API
Query zone's rrsets via PDNS API
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
try:
@ -59,7 +59,7 @@ class Record(object):
verify=Setting().get('verify_ssl_connections'))
except Exception as e:
current_app.logger.error(
"Cannot fetch domain's record data from remote powerdns api. DETAIL: {0}"
"Cannot fetch zone's record data from remote powerdns api. DETAIL: {0}"
.format(e))
return []
@ -77,7 +77,7 @@ class Record(object):
def add(self, domain_name, rrset):
"""
Add a record to a domain (Used by auto_ptr and DynDNS)
Add a record to a zone (Used by auto_ptr and DynDNS)
Args:
domain_name(str): The zone name
@ -115,7 +115,7 @@ class Record(object):
return {'status': 'ok', 'msg': 'Record was added successfully'}
except Exception as e:
current_app.logger.error(
"Cannot add record to domain {}. Error: {}".format(
"Cannot add record to zone {}. Error: {}".format(
domain_name, e))
current_app.logger.debug("Submitted record rrset: \n{}".format(
utils.pretty_json(rrset)))
@ -172,7 +172,7 @@ class Record(object):
record['record_name'] = utils.to_idna(record["record_name"], "encode")
#TODO: error handling
# If the record is an alias (CNAME), we will also make sure that
# the target domain is properly converted to punycode (IDN)
# the target zone is properly converted to punycode (IDN)
if record['record_type'] == 'CNAME' or record['record_type'] == 'SOA':
record['record_data'] = utils.to_idna(record['record_data'], 'encode')
#TODO: error handling
@ -343,7 +343,7 @@ class Record(object):
def apply(self, domain_name, submitted_records):
"""
Apply record changes to a domain. This function
Apply record changes to a zone. This function
will make 1 call to the PDNS API to DELETE and
REPLACE records (rrsets)
"""
@ -377,7 +377,7 @@ class Record(object):
return {'status': 'ok', 'msg': 'Record was applied successfully', 'data': (new_rrsets, del_rrsets)}
except Exception as e:
current_app.logger.error(
"Cannot apply record changes to domain {0}. Error: {1}".format(
"Cannot apply record changes to zone {0}. Error: {1}".format(
domain_name, e))
current_app.logger.debug(traceback.format_exc())
return {
@ -480,7 +480,7 @@ class Record(object):
}
except Exception as e:
current_app.logger.error(
"Cannot update auto-ptr record changes to domain {0}. Error: {1}"
"Cannot update auto-ptr record changes to zone {0}. Error: {1}"
.format(domain_name, e))
current_app.logger.debug(traceback.format_exc())
return {
@ -492,7 +492,7 @@ class Record(object):
def delete(self, domain):
"""
Delete a record from domain
Delete a record from zone
"""
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
data = {
@ -517,7 +517,7 @@ class Record(object):
return {'status': 'ok', 'msg': 'Record was removed successfully'}
except Exception as e:
current_app.logger.error(
"Cannot remove record {0}/{1}/{2} from domain {3}. DETAIL: {4}"
"Cannot remove record {0}/{1}/{2} from zone {3}. DETAIL: {4}"
.format(self.name, self.type, self.data, domain, e))
return {
'status': 'error',
@ -540,7 +540,7 @@ class Record(object):
def exists(self, domain):
"""
Check if record is present within domain records, and if it's present set self to found record
Check if record is present within zone records, and if it's present set self to found record
"""
rrsets = self.get_rrsets(domain)
for r in rrsets:
@ -588,7 +588,7 @@ class Record(object):
return {'status': 'ok', 'msg': 'Record was updated successfully'}
except Exception as e:
current_app.logger.error(
"Cannot add record {0}/{1}/{2} to domain {3}. DETAIL: {4}".
"Cannot add record {0}/{1}/{2} to zone {3}. DETAIL: {4}".
format(self.name, self.type, self.data, domain, e))
return {
'status': 'error',
@ -614,11 +614,11 @@ class Record(object):
db.session.commit()
return {
'status': True,
'msg': 'Synced local serial for domain name {0}'.format(domain)
'msg': 'Synced local serial for zone name {0}'.format(domain)
}
else:
return {
'status': False,
'msg':
'Could not find domain name {0} in local db'.format(domain)
'Could not find zone name {0} in local db'.format(domain)
}

View File

@ -13,8 +13,132 @@ class Setting(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, index=True)
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,
@ -28,7 +152,8 @@ class Setting(db.Model):
'allow_user_create_domain': False,
'allow_user_remove_domain': False,
'allow_user_view_history': False,
'delete_sso_accounts': False,
'custom_history_header': '',
'delete_sso_accounts': False,
'bg_domain_updates': False,
'enable_api_rr_history': True,
'preserve_history': False,
@ -41,53 +166,82 @@ class Setting(db.Model):
'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,
'autoprovisioning': False,
'urn_value':'',
'autoprovisioning_attribute': '',
'purge': False,
'verify_user_email': False,
'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_group': '',
'ldap_filter_username': '',
'ldap_filter_group': '',
'ldap_filter_groupname': '',
'ldap_sg_enabled': False,
'ldap_admin_group': '',
'ldap_operator_group': '',
'ldap_user_group': '',
'ldap_domain': '',
'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_token_url':
'https://github.com/login/oauth/access_token',
'github_oauth_authorize_url':
'https://github.com/login/oauth/authorize',
'google_oauth_enabled': False,
'google_oauth_client_id': '',
'google_oauth_client_secret': '',
'google_token_url': 'https://oauth2.googleapis.com/token',
'google_oauth_scope': 'openid email profile',
'google_authorize_url': 'https://accounts.google.com/o/oauth2/v2/auth',
'google_base_url': 'https://www.googleapis.com/oauth2/v3/',
'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_token_url':
'https://login.microsoftonline.com/[tenancy]/oauth2/v2.0/token',
'azure_oauth_authorize_url':
'https://login.microsoftonline.com/[tenancy]/oauth2/v2.0/authorize',
'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': '',
@ -97,22 +251,26 @@ class Setting(db.Model):
'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_metadata_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_email': 'email',
'oidc_oauth_account_name_property': '',
'oidc_oauth_account_description_property': '',
'enforce_api_ttl': False,
# Zone Record Settings
'forward_records_allow_edit': {
'A': True,
'AAAA': True,
@ -189,14 +347,103 @@ class Setting(db.Model):
'TXT': True,
'URI': 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,
}
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):
@ -210,6 +457,34 @@ class Setting(db.Model):
self.name = name
self.value = value
def convert_type(self, name, value):
import json
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 == 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:
return json.loads(value)
if var_type == str:
return str(value)
return value
def set_maintenance(self, mode):
maintenance = Setting.query.filter(
Setting.name == 'maintenance').first()
@ -262,7 +537,7 @@ class Setting(db.Model):
current_setting = Setting(name=setting, value=None)
db.session.add(current_setting)
value = str(value)
value = str(self.convert_type(setting, value))
try:
current_setting.value = value
@ -284,16 +559,28 @@ class Setting(db.Model):
result = self.query.filter(Setting.name == setting).first()
if result is not None:
if hasattr(result,'value'):
if hasattr(result, 'value'):
result = result.value
return strtobool(result) if result in [
'True', 'False'
] else result
return self.convert_type(setting, result)
else:
return self.defaults[setting]
else:
current_app.logger.error('Unknown setting queried: {0}'.format(setting))
def get_group(self, group):
if not isinstance(group, list):
group = self.groups[group]
result = {}
records = self.query.all()
for record in records:
if record.name in group:
result[record.name] = self.convert_type(record.name, record.value)
return result
def get_records_allow_to_edit(self):
return list(
set(self.get_forward_records_allow_to_edit() +

View File

@ -5,6 +5,7 @@ import bcrypt
import pyotp
import ldap
import ldap.filter
from collections import OrderedDict
from flask import current_app
from flask_login import AnonymousUserMixin
from sqlalchemy import orm
@ -90,8 +91,8 @@ class User(db.Model):
return '<User {0}>'.format(self.username)
def get_totp_uri(self):
return "otpauth://totp/PowerDNS-Admin:{0}?secret={1}&issuer=PowerDNS-Admin".format(
self.username, self.otp_secret)
return "otpauth://totp/{0}:{1}?secret={2}&issuer=PowerDNS-Admin".format(
Setting().get('site_name'), self.username, self.otp_secret)
def verify_totp(self, token):
totp = pyotp.TOTP(self.otp_secret)
@ -254,82 +255,82 @@ class User(db.Model):
if LDAP_TYPE == 'ldap':
groupSearchFilter = "(&({0}={1}){2})".format(LDAP_FILTER_GROUPNAME, ldap_username, LDAP_FILTER_GROUP)
current_app.logger.debug('Ldap groupSearchFilter {0}'.format(groupSearchFilter))
if (self.ldap_search(groupSearchFilter,
LDAP_ADMIN_GROUP)):
if (LDAP_ADMIN_GROUP and self.ldap_search(groupSearchFilter, LDAP_ADMIN_GROUP)):
role_name = 'Administrator'
current_app.logger.info(
'User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'
.format(self.username,
LDAP_ADMIN_GROUP))
elif (self.ldap_search(groupSearchFilter,
LDAP_OPERATOR_GROUP)):
.format(self.username, LDAP_ADMIN_GROUP))
elif (LDAP_OPERATOR_GROUP and self.ldap_search(groupSearchFilter, LDAP_OPERATOR_GROUP)):
role_name = 'Operator'
current_app.logger.info(
'User {0} is part of the "{1}" group that allows operator access to PowerDNS-Admin'
.format(self.username,
LDAP_OPERATOR_GROUP))
elif (self.ldap_search(groupSearchFilter,
LDAP_USER_GROUP)):
.format(self.username, LDAP_OPERATOR_GROUP))
elif (LDAP_USER_GROUP and self.ldap_search(groupSearchFilter, LDAP_USER_GROUP)):
current_app.logger.info(
'User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'
.format(self.username,
LDAP_USER_GROUP))
.format(self.username, LDAP_USER_GROUP))
else:
current_app.logger.error(
'User {0} is not part of the "{1}", "{2}" or "{3}" groups that allow access to PowerDNS-Admin'
.format(self.username,
LDAP_ADMIN_GROUP,
LDAP_OPERATOR_GROUP,
LDAP_USER_GROUP))
return False
elif LDAP_TYPE == 'ad':
ldap_admin_group_filter, ldap_operator_group, ldap_user_group = "", "", ""
if LDAP_ADMIN_GROUP:
ldap_admin_group_filter = "(memberOf:1.2.840.113556.1.4.1941:={0})".format(LDAP_ADMIN_GROUP)
if LDAP_OPERATOR_GROUP:
ldap_operator_group = "(memberOf:1.2.840.113556.1.4.1941:={0})".format(LDAP_OPERATOR_GROUP)
if LDAP_USER_GROUP:
ldap_user_group = "(memberOf:1.2.840.113556.1.4.1941:={0})".format(LDAP_USER_GROUP)
searchFilter = "(&({0}={1})(|{2}{3}{4}))".format(LDAP_FILTER_USERNAME, self.username,
ldap_admin_group_filter,
ldap_operator_group, ldap_user_group)
ldap_result = self.ldap_search(searchFilter, LDAP_BASE_DN)
user_ad_member_of = ldap_result[0][0][1].get(
'memberOf')
if not user_ad_member_of:
current_app.logger.error(
'User {0} does not belong to any group while LDAP_GROUP_SECURITY_ENABLED is ON'
'User {0} is not part of any security groups that allow access to PowerDNS-Admin'
.format(self.username))
return False
elif LDAP_TYPE == 'ad':
ldap_group_security_roles = OrderedDict(
Administrator=LDAP_ADMIN_GROUP,
Operator=LDAP_OPERATOR_GROUP,
User=LDAP_USER_GROUP,
)
user_dn = ldap_result[0][0][0]
sf_groups = ""
user_ad_member_of = [g.decode("utf-8") for g in user_ad_member_of]
for group in ldap_group_security_roles.values():
if not group:
continue
if (LDAP_ADMIN_GROUP in user_ad_member_of):
role_name = 'Administrator'
current_app.logger.info(
'User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'
.format(self.username,
LDAP_ADMIN_GROUP))
elif (LDAP_OPERATOR_GROUP in user_ad_member_of):
role_name = 'Operator'
current_app.logger.info(
'User {0} is part of the "{1}" group that allows operator access to PowerDNS-Admin'
.format(self.username,
LDAP_OPERATOR_GROUP))
elif (LDAP_USER_GROUP in user_ad_member_of):
current_app.logger.info(
'User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'
.format(self.username,
LDAP_USER_GROUP))
else:
sf_groups += f"(distinguishedName={group})"
sf_member_user = f"(member:1.2.840.113556.1.4.1941:={user_dn})"
search_filter = f"(&(|{sf_groups}){sf_member_user})"
current_app.logger.debug(f"LDAP groupSearchFilter '{search_filter}'")
ldap_user_groups = [
group[0][0]
for group in self.ldap_search(
search_filter,
LDAP_BASE_DN
)
]
if not ldap_user_groups:
current_app.logger.error(
'User {0} is not part of the "{1}", "{2}" or "{3}" groups that allow access to PowerDNS-Admin'
.format(self.username,
LDAP_ADMIN_GROUP,
LDAP_OPERATOR_GROUP,
LDAP_USER_GROUP))
f"User '{self.username}' "
"does not belong to any group "
"while LDAP_GROUP_SECURITY_ENABLED is ON"
)
return False
current_app.logger.debug(
"LDAP User security groups "
f"for user '{self.username}': "
" ".join(ldap_user_groups)
)
for role, ldap_group in ldap_group_security_roles.items():
# Continue when groups is not defined or
# user is'nt member of LDAP group
if not ldap_group or not ldap_group in ldap_user_groups:
continue
role_name = role
current_app.logger.info(
f"User '{self.username}' member of "
f"the '{ldap_group}' group that allows "
f"'{role}' access to to PowerDNS-Admin"
)
# Stop loop on first found
break
else:
current_app.logger.error('Invalid LDAP type')
return False
@ -527,7 +528,7 @@ class User(db.Model):
def get_domains(self):
"""
Get list of domains which the user is granted to have
Get list of zones which the user is granted to have
access.
Note: This doesn't include the permission granting from Account
@ -680,7 +681,7 @@ class User(db.Model):
def addMissingDomain(self, autoprovision_domain, current_domains):
"""
Add domain gathered by autoprovisioning to the current domains list of a user
Add domain gathered by autoprovisioning to the current zones list of a user
"""
from ..models.domain import Domain
user = db.session.query(User).filter(User.username == self.username).first()

View File

@ -72,8 +72,8 @@ def get_record_changes(del_rrset, add_rrset):
"""For the given record, return the state dict."""
return {
"disabled": record['disabled'],
"content": record['content'],
"comment": record.get('comment', ''),
"content": record['content'],
"comment": record.get('comment', ''),
}
add_records = get_records(add_rrset)
@ -149,8 +149,8 @@ def extract_changelogs_from_a_history_entry(out_changes, history_entry, change_n
# Sort them by the record name
if change_num in out_changes:
out_changes[change_num].sort(key=lambda change:
change.del_rrset['name'] if change.del_rrset else change.add_rrset['name']
)
change.del_rrset['name'] if change.del_rrset else change.add_rrset['name']
)
# only used for changelog per record
if record_name != None and record_type != None: # then get only the records with the specific (record_name, record_type) tuple
@ -838,10 +838,10 @@ class DetailedHistory():
detail_dict = json.loads(history.detail)
if 'domain_type' in detail_dict and 'account_id' in detail_dict: # this is a domain creation
if 'domain_type' in detail_dict and 'account_id' in detail_dict: # this is a zone creation
self.detailed_msg = render_template_string("""
<table class="table table-bordered table-striped">
<tr><td>Domain Type:</td><td>{{ domaintype }}</td></tr>
<tr><td>Zone Type:</td><td>{{ domaintype }}</td></tr>
<tr><td>Account:</td><td>{{ account }}</td></tr>
</table>
""",
@ -881,7 +881,7 @@ class DetailedHistory():
authenticator=detail_dict['authenticator'],
ip_address=detail_dict['ip_address'])
elif 'add_rrsets' in detail_dict: # this is a domain record change
elif 'add_rrsets' in detail_dict: # this is a zone record change
# changes_set = []
self.detailed_msg = ""
# extract_changelogs_from_a_history_entry(changes_set, history, 0)
@ -897,11 +897,12 @@ class DetailedHistory():
description=DetailedHistory.get_key_val(detail_dict,
"description"))
elif 'Change domain' in history.msg and 'access control' in history.msg: # added or removed a user from a domain
elif any(msg in history.msg for msg in ['Change zone',
'Change domain']) and 'access control' in history.msg: # added or removed a user from a zone
users_with_access = DetailedHistory.get_key_val(detail_dict, "user_has_access")
self.detailed_msg = render_template_string("""
<table class="table table-bordered table-striped">
<tr><td>Users with access to this domain</td><td>{{ users_with_access }}</td></tr>
<tr><td>Users with access to this zone</td><td>{{ users_with_access }}</td></tr>
<tr><td>Number of users:</td><td>{{ users_with_access | length }}</td><tr>
</table>
""",
@ -913,7 +914,7 @@ class DetailedHistory():
<tr><td>Key: </td><td>{{ keyname }}</td></tr>
<tr><td>Role:</td><td>{{ rolename }}</td></tr>
<tr><td>Description:</td><td>{{ description }}</td></tr>
<tr><td>Accessible domains with this API key:</td><td>{{ linked_domains }}</td></tr>
<tr><td>Accessible zones with this API key:</td><td>{{ linked_domains }}</td></tr>
<tr><td>Accessible accounts with this API key:</td><td>{{ linked_accounts }}</td></tr>
</table>
""",
@ -932,7 +933,7 @@ class DetailedHistory():
<tr><td>Key: </td><td>{{ keyname }}</td></tr>
<tr><td>Role:</td><td>{{ rolename }}</td></tr>
<tr><td>Description:</td><td>{{ description }}</td></tr>
<tr><td>Accessible domains with this API key:</td><td>{{ linked_domains }}</td></tr>
<tr><td>Accessible zones with this API key:</td><td>{{ linked_domains }}</td></tr>
</table>
""",
keyname=DetailedHistory.get_key_val(detail_dict, "key"),
@ -942,11 +943,11 @@ class DetailedHistory():
linked_domains=DetailedHistory.get_key_val(detail_dict,
"domains"))
elif 'Update type for domain' in history.msg:
elif any(msg in history.msg for msg in ['Update type for zone', 'Update type for domain']):
self.detailed_msg = render_template_string("""
<table class="table table-bordered table-striped">
<tr><td>Domain: </td><td>{{ domain }}</td></tr>
<tr><td>Domain type:</td><td>{{ domain_type }}</td></tr>
<tr><td>Zone: </td><td>{{ domain }}</td></tr>
<tr><td>Zone type:</td><td>{{ domain_type }}</td></tr>
<tr><td>Masters:</td><td>{{ masters }}</td></tr>
</table>
""",
@ -957,8 +958,8 @@ class DetailedHistory():
elif 'reverse' in history.msg:
self.detailed_msg = render_template_string("""
<table class="table table-bordered table-striped">
<tr><td>Domain Type: </td><td>{{ domain_type }}</td></tr>
<tr><td>Domain Master IPs:</td><td>{{ domain_master_ips }}</td></tr>
<tr><td>Zone Type: </td><td>{{ domain_type }}</td></tr>
<tr><td>Zone Master IPs:</td><td>{{ domain_master_ips }}</td></tr>
</table>
""",
domain_type=DetailedHistory.get_key_val(detail_dict,
@ -977,7 +978,8 @@ class DetailedHistory():
'status'),
history_msg=DetailedHistory.get_key_val(detail_dict, 'msg'))
elif 'Update domain' in history.msg and 'associate account' in history.msg: # When an account gets associated or dissociate with domains
elif any(msg in history.msg for msg in ['Update zone',
'Update domain']) and 'associate account' in history.msg: # When an account gets associated or dissociate with zones
self.detailed_msg = render_template_string('''
<table class="table table-bordered table-striped">
<tr><td>Associate: </td><td>{{ history_assoc_account }}</td></tr>
@ -1164,7 +1166,7 @@ def history_table(): # ajax call data
else:
# if the user isn't an administrator or operator,
# allow_user_view_history must be enabled to get here,
# so include history for the domains for the user
# so include history for the zones for the user
base_query = db.session.query(History) \
.join(Domain, History.domain_id == Domain.id) \
.outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \
@ -1233,9 +1235,14 @@ def history_table(): # ajax call data
db.or_(
History.msg.like("%domain " + domain_name) if domain_name != "*" else History.msg.like(
"%domain%"),
History.msg.like("%zone " + domain_name) if domain_name != "*" else History.msg.like(
"%zone%"),
History.msg.like(
"%domain " + domain_name + " access control") if domain_name != "*" else History.msg.like(
"%domain%access control")
"%domain%access control"),
History.msg.like(
"%zone " + domain_name + " access control") if domain_name != "*" else History.msg.like(
"%zone%access control")
),
History.created_on <= max_date if max_date != None else True,
History.created_on >= min_date if min_date != None else True,
@ -1247,8 +1254,12 @@ def history_table(): # ajax call data
histories = base_query \
.filter(
db.and_(
History.msg.like("Apply record changes to domain " + domain_name) if domain_name != "*" \
else History.msg.like("Apply record changes to domain%"),
db.or_(
History.msg.like("Apply record changes to domain " + domain_name) if domain_name != "*" \
else History.msg.like("Apply record changes to domain%"),
History.msg.like("Apply record changes to zone " + domain_name) if domain_name != "*" \
else History.msg.like("Apply record changes to zone%"),
),
History.created_on <= max_date if max_date != None else True,
History.created_on >= min_date if min_date != None else True,
History.created_by == changed_by if changed_by != None else True
@ -1391,6 +1402,7 @@ def setting_basic():
'default_domain_table_size',
'default_record_table_size',
'delete_sso_accounts',
'custom_history_header',
'deny_domain_override',
'dnssec_admins_only',
'enable_api_rr_history',
@ -1532,253 +1544,34 @@ def has_an_auth_method(local_db_enabled=None,
oidc_oauth_enabled = Setting().get('oidc_oauth_enabled')
if azure_oauth_enabled is None:
azure_oauth_enabled = Setting().get('azure_oauth_enabled')
return local_db_enabled or ldap_enabled or google_oauth_enabled or github_oauth_enabled or oidc_oauth_enabled or azure_oauth_enabled
return local_db_enabled or ldap_enabled or google_oauth_enabled or github_oauth_enabled or oidc_oauth_enabled \
or azure_oauth_enabled
@admin_bp.route('/setting/authentication', methods=['GET', 'POST'])
@login_required
@admin_role_required
def setting_authentication():
if request.method == 'GET':
return render_template('admin_setting_authentication.html')
elif request.method == 'POST':
conf_type = request.form.get('config_tab')
result = None
return render_template('admin_setting_authentication.html')
if conf_type == 'general':
local_db_enabled = True if request.form.get(
'local_db_enabled') else False
signup_enabled = True if request.form.get(
'signup_enabled', ) else False
if not has_an_auth_method(local_db_enabled=local_db_enabled):
result = {
'status':
False,
'msg':
'Must have at least one authentication method enabled.'
}
else:
Setting().set('local_db_enabled', local_db_enabled)
Setting().set('signup_enabled', signup_enabled)
result = {'status': True, 'msg': 'Saved successfully'}
elif conf_type == 'ldap':
ldap_enabled = True if request.form.get('ldap_enabled') else False
@admin_bp.route('/setting/authentication/api', methods=['POST'])
@login_required
@admin_role_required
def setting_authentication_api():
result = {'status': 1, 'messages': [], 'data': {}}
if not has_an_auth_method(ldap_enabled=ldap_enabled):
result = {
'status':
False,
'msg':
'Must have at least one authentication method enabled.'
}
else:
Setting().set('ldap_enabled', ldap_enabled)
Setting().set('ldap_type', request.form.get('ldap_type'))
Setting().set('ldap_uri', request.form.get('ldap_uri'))
Setting().set('ldap_base_dn', request.form.get('ldap_base_dn'))
Setting().set('ldap_admin_username',
request.form.get('ldap_admin_username'))
Setting().set('ldap_admin_password',
request.form.get('ldap_admin_password'))
Setting().set('ldap_filter_basic',
request.form.get('ldap_filter_basic'))
Setting().set('ldap_filter_group',
request.form.get('ldap_filter_group'))
Setting().set('ldap_filter_username',
request.form.get('ldap_filter_username'))
Setting().set('ldap_filter_groupname',
request.form.get('ldap_filter_groupname'))
Setting().set(
'ldap_sg_enabled', True
if request.form.get('ldap_sg_enabled') == 'ON' else False)
Setting().set('ldap_admin_group',
request.form.get('ldap_admin_group'))
Setting().set('ldap_operator_group',
request.form.get('ldap_operator_group'))
Setting().set('ldap_user_group',
request.form.get('ldap_user_group'))
Setting().set('ldap_domain', request.form.get('ldap_domain'))
Setting().set(
'autoprovisioning', True
if request.form.get('autoprovisioning') == 'ON' else False)
Setting().set('autoprovisioning_attribute',
request.form.get('autoprovisioning_attribute'))
if request.form.get('commit') == '1':
model = Setting()
data = json.loads(request.form.get('data'))
if request.form.get('autoprovisioning') == 'ON':
if validateURN(request.form.get('urn_value')):
Setting().set('urn_value',
request.form.get('urn_value'))
else:
return render_template('admin_setting_authentication.html',
error="Invalid urn")
else:
Setting().set('urn_value',
request.form.get('urn_value'))
for key, value in data.items():
if key in model.groups['authentication']:
model.set(key, value)
Setting().set('purge', True
if request.form.get('purge') == 'ON' else False)
result['data'] = Setting().get_group('authentication')
result = {'status': True, 'msg': 'Saved successfully'}
elif conf_type == 'google':
google_oauth_enabled = True if request.form.get(
'google_oauth_enabled') else False
if not has_an_auth_method(google_oauth_enabled=google_oauth_enabled):
result = {
'status':
False,
'msg':
'Must have at least one authentication method enabled.'
}
else:
Setting().set('google_oauth_enabled', google_oauth_enabled)
Setting().set('google_oauth_client_id',
request.form.get('google_oauth_client_id'))
Setting().set('google_oauth_client_secret',
request.form.get('google_oauth_client_secret'))
Setting().set('google_token_url',
request.form.get('google_token_url'))
Setting().set('google_oauth_scope',
request.form.get('google_oauth_scope'))
Setting().set('google_authorize_url',
request.form.get('google_authorize_url'))
Setting().set('google_base_url',
request.form.get('google_base_url'))
result = {
'status': True,
'msg':
'Saved successfully. Please reload PDA to take effect.'
}
elif conf_type == 'github':
github_oauth_enabled = True if request.form.get(
'github_oauth_enabled') else False
if not has_an_auth_method(github_oauth_enabled=github_oauth_enabled):
result = {
'status':
False,
'msg':
'Must have at least one authentication method enabled.'
}
else:
Setting().set('github_oauth_enabled', github_oauth_enabled)
Setting().set('github_oauth_key',
request.form.get('github_oauth_key'))
Setting().set('github_oauth_secret',
request.form.get('github_oauth_secret'))
Setting().set('github_oauth_scope',
request.form.get('github_oauth_scope'))
Setting().set('github_oauth_api_url',
request.form.get('github_oauth_api_url'))
Setting().set('github_oauth_token_url',
request.form.get('github_oauth_token_url'))
Setting().set('github_oauth_authorize_url',
request.form.get('github_oauth_authorize_url'))
result = {
'status': True,
'msg':
'Saved successfully. Please reload PDA to take effect.'
}
elif conf_type == 'azure':
azure_oauth_enabled = True if request.form.get(
'azure_oauth_enabled') else False
if not has_an_auth_method(azure_oauth_enabled=azure_oauth_enabled):
result = {
'status':
False,
'msg':
'Must have at least one authentication method enabled.'
}
else:
Setting().set('azure_oauth_enabled', azure_oauth_enabled)
Setting().set('azure_oauth_key',
request.form.get('azure_oauth_key'))
Setting().set('azure_oauth_secret',
request.form.get('azure_oauth_secret'))
Setting().set('azure_oauth_scope',
request.form.get('azure_oauth_scope'))
Setting().set('azure_oauth_api_url',
request.form.get('azure_oauth_api_url'))
Setting().set('azure_oauth_token_url',
request.form.get('azure_oauth_token_url'))
Setting().set('azure_oauth_authorize_url',
request.form.get('azure_oauth_authorize_url'))
Setting().set(
'azure_sg_enabled', True
if request.form.get('azure_sg_enabled') == 'ON' else False)
Setting().set('azure_admin_group',
request.form.get('azure_admin_group'))
Setting().set('azure_operator_group',
request.form.get('azure_operator_group'))
Setting().set('azure_user_group',
request.form.get('azure_user_group'))
Setting().set(
'azure_group_accounts_enabled', True
if request.form.get('azure_group_accounts_enabled') == 'ON' else False)
Setting().set('azure_group_accounts_name',
request.form.get('azure_group_accounts_name'))
Setting().set('azure_group_accounts_name_re',
request.form.get('azure_group_accounts_name_re'))
Setting().set('azure_group_accounts_description',
request.form.get('azure_group_accounts_description'))
Setting().set('azure_group_accounts_description_re',
request.form.get('azure_group_accounts_description_re'))
result = {
'status': True,
'msg':
'Saved successfully. Please reload PDA to take effect.'
}
elif conf_type == 'oidc':
oidc_oauth_enabled = True if request.form.get(
'oidc_oauth_enabled') else False
if not has_an_auth_method(oidc_oauth_enabled=oidc_oauth_enabled):
result = {
'status':
False,
'msg':
'Must have at least one authentication method enabled.'
}
else:
Setting().set(
'oidc_oauth_enabled',
True if request.form.get('oidc_oauth_enabled') else False)
Setting().set('oidc_oauth_key',
request.form.get('oidc_oauth_key'))
Setting().set('oidc_oauth_secret',
request.form.get('oidc_oauth_secret'))
Setting().set('oidc_oauth_scope',
request.form.get('oidc_oauth_scope'))
Setting().set('oidc_oauth_api_url',
request.form.get('oidc_oauth_api_url'))
Setting().set('oidc_oauth_token_url',
request.form.get('oidc_oauth_token_url'))
Setting().set('oidc_oauth_authorize_url',
request.form.get('oidc_oauth_authorize_url'))
Setting().set('oidc_oauth_metadata_url',
request.form.get('oidc_oauth_metadata_url'))
Setting().set('oidc_oauth_logout_url',
request.form.get('oidc_oauth_logout_url'))
Setting().set('oidc_oauth_username',
request.form.get('oidc_oauth_username'))
Setting().set('oidc_oauth_firstname',
request.form.get('oidc_oauth_firstname'))
Setting().set('oidc_oauth_last_name',
request.form.get('oidc_oauth_last_name'))
Setting().set('oidc_oauth_email',
request.form.get('oidc_oauth_email'))
Setting().set('oidc_oauth_account_name_property',
request.form.get('oidc_oauth_account_name_property'))
Setting().set('oidc_oauth_account_description_property',
request.form.get('oidc_oauth_account_description_property'))
result = {
'status': True,
'msg':
'Saved successfully. Please reload PDA to take effect.'
}
else:
return abort(400)
return render_template('admin_setting_authentication.html',
result=result)
return result
@admin_bp.route('/templates', methods=['GET', 'POST'])
@ -1815,7 +1608,7 @@ def create_template():
t = DomainTemplate(name=name, description=description)
result = t.create()
if result['status'] == 'ok':
history = History(msg='Add domain template {0}'.format(name),
history = History(msg='Add zone template {0}'.format(name),
detail=json.dumps({
'name': name,
'description': description
@ -1828,7 +1621,7 @@ def create_template():
return redirect(url_for('admin.create_template'))
except Exception as e:
current_app.logger.error(
'Cannot create domain template. Error: {0}'.format(e))
'Cannot create zone template. Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
abort(500)
@ -1862,7 +1655,7 @@ def create_template_from_zone():
t = DomainTemplate(name=name, description=description)
result = t.create()
if result['status'] == 'ok':
history = History(msg='Add domain template {0}'.format(name),
history = History(msg='Add zone template {0}'.format(name),
detail=json.dumps({
'name': name,
'description': description
@ -1870,7 +1663,7 @@ def create_template_from_zone():
created_by=current_user.username)
history.add()
# After creating the domain in Domain Template in the,
# After creating the zone in Zone Template in the,
# local DB. We add records into it Record Template.
records = []
domain = Domain.query.filter(Domain.name == domain_name).first()
@ -1899,7 +1692,7 @@ def create_template_from_zone():
'msg': result['msg']
}), 200)
else:
# Revert the domain template (remove it)
# Revert the zone template (remove it)
# ff we cannot add records.
t.delete_template()
return make_response(
@ -1956,7 +1749,7 @@ def edit_template(template):
ttl_options=ttl_options)
except Exception as e:
current_app.logger.error(
'Cannot open domain template page. DETAIL: {0}'.format(e))
'Cannot open zone template page. DETAIL: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
abort(500)
return redirect(url_for('admin.templates'))
@ -1994,7 +1787,7 @@ def apply_records(template):
jdata.pop('_csrf_token',
None) # don't store csrf token in the history.
history = History(
msg='Apply domain template record changes to domain template {0}'
msg='Apply zone template record changes to zone template {0}'
.format(template),
detail=json.dumps(jdata),
created_by=current_user.username)
@ -2025,7 +1818,7 @@ def delete_template(template):
result = t.delete_template()
if result['status'] == 'ok':
history = History(
msg='Deleted domain template {0}'.format(template),
msg='Deleted zone template {0}'.format(template),
detail=json.dumps({'name': template}),
created_by=current_user.username)
history.add()
@ -2055,16 +1848,16 @@ def global_search():
results = server.global_search(object_type='all', query=query)
# Filter results to domains to which the user has access permission
if current_user.role.name not in [ 'Administrator', 'Operator' ]:
if current_user.role.name not in ['Administrator', 'Operator']:
allowed_domains = db.session.query(Domain) \
.outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \
.outerjoin(Account, Domain.account_id == Account.id) \
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
.filter(
db.or_(
DomainUser.user_id == current_user.id,
AccountUser.user_id == current_user.id
)) \
db.or_(
DomainUser.user_id == current_user.id,
AccountUser.user_id == current_user.id
)) \
.with_entities(Domain.name) \
.all()
allowed_domains = [value for value, in allowed_domains]
@ -2129,3 +1922,10 @@ def validateURN(value):
return False
return True
def safe_cast(val, to_type, default=None):
try:
return to_type(val)
except (ValueError, TypeError):
return default

View File

@ -48,6 +48,12 @@ user_detailed_schema = UserDetailedSchema()
account_schema = AccountSchema(many=True)
account_single_schema = AccountSchema()
def is_custom_header_api():
custom_header_setting = Setting().get('custom_history_header')
if custom_header_setting != '' and custom_header_setting in request.headers:
return request.headers[custom_header_setting]
else:
return g.apikey.description
def get_user_domains():
domains = db.session.query(Domain) \
@ -205,7 +211,7 @@ def api_login_create_zone():
accept='application/json; q=1',
verify=Setting().get('verify_ssl_connections'))
except Exception as e:
current_app.logger.error("Cannot create domain. Error: {}".format(e))
current_app.logger.error("Cannot create zone. Error: {}".format(e))
abort(500)
if resp.status_code == 201:
@ -216,7 +222,7 @@ def api_login_create_zone():
domain.update()
domain_id = domain.get_id_by_name(data['name'].rstrip('.'))
history = History(msg='Add domain {0}'.format(
history = History(msg='Add zone {0}'.format(
data['name'].rstrip('.')),
detail=json.dumps(data),
created_by=current_user.username,
@ -224,7 +230,7 @@ def api_login_create_zone():
history.add()
if current_user.role.name not in ['Administrator', 'Operator']:
current_app.logger.debug("User is ordinary user, assigning created domain")
current_app.logger.debug("User is ordinary user, assigning created zone")
domain = Domain(name=data['name'].rstrip('.'))
domain.update()
domain.grant_privileges([current_user.id])
@ -290,7 +296,7 @@ def api_login_delete_zone(domain_name):
domain_id = domain.get_id_by_name(domain_name)
domain.update()
history = History(msg='Delete domain {0}'.format(
history = History(msg='Delete zone {0}'.format(
utils.pretty_domain_name(domain_name)),
detail='',
created_by=current_user.username,
@ -341,13 +347,13 @@ def api_generate_apikey():
abort(400)
if role_name == 'User' and len(domains) == 0 and len(accounts) == 0:
current_app.logger.error("Apikey with User role must have domains or accounts")
current_app.logger.error("Apikey with User role must have zones or accounts")
raise ApiKeyNotUsable()
if role_name == 'User' and len(domains) > 0:
domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all()
if len(domain_obj_list) == 0:
msg = "One of supplied domains does not exist"
msg = "One of supplied zones does not exist"
current_app.logger.error(msg)
raise DomainNotExists(message=msg)
@ -377,13 +383,13 @@ def api_generate_apikey():
domain_list = [item.name for item in domain_obj_list]
user_domain_list = [item.name for item in user_domain_obj_list]
current_app.logger.debug("Input domain list: {0}".format(domain_list))
current_app.logger.debug("User domain list: {0}".format(user_domain_list))
current_app.logger.debug("Input zone list: {0}".format(domain_list))
current_app.logger.debug("User zone list: {0}".format(user_domain_list))
inter = set(domain_list).intersection(set(user_domain_list))
if not (len(inter) == len(domain_list)):
msg = "You don't have access to one of domains"
msg = "You don't have access to one of zones"
current_app.logger.error(msg)
raise DomainAccessForbidden(message=msg)
@ -411,7 +417,7 @@ def api_get_apikeys(domain_name):
if current_user.role.name not in ['Administrator', 'Operator']:
if domain_name:
msg = "Check if domain {0} exists and is allowed for user.".format(
msg = "Check if zone {0} exists and is allowed for user.".format(
domain_name)
current_app.logger.debug(msg)
apikeys = get_user_apikeys(domain_name)
@ -421,7 +427,7 @@ def api_get_apikeys(domain_name):
current_app.logger.debug(apikey_schema.dump(apikeys))
else:
msg_str = "Getting all allowed domains for user {0}"
msg_str = "Getting all allowed zones for user {0}"
msg = msg_str.format(current_user.username)
current_app.logger.debug(msg)
@ -432,7 +438,7 @@ def api_get_apikeys(domain_name):
current_app.logger.error('Error: {0}'.format(e))
abort(500)
else:
current_app.logger.debug("Getting all domains for administrative user")
current_app.logger.debug("Getting all zones for administrative user")
try:
apikeys = ApiKey.query.all()
current_app.logger.debug(apikey_schema.dump(apikeys))
@ -482,7 +488,7 @@ def api_delete_apikey(apikey_id):
inter = set(apikey_domains_list).intersection(set(user_domains_list))
if not (len(inter) == len(apikey_domains_list)):
msg = "You don't have access to some domains apikey belongs to"
msg = "You don't have access to some zones apikey belongs to"
current_app.logger.error(msg)
raise DomainAccessForbidden(message=msg)
@ -552,7 +558,7 @@ def api_update_apikey(apikey_id):
if domains is not None:
domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all()
if len(domain_obj_list) != len(domains):
msg = "One of supplied domains does not exist"
msg = "One of supplied zones does not exist"
current_app.logger.error(msg)
raise DomainNotExists(message=msg)
@ -572,12 +578,12 @@ def api_update_apikey(apikey_id):
target_accounts = current_accounts
if len(target_domains) == 0 and len(target_accounts) == 0:
current_app.logger.error("Apikey with User role must have domains or accounts")
current_app.logger.error("Apikey with User role must have zones or accounts")
raise ApiKeyNotUsable()
if domains is not None and set(domains) == set(current_domains):
current_app.logger.debug(
"Domains are the same, apikey domains won't be updated")
"Zones are the same, apikey zones won't be updated")
domains = None
if accounts is not None and set(accounts) == set(current_accounts):
@ -604,19 +610,19 @@ def api_update_apikey(apikey_id):
domain_list = [item.name for item in domain_obj_list]
user_domain_list = [item.name for item in user_domain_obj_list]
current_app.logger.debug("Input domain list: {0}".format(domain_list))
current_app.logger.debug("Input zone list: {0}".format(domain_list))
current_app.logger.debug(
"User domain list: {0}".format(user_domain_list))
"User zone list: {0}".format(user_domain_list))
inter = set(domain_list).intersection(set(user_domain_list))
if not (len(inter) == len(domain_list)):
msg = "You don't have access to one of domains"
msg = "You don't have access to one of zones"
current_app.logger.error(msg)
raise DomainAccessForbidden(message=msg)
if apikey_id not in apikeys_ids:
msg = 'Apikey does not belong to domain to which user has access'
msg = 'Apikey does not belong to zone to which user has access'
current_app.logger.error(msg)
raise DomainAccessForbidden()
@ -960,9 +966,9 @@ def api_delete_account(account_id):
# Remove account association from domains first
if len(account.domains) > 0:
for domain in account.domains:
current_app.logger.info(f"Disassociating domain {domain.name} with {account.name}")
current_app.logger.info(f"Disassociating zone {domain.name} with {account.name}")
Domain(name=domain.name).assoc_account(None, update=False)
current_app.logger.info("Syncing all domains")
current_app.logger.info("Syncing all zones")
Domain().update()
current_app.logger.debug(
@ -1104,31 +1110,32 @@ def api_zone_forward(server_id, zone_id):
domain = Domain()
domain.update()
status = resp.status_code
created_by_value=is_custom_header_api()
if 200 <= status < 300:
current_app.logger.debug("Request to powerdns API successful")
if Setting().get('enable_api_rr_history'):
if request.method in ['POST', 'PATCH']:
data = request.get_json(force=True)
history = History(
msg='Apply record changes to domain {0}'.format(zone_id.rstrip('.')),
msg='Apply record changes to zone {0}'.format(zone_id.rstrip('.')),
detail = json.dumps({
'domain': zone_id.rstrip('.'),
'add_rrsets': list(filter(lambda r: r['changetype'] == "REPLACE", data['rrsets'])),
'del_rrsets': list(filter(lambda r: r['changetype'] == "DELETE", data['rrsets']))
}),
created_by=g.apikey.description,
created_by=created_by_value,
domain_id=Domain().get_id_by_name(zone_id.rstrip('.')))
history.add()
elif request.method == 'DELETE':
history = History(msg='Deleted zone {0}'.format(zone_id.rstrip('.')),
detail='',
created_by=g.apikey.description,
created_by=created_by_value,
domain_id=Domain().get_id_by_name(zone_id.rstrip('.')))
history.add()
elif request.method != 'GET':
history = History(msg='Updated zone {0}'.format(zone_id.rstrip('.')),
detail='',
created_by=g.apikey.description,
created_by=created_by_value,
domain_id=Domain().get_id_by_name(zone_id.rstrip('.')))
history.add()
return resp.content, resp.status_code, resp.headers.items()
@ -1152,21 +1159,22 @@ def api_create_zone(server_id):
if resp.status_code == 201:
current_app.logger.debug("Request to powerdns API successful")
created_by_value=is_custom_header_api()
data = request.get_json(force=True)
if g.apikey.role.name not in ['Administrator', 'Operator']:
current_app.logger.debug(
"Apikey is user key, assigning created domain")
"Apikey is user key, assigning created zone")
domain = Domain(name=data['name'].rstrip('.'))
g.apikey.domains.append(domain)
domain = Domain()
domain.update()
history = History(msg='Add domain {0}'.format(
history = History(msg='Add zone {0}'.format(
data['name'].rstrip('.')),
detail=json.dumps(data),
created_by=g.apikey.description,
created_by=created_by_value,
domain_id=domain.get_id_by_name(data['name'].rstrip('.')))
history.add()
@ -1190,7 +1198,7 @@ def api_get_zones(server_id):
accounts_domains = [d.name for a in g.apikey.accounts for d in a.domains]
allowed_domains = set(domain_list + accounts_domains)
current_app.logger.debug("Account domains: {}".format('/'.join(accounts_domains)))
current_app.logger.debug("Account zones: {}".format('/'.join(accounts_domains)))
content = json.dumps([i for i in json.loads(resp.content)
if i['name'].rstrip('.') in allowed_domains])
return content, resp.status_code, resp.headers.items()
@ -1228,14 +1236,14 @@ def health():
domain_to_query = domain.query.first()
if not domain_to_query:
current_app.logger.error("No domain found to query a health check")
current_app.logger.error("No zone found to query a health check")
return make_response("Unknown", 503)
try:
domain.get_domain_info(domain_to_query.name)
except Exception as e:
current_app.logger.error(
"Health Check - Failed to query authoritative server for domain {}".format(domain_to_query.name))
"Health Check - Failed to query authoritative server for zone {}".format(domain_to_query.name))
return make_response("Down", 503)
return make_response("Up", 200)

View File

@ -60,15 +60,31 @@ def login_via_authorization_header_or_remote_user(request):
# Try to login using Basic Authentication
auth_header = request.headers.get('Authorization')
if auth_header:
if auth_header[:6] != "Basic ":
return None
auth_method = request.args.get('auth_method', 'LOCAL')
auth_method = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL'
auth_header = auth_header.replace('Basic ', '', 1)
# Remove "Basic " from the header value
auth_header = auth_header[6:]
try:
auth_header = str(base64.b64decode(auth_header), 'utf-8')
username, password = auth_header.split(":")
except TypeError as e:
except (UnicodeDecodeError, TypeError) as e:
return None
# 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
# username, the rest of the string would be the password.
auth_components = auth_header.split(':', maxsplit=1)
# If we don't have two auth components (username, password), we can return
if len(auth_components) != 2:
return None
(username, password) = auth_components
user = User(username=username,
password=password,
plain_text_password=password)

View File

@ -141,7 +141,7 @@ def domains_custom(tab_id):
filtered_count = domains.count()
start = int(request.args.get("start", 0))
length = min(int(request.args.get("length", 0)), 100)
length = min(int(request.args.get("length", 0)), max(100, int(Setting().get('default_domain_table_size'))))
if length != -1:
domains = domains[start:start + length]
@ -176,10 +176,10 @@ def dashboard():
BG_DOMAIN_UPDATE = Setting().get('bg_domain_updates')
if not BG_DOMAIN_UPDATE:
current_app.logger.info('Updating domains in foreground...')
current_app.logger.info('Updating zones in foreground...')
Domain().update()
else:
current_app.logger.info('Updating domains in background...')
current_app.logger.info('Updating zones in background...')
show_bg_domain_button = BG_DOMAIN_UPDATE
if BG_DOMAIN_UPDATE and current_user.role.name not in ['Administrator', 'Operator']:
@ -196,7 +196,7 @@ def dashboard():
@login_required
@operator_role_required
def domains_updater():
current_app.logger.debug('Update domains in background')
current_app.logger.debug('Update zones in background')
d = Domain().update()
response_data = {

View File

@ -66,7 +66,7 @@ def domain(domain_name):
current_app.logger.debug("Fetched rrsets: \n{}".format(pretty_json(rrsets)))
# API server might be down, misconfigured
if not rrsets and domain.type != 'Slave':
if not rrsets and domain.type != 'slave':
abort(500)
quick_edit = Setting().get('record_quick_edit')
@ -178,7 +178,7 @@ def remove():
if result['status'] == 'error':
abort(500)
history = History(msg='Delete domain {0}'.format(
history = History(msg='Delete zone {0}'.format(
pretty_domain_name(domain_name)),
created_by=current_user.username)
history.add()
@ -206,7 +206,7 @@ def changelog(domain_name):
current_app.logger.debug("Fetched rrsets: \n{}".format(pretty_json(rrsets)))
# API server might be down, misconfigured
if not rrsets and domain.type != 'Slave':
if not rrsets and domain.type != 'slave':
abort(500)
records_allow_to_edit = Setting().get_records_allow_to_edit()
@ -294,7 +294,7 @@ def record_changelog(domain_name, record_name, record_type):
current_app.logger.debug("Fetched rrsets: \n{}".format(pretty_json(rrsets)))
# API server might be down, misconfigured
if not rrsets and domain.type != 'Slave':
if not rrsets and domain.type != 'slave':
abort(500)
# get all changelogs for this domain, in descening order
@ -362,7 +362,7 @@ def add():
if ' ' in domain_name or not domain_name or not domain_type:
return render_template(
'errors/400.html',
msg="Please enter a valid domain name"), 400
msg="Please enter a valid zone name"), 400
if domain_name.endswith('.'):
domain_name = domain_name[:-1]
@ -385,11 +385,11 @@ def add():
try:
domain_name = to_idna(domain_name, 'encode')
except:
current_app.logger.error("Cannot encode the domain name {}".format(domain_name))
current_app.logger.error("Cannot encode the zone name {}".format(domain_name))
current_app.logger.debug(traceback.format_exc())
return render_template(
'errors/400.html',
msg="Please enter a valid domain name"), 400
msg="Please enter a valid zone name"), 400
if domain_type == 'slave':
if request.form.getlist('domain_master_address'):
@ -429,7 +429,7 @@ def add():
else:
accounts = current_user.get_accounts()
msg = 'Domain already exists as a record under domain: {}'.format(upper_domain)
msg = 'Zone already exists as a record under zone: {}'.format(upper_domain)
return render_template('domain_add.html',
domain_override_message=msg,
@ -443,7 +443,7 @@ def add():
account_name=account_name)
if result['status'] == 'ok':
domain_id = Domain().get_id_by_name(domain_name)
history = History(msg='Add domain {0}'.format(
history = History(msg='Add zone {0}'.format(
pretty_domain_name(domain_name)),
detail = json.dumps({
'domain_type': domain_type,
@ -507,7 +507,7 @@ def add():
return render_template('errors/400.html',
msg=result['msg']), 400
except Exception as e:
current_app.logger.error('Cannot add domain. Error: {0}'.format(e))
current_app.logger.error('Cannot add zone. Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
abort(500)
@ -537,7 +537,7 @@ def delete(domain_name):
if result['status'] == 'error':
abort(500)
history = History(msg='Delete domain {0}'.format(
history = History(msg='Delete zone {0}'.format(
pretty_domain_name(domain_name)),
created_by=current_user.username)
history.add()
@ -560,13 +560,17 @@ def setting(domain_name):
d = Domain(name=domain_name)
domain_user_ids = d.get_user()
account = d.get_account()
domain_info = d.get_domain_info(domain_name)
return render_template('domain_setting.html',
domain=domain,
users=users,
domain_user_ids=domain_user_ids,
accounts=accounts,
domain_account=account)
domain_account=account,
zone_type=domain_info["kind"].lower(),
masters=','.join(domain_info["masters"]),
soa_edit_api=domain_info["soa_edit_api"].upper())
if request.method == 'POST':
# username in right column
@ -581,7 +585,7 @@ def setting(domain_name):
d.grant_privileges(new_user_ids)
history = History(
msg='Change domain {0} access control'.format(
msg='Change zone {0} access control'.format(
pretty_domain_name(domain_name)),
detail=json.dumps({'user_has_access': new_user_list}),
created_by=current_user.username,
@ -619,7 +623,7 @@ def change_type(domain_name):
kind=domain_type,
masters=domain_master_ips)
if status['status'] == 'ok':
history = History(msg='Update type for domain {0}'.format(
history = History(msg='Update type for zone {0}'.format(
pretty_domain_name(domain_name)),
detail=json.dumps({
"domain": domain_name,
@ -653,7 +657,7 @@ def change_soa_edit_api(domain_name):
soa_edit_api=new_setting)
if status['status'] == 'ok':
history = History(
msg='Update soa_edit_api for domain {0}'.format(
msg='Update soa_edit_api for zone {0}'.format(
pretty_domain_name(domain_name)),
detail = json.dumps({
'domain': domain_name,
@ -697,7 +701,7 @@ def record_apply(domain_name):
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
current_app.logger.debug('Current domain serial: {0}'.format(
current_app.logger.debug('Current zone serial: {0}'.format(
domain.serial))
if int(submitted_serial) != domain.serial:
@ -714,14 +718,14 @@ def record_apply(domain_name):
'status':
'error',
'msg':
'Domain name {0} does not exist'.format(pretty_domain_name(domain_name))
'Zone name {0} does not exist'.format(pretty_domain_name(domain_name))
}), 404)
r = Record()
result = r.apply(domain_name, submitted_record)
if result['status'] == 'ok':
history = History(
msg='Apply record changes to domain {0}'.format(pretty_domain_name(domain_name)),
msg='Apply record changes to zone {0}'.format(pretty_domain_name(domain_name)),
detail = json.dumps({
'domain': domain_name,
'add_rrsets': result['data'][0]['rrsets'],
@ -733,7 +737,7 @@ def record_apply(domain_name):
return make_response(jsonify(result), 200)
else:
history = History(
msg='Failed to apply record changes to domain {0}'.format(
msg='Failed to apply record changes to zone {0}'.format(
pretty_domain_name(domain_name)),
detail = json.dumps({
'domain': domain_name,
@ -760,7 +764,7 @@ def record_apply(domain_name):
@can_access_domain
def record_update(domain_name):
"""
This route is used for domain work as Slave Zone only
This route is used for zone work as Slave Zone only
Pulling the records update from its Master
"""
try:
@ -818,7 +822,7 @@ def dnssec_enable(domain_name):
dnssec = domain.enable_domain_dnssec(domain_name)
domain_object = Domain.query.filter(domain_name == Domain.name).first()
history = History(
msg='DNSSEC was enabled for domain ' + domain_name ,
msg='DNSSEC was enabled for zone ' + domain_name ,
created_by=current_user.username,
domain_id=domain_object.id)
history.add()
@ -837,7 +841,7 @@ def dnssec_disable(domain_name):
domain.delete_dnssec_key(domain_name, key['id'])
domain_object = Domain.query.filter(domain_name == Domain.name).first()
history = History(
msg='DNSSEC was disabled for domain ' + domain_name ,
msg='DNSSEC was disabled for zone ' + domain_name ,
created_by=current_user.username,
domain_id=domain_object.id)
history.add()
@ -914,7 +918,7 @@ def admin_setdomainsetting(domain_name):
}), 400)
except Exception as e:
current_app.logger.error(
'Cannot change domain setting. Error: {0}'.format(e))
'Cannot change zone setting. Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
return make_response(
jsonify({

View File

@ -5,6 +5,8 @@ import traceback
import datetime
import ipaddress
import base64
import string
from zxcvbn import zxcvbn
from distutils.util import strtobool
from yaml import Loader, load
from flask import Blueprint, render_template, make_response, url_for, current_app, g, session, request, redirect, abort
@ -43,6 +45,7 @@ index_bp = Blueprint('index',
template_folder='templates',
url_prefix='/')
@index_bp.before_app_first_request
def register_modules():
global google
@ -66,7 +69,7 @@ def before_request():
# Check site is in maintenance mode
maintenance = Setting().get('maintenance')
if maintenance and current_user.is_authenticated and current_user.role.name not in [
'Administrator', 'Operator'
'Administrator', 'Operator'
]:
return render_template('maintenance.html')
@ -96,7 +99,11 @@ def google_login():
)
abort(400)
else:
redirect_uri = url_for('google_authorized', _external=True)
use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL')
params = {'_external': True}
if isinstance(use_ssl, bool):
params['_scheme'] = 'https' if use_ssl else 'http'
redirect_uri = url_for('google_authorized', **params)
return google.authorize_redirect(redirect_uri)
@ -108,7 +115,11 @@ def github_login():
)
abort(400)
else:
redirect_uri = url_for('github_authorized', _external=True)
use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL')
params = {'_external': True}
if isinstance(use_ssl, bool):
params['_scheme'] = 'https' if use_ssl else 'http'
redirect_uri = url_for('github_authorized', **params)
return github.authorize_redirect(redirect_uri)
@ -120,9 +131,11 @@ def azure_login():
)
abort(400)
else:
redirect_uri = url_for('azure_authorized',
_external=True,
_scheme='https')
use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL')
params = {'_external': True}
if isinstance(use_ssl, bool):
params['_scheme'] = 'https' if use_ssl else 'http'
redirect_uri = url_for('azure_authorized', **params)
return azure.authorize_redirect(redirect_uri)
@ -134,7 +147,11 @@ def oidc_login():
)
abort(400)
else:
redirect_uri = url_for('oidc_authorized', _external=True)
use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL')
params = {'_external': True}
if isinstance(use_ssl, bool):
params['_scheme'] = 'https' if use_ssl else 'http'
redirect_uri = url_for('oidc_authorized', **params)
return oidc.authorize_redirect(redirect_uri)
@ -147,18 +164,18 @@ def login():
if 'google_token' in session:
user_data = json.loads(google.get('userinfo').text)
first_name = user_data['given_name']
surname = user_data['family_name']
email = user_data['email']
user = User.query.filter_by(username=email).first()
google_first_name = user_data['given_name']
google_last_name = user_data['family_name']
google_email = user_data['email']
user = User.query.filter_by(username=google_email).first()
if user is None:
user = User.query.filter_by(email=email).first()
user = User.query.filter_by(email=google_email).first()
if not user:
user = User(username=email,
firstname=first_name,
lastname=surname,
user = User(username=google_email,
firstname=google_first_name,
lastname=google_last_name,
plain_text_password=None,
email=email)
email=google_email)
result = user.create_local_user()
if not result['status']:
@ -170,10 +187,18 @@ def login():
return authenticate_user(user, 'Google OAuth')
if 'github_token' in session:
me = json.loads(github.get('user').text)
github_username = me['login']
github_name = me['name']
github_email = me['email']
user_data = json.loads(github.get('user').text)
github_username = user_data['login']
github_first_name = user_data['name']
github_last_name = ''
github_email = user_data['email']
# If the user's full name from GitHub contains at least two words, use the first word as the first name and
# the rest as the last name.
github_name_parts = github_first_name.split(' ')
if len(github_name_parts) > 1:
github_first_name = github_name_parts[0]
github_last_name = ' '.join(github_name_parts[1:])
user = User.query.filter_by(username=github_username).first()
if user is None:
@ -181,8 +206,8 @@ def login():
if not user:
user = User(username=github_username,
plain_text_password=None,
firstname=github_name,
lastname='',
firstname=github_first_name,
lastname=github_last_name,
email=github_email)
result = user.create_local_user()
@ -196,8 +221,8 @@ def login():
if 'azure_token' in session:
azure_info = azure.get('me?$select=displayName,givenName,id,mail,surname,userPrincipalName').text
current_app.logger.info('Azure login returned: '+azure_info)
me = json.loads(azure_info)
current_app.logger.info('Azure login returned: ' + azure_info)
user_data = json.loads(azure_info)
azure_info = azure.post('me/getMemberGroups',
json={'securityEnabledOnly': False}).text
@ -209,15 +234,15 @@ def login():
else:
mygroups = []
azure_username = me["userPrincipalName"]
azure_givenname = me["givenName"]
azure_familyname = me["surname"]
if "mail" in me:
azure_email = me["mail"]
azure_username = user_data["userPrincipalName"]
azure_first_name = user_data["givenName"]
azure_last_name = user_data["surname"]
if "mail" in user_data:
azure_email = user_data["mail"]
else:
azure_email = ""
if not azure_email:
azure_email = me["userPrincipalName"]
azure_email = user_data["userPrincipalName"]
# Handle foreign principals such as guest users
azure_email = re.sub(r"#.*$", "", azure_email)
@ -227,8 +252,8 @@ def login():
if not user:
user = User(username=azure_username,
plain_text_password=None,
firstname=azure_givenname,
lastname=azure_familyname,
firstname=azure_first_name,
lastname=azure_last_name,
email=azure_email)
result = user.create_local_user()
@ -248,30 +273,30 @@ def login():
if Setting().get('azure_sg_enabled'):
if Setting().get('azure_admin_group') in mygroups:
current_app.logger.info('Setting role for user ' +
azure_username +
' to Administrator due to group membership')
azure_username +
' to Administrator due to group membership')
user.set_role("Administrator")
else:
if Setting().get('azure_operator_group') in mygroups:
current_app.logger.info('Setting role for user ' +
azure_username +
' to Operator due to group membership')
azure_username +
' to Operator due to group membership')
user.set_role("Operator")
else:
if Setting().get('azure_user_group') in mygroups:
current_app.logger.info('Setting role for user ' +
azure_username +
' to User due to group membership')
azure_username +
' to User due to group membership')
user.set_role("User")
else:
current_app.logger.warning('User ' +
azure_username +
' has no relevant group memberships')
azure_username +
' has no relevant group memberships')
session.pop('azure_token', None)
return render_template('login.html',
saml_enabled=SAML_ENABLED,
error=('User ' + azure_username +
' is not in any authorised groups.'))
saml_enabled=SAML_ENABLED,
error=('User ' + azure_username +
' is not in any authorised groups.'))
# Handle account/group creation, if enabled
if Setting().get('azure_group_accounts_enabled') and mygroups:
@ -367,23 +392,23 @@ def login():
return authenticate_user(user, 'Azure OAuth')
if 'oidc_token' in session:
me = json.loads(oidc.get('userinfo').text)
oidc_username = me[Setting().get('oidc_oauth_username')]
oidc_givenname = me[Setting().get('oidc_oauth_firstname')]
oidc_familyname = me[Setting().get('oidc_oauth_last_name')]
oidc_email = me[Setting().get('oidc_oauth_email')]
user_data = json.loads(oidc.get('userinfo').text)
oidc_username = user_data[Setting().get('oidc_oauth_username')]
oidc_first_name = user_data[Setting().get('oidc_oauth_firstname')]
oidc_last_name = user_data[Setting().get('oidc_oauth_last_name')]
oidc_email = user_data[Setting().get('oidc_oauth_email')]
user = User.query.filter_by(username=oidc_username).first()
if not user:
user = User(username=oidc_username,
plain_text_password=None,
firstname=oidc_givenname,
lastname=oidc_familyname,
firstname=oidc_first_name,
lastname=oidc_last_name,
email=oidc_email)
result = user.create_local_user()
else:
user.firstname = oidc_givenname
user.lastname = oidc_familyname
user.firstname = oidc_first_name
user.lastname = oidc_last_name
user.email = oidc_email
user.plain_text_password = None
result = user.update_local_user()
@ -392,20 +417,22 @@ def login():
session.pop('oidc_token', None)
return redirect(url_for('index.login'))
#This checks if the account_name_property and account_description property were included in settings.
if Setting().get('oidc_oauth_account_name_property') and Setting().get('oidc_oauth_account_description_property'):
# This checks if the account_name_property and account_description property were included in settings.
if Setting().get('oidc_oauth_account_name_property') and Setting().get(
'oidc_oauth_account_description_property'):
#Gets the name_property and description_property.
# Gets the name_property and description_property.
name_prop = Setting().get('oidc_oauth_account_name_property')
desc_prop = Setting().get('oidc_oauth_account_description_property')
account_to_add = []
#If the name_property and desc_property exist in me (A variable that contains all the userinfo from the IdP).
if name_prop in me and desc_prop in me:
accounts_name_prop = [me[name_prop]] if type(me[name_prop]) is not list else me[name_prop]
accounts_desc_prop = [me[desc_prop]] if type(me[desc_prop]) is not list else me[desc_prop]
# If the name_property and desc_property exist in me (A variable that contains all the userinfo from the
# IdP).
if name_prop in user_data and desc_prop in user_data:
accounts_name_prop = [user_data[name_prop]] if type(user_data[name_prop]) is not list else user_data[name_prop]
accounts_desc_prop = [user_data[desc_prop]] if type(user_data[desc_prop]) is not list else user_data[desc_prop]
#Run on all groups the user is in by the index num.
# Run on all groups the user is in by the index num.
for i in range(len(accounts_name_prop)):
description = ''
if i < len(accounts_desc_prop):
@ -415,7 +442,7 @@ def login():
account_to_add.append(account)
user_accounts = user.get_accounts()
# Add accounts
# Add accounts
for account in account_to_add:
if account not in user_accounts:
account.add_user(user)
@ -424,7 +451,7 @@ def login():
if Setting().get('delete_sso_accounts'):
for account in user_accounts:
if account not in account_to_add:
account.remove_user(user)
account.remove_user(user)
session['user_id'] = user.id
session['authentication_type'] = 'OAuth'
@ -488,34 +515,36 @@ def login():
saml_enabled=SAML_ENABLED,
error='Token required')
if Setting().get('autoprovisioning') and auth_method!='LOCAL':
urn_value=Setting().get('urn_value')
Entitlements=user.read_entitlements(Setting().get('autoprovisioning_attribute'))
if len(Entitlements)==0 and Setting().get('purge'):
if Setting().get('autoprovisioning') and auth_method != 'LOCAL':
urn_value = Setting().get('urn_value')
Entitlements = user.read_entitlements(Setting().get('autoprovisioning_attribute'))
if len(Entitlements) == 0 and Setting().get('purge'):
user.set_role("User")
user.revoke_privilege(True)
elif len(Entitlements)!=0:
elif len(Entitlements) != 0:
if checkForPDAEntries(Entitlements, urn_value):
user.updateUser(Entitlements)
else:
current_app.logger.warning('Not a single powerdns-admin record was found, possibly a typo in the prefix')
current_app.logger.warning(
'Not a single powerdns-admin record was found, possibly a typo in the prefix')
if Setting().get('purge'):
user.set_role("User")
user.revoke_privilege(True)
current_app.logger.warning('Procceding to revoke every privilige from ' + user.username + '.' )
current_app.logger.warning('Procceding to revoke every privilige from ' + user.username + '.')
return authenticate_user(user, auth_method, remember_me)
def checkForPDAEntries(Entitlements, urn_value):
"""
Run through every record located in the ldap attribute given and determine if there are any valid powerdns-admin records
"""
urnArguments=[x.lower() for x in urn_value.split(':')]
urnArguments = [x.lower() for x in urn_value.split(':')]
for Entitlement in Entitlements:
entArguments=Entitlement.split(':powerdns-admin')
entArguments=[x.lower() for x in entArguments[0].split(':')]
if (entArguments==urnArguments):
entArguments = Entitlement.split(':powerdns-admin')
entArguments = [x.lower() for x in entArguments[0].split(':')]
if (entArguments == urnArguments):
return True
return False
@ -524,9 +553,10 @@ def clear_session():
session.pop('user_id', None)
session.pop('github_token', None)
session.pop('google_token', None)
session.pop('azure_token', None)
session.pop('oidc_token', None)
session.pop('authentication_type', None)
session.pop('remote_user', None)
session.clear()
logout_user()
@ -552,14 +582,15 @@ def signin_history(username, authenticator, success):
# Write history
History(msg='User {} authentication {}'.format(username, str_success),
detail = json.dumps({
'username': username,
'authenticator': authenticator,
'ip_address': request_ip,
'success': 1 if success else 0
}),
detail=json.dumps({
'username': username,
'authenticator': authenticator,
'ip_address': request_ip,
'success': 1 if success else 0
}),
created_by='System').add()
# Get a list of Azure security groups the user is a member of
def get_azure_groups(uri):
azure_info = azure.get(uri).text
@ -575,30 +606,33 @@ def get_azure_groups(uri):
mygroups = []
return mygroups
# Handle user login, write history and, if set, handle showing the register_otp QR code.
# if Setting for OTP on first login is enabled, and OTP field is also enabled,
# but user isn't using it yet, enable OTP, get QR code and display it, logging the user out.
def authenticate_user(user, authenticator, remember=False):
login_user(user, remember=remember)
signin_history(user.username, authenticator, True)
if Setting().get('otp_force') and Setting().get('otp_field_enabled') and not user.otp_secret and session['authentication_type'] not in ['OAuth']:
if Setting().get('otp_force') and Setting().get('otp_field_enabled') and not user.otp_secret \
and session['authentication_type'] not in ['OAuth']:
user.update_profile(enable_otp=True)
user_id = current_user.id
prepare_welcome_user(user_id)
return redirect(url_for('index.welcome'))
return redirect(url_for('index.login'))
# Prepare user to enter /welcome screen, otherwise they won't have permission to do so
def prepare_welcome_user(user_id):
logout_user()
session['welcome_user_id'] = user_id
@index_bp.route('/logout')
def logout():
if current_app.config.get(
'SAML_ENABLED'
) and 'samlSessionIndex' in session and current_app.config.get(
'SAML_LOGOUT'):
) and 'samlSessionIndex' in session and current_app.config.get('SAML_LOGOUT'):
req = saml.prepare_flask_request(request)
auth = saml.init_saml_auth(req)
if current_app.config.get('SAML_LOGOUT_URL'):
@ -649,75 +683,168 @@ def logout():
return redirect(redirect_uri)
def password_policy_check(user, password):
def check_policy(chars, user_password, setting):
setting_as_int = int(Setting().get(setting))
test_string = user_password
for c in chars:
test_string = test_string.replace(c, '')
return (setting_as_int, len(user_password) - len(test_string))
def matches_policy(item, policy_fails):
return "*" if item in policy_fails else ""
policy = []
policy_fails = {}
# If either policy is enabled check basics first ... this is obvious!
if Setting().get('pwd_enforce_characters') or Setting().get('pwd_enforce_complexity'):
# Cannot contain username
if user.username in password:
policy_fails["username"] = True
policy.append(f"{matches_policy('username', policy_fails)}cannot contain username")
# Cannot contain password
if user.firstname in password:
policy_fails["firstname"] = True
policy.append(f"{matches_policy('firstname', policy_fails)}cannot contain firstname")
# Cannot contain lastname
if user.lastname in password:
policy_fails["lastname"] = True
policy.append(f"{matches_policy('lastname', policy_fails)}cannot contain lastname")
# Cannot contain email
if user.email in password:
policy_fails["email"] = True
policy.append(f"{matches_policy('email', policy_fails)}cannot contain email")
# Check if we're enforcing character requirements
if Setting().get('pwd_enforce_characters'):
# Length
pwd_min_len_setting = int(Setting().get('pwd_min_len'))
pwd_len = len(password)
if pwd_len < pwd_min_len_setting:
policy_fails["length"] = True
policy.append(f"{matches_policy('length', policy_fails)}length={pwd_len}/{pwd_min_len_setting}")
# Digits
(pwd_min_digits_setting, pwd_digits) = check_policy(string.digits, password, 'pwd_min_digits')
if pwd_digits < pwd_min_digits_setting:
policy_fails["digits"] = True
policy.append(f"{matches_policy('digits', policy_fails)}digits={pwd_digits}/{pwd_min_digits_setting}")
# Lowercase
(pwd_min_lowercase_setting, pwd_lowercase) = check_policy(string.digits, password, 'pwd_min_lowercase')
if pwd_lowercase < pwd_min_lowercase_setting:
policy_fails["lowercase"] = True
policy.append(
f"{matches_policy('lowercase', policy_fails)}lowercase={pwd_lowercase}/{pwd_min_lowercase_setting}")
# Uppercase
(pwd_min_uppercase_setting, pwd_uppercase) = check_policy(string.digits, password, 'pwd_min_uppercase')
if pwd_uppercase < pwd_min_uppercase_setting:
policy_fails["uppercase"] = True
policy.append(
f"{matches_policy('uppercase', policy_fails)}uppercase={pwd_uppercase}/{pwd_min_uppercase_setting}")
# Special
(pwd_min_special_setting, pwd_special) = check_policy(string.digits, password, 'pwd_min_special')
if pwd_special < pwd_min_special_setting:
policy_fails["special"] = True
policy.append(f"{matches_policy('special', policy_fails)}special={pwd_special}/{pwd_min_special_setting}")
if Setting().get('pwd_enforce_complexity'):
# Complexity checking
zxcvbn_inputs = []
for input in (user.firstname, user.lastname, user.username, user.email):
if len(input):
zxcvbn_inputs.append(input)
result = zxcvbn(password, user_inputs=zxcvbn_inputs)
pwd_min_complexity_setting = int(Setting().get('pwd_min_complexity'))
pwd_complexity = result['guesses_log10']
if pwd_complexity < pwd_min_complexity_setting:
policy_fails["complexity"] = True
policy.append(
f"{matches_policy('complexity', policy_fails)}complexity={pwd_complexity:.0f}/{pwd_min_complexity_setting}")
policy_str = {"password": f"Fails policy: {', '.join(policy)}. Items prefixed with '*' failed."}
# NK: the first item in the tuple indicates a PASS, so, we check for any True's and negate that
return (not any(policy_fails.values()), policy_str)
@index_bp.route('/register', methods=['GET', 'POST'])
def register():
CAPTCHA_ENABLE = current_app.config.get('CAPTCHA_ENABLE')
if Setting().get('signup_enabled'):
if current_user.is_authenticated:
return redirect(url_for('index.index'))
if request.method == 'GET':
return render_template('register.html', captcha_enable=CAPTCHA_ENABLE)
elif request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
firstname = request.form.get('firstname', '').strip()
lastname = request.form.get('lastname', '').strip()
email = request.form.get('email', '').strip()
rpassword = request.form.get('rpassword', '')
CAPTCHA_ENABLE = current_app.config.get('CAPTCHA_ENABLE')
if Setting().get('signup_enabled'):
if current_user.is_authenticated:
return redirect(url_for('index.index'))
if request.method == 'GET':
return render_template('register.html', captcha_enable=CAPTCHA_ENABLE)
elif request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
firstname = request.form.get('firstname', '').strip()
lastname = request.form.get('lastname', '').strip()
email = request.form.get('email', '').strip()
rpassword = request.form.get('rpassword', '')
is_valid_email = re.compile(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')
is_valid_email = re.compile(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')
error_messages = {}
if not firstname:
error_messages['firstname'] = 'First Name is required'
if not lastname:
error_messages['lastname'] = 'Last Name is required'
if not username:
error_messages['username'] = 'Username is required'
if not password:
error_messages['password'] = 'Password is required'
if not rpassword:
error_messages['rpassword'] = 'Password confirmation is required'
if not email:
error_messages['email'] = 'Email is required'
if not is_valid_email.match(email):
error_messages['email'] = 'Invalid email address'
if password != rpassword:
error_messages['password'] = 'Password confirmation does not match'
error_messages['rpassword'] = 'Password confirmation does not match'
error_messages = {}
if not firstname:
error_messages['firstname'] = 'First Name is required'
if not lastname:
error_messages['lastname'] = 'Last Name is required'
if not username:
error_messages['username'] = 'Username is required'
if not password:
error_messages['password'] = 'Password is required'
if not rpassword:
error_messages['rpassword'] = 'Password confirmation is required'
if not email:
error_messages['email'] = 'Email is required'
if not is_valid_email.match(email):
error_messages['email'] = 'Invalid email address'
if password != rpassword:
error_messages['password'] = 'Password confirmation does not match'
error_messages['rpassword'] = 'Password confirmation does not match'
if not captcha.validate():
return render_template(
'register.html', error='Invalid CAPTCHA answer', error_messages=error_messages, captcha_enable=CAPTCHA_ENABLE)
if not captcha.validate():
return render_template(
'register.html', error='Invalid CAPTCHA answer', error_messages=error_messages,
captcha_enable=CAPTCHA_ENABLE)
if error_messages:
return render_template('register.html', error_messages=error_messages, captcha_enable=CAPTCHA_ENABLE)
if error_messages:
return render_template('register.html', error_messages=error_messages, captcha_enable=CAPTCHA_ENABLE)
user = User(username=username,
plain_text_password=password,
firstname=firstname,
lastname=lastname,
email=email
)
user = User(username=username,
plain_text_password=password,
firstname=firstname,
lastname=lastname,
email=email
)
try:
result = user.create_local_user()
if result and result['status']:
if Setting().get('verify_user_email'):
send_account_verification(email)
if Setting().get('otp_force') and Setting().get('otp_field_enabled'):
user.update_profile(enable_otp=True)
prepare_welcome_user(user.id)
return redirect(url_for('index.welcome'))
else:
return redirect(url_for('index.login'))
(password_policy_pass, password_policy) = password_policy_check(user, password)
if not password_policy_pass:
return render_template('register.html', error_messages=password_policy, captcha_enable=CAPTCHA_ENABLE)
try:
result = user.create_local_user()
if result and result['status']:
if Setting().get('verify_user_email'):
send_account_verification(email)
if Setting().get('otp_force') and Setting().get('otp_field_enabled'):
user.update_profile(enable_otp=True)
prepare_welcome_user(user.id)
return redirect(url_for('index.welcome'))
else:
return redirect(url_for('index.login'))
else:
return render_template('register.html',
error=result['msg'], captcha_enable=CAPTCHA_ENABLE)
except Exception as e:
return render_template('register.html', error=e, captcha_enable=CAPTCHA_ENABLE)
else:
return render_template('register.html',
error=result['msg'], captcha_enable=CAPTCHA_ENABLE)
except Exception as e:
return render_template('register.html', error=e, captcha_enable=CAPTCHA_ENABLE)
else:
return render_template('errors/404.html'), 404
return render_template('errors/404.html'), 404
# Show welcome page on first login if otp_force is enabled
@ -736,12 +863,15 @@ def welcome():
if otp_token and otp_token.isdigit():
good_token = user.verify_totp(otp_token)
if not good_token:
return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user, error="Invalid token")
return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user,
error="Invalid token")
else:
return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user, error="Token required")
return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user,
error="Token required")
session.pop('welcome_user_id')
return redirect(url_for('index.index'))
@index_bp.route('/confirm/<token>', methods=['GET'])
def confirm_email(token):
email = confirm_token(token)
@ -828,10 +958,10 @@ def dyndns_update():
.outerjoin(Account, Domain.account_id == Account.id) \
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
.filter(
db.or_(
DomainUser.user_id == current_user.id,
AccountUser.user_id == current_user.id
)).all()
db.or_(
DomainUser.user_id == current_user.id,
AccountUser.user_id == current_user.id
)).all()
except Exception as e:
current_app.logger.error('DynDNS Error: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
@ -891,13 +1021,13 @@ def dyndns_update():
if result['status'] == 'ok':
history = History(
msg='DynDNS update: updated {} successfully'.format(hostname),
detail = json.dumps({
'domain': domain.name,
'record': hostname,
'type': rtype,
'old_value': oldip,
'new_value': str(ip)
}),
detail=json.dumps({
'domain': domain.name,
'record': hostname,
'type': rtype,
'old_value': oldip,
'new_value': str(ip)
}),
created_by=current_user.username,
domain_id=domain.id)
history.add()
@ -908,7 +1038,7 @@ def dyndns_update():
elif r.is_allowed_edit():
ondemand_creation = DomainSetting.query.filter(
DomainSetting.domain == domain).filter(
DomainSetting.setting == 'create_via_dyndns').first()
DomainSetting.setting == 'create_via_dyndns').first()
if (ondemand_creation is not None) and (strtobool(
ondemand_creation.value) == True):
@ -933,11 +1063,11 @@ def dyndns_update():
msg=
'DynDNS update: created record {0} in zone {1} successfully'
.format(hostname, domain.name, str(ip)),
detail = json.dumps({
'domain': domain.name,
'record': hostname,
'value': str(ip)
}),
detail=json.dumps({
'domain': domain.name,
'record': hostname,
'value': str(ip)
}),
created_by=current_user.username,
domain_id=domain.id)
history.add()
@ -997,7 +1127,7 @@ def saml_authorized():
req = saml.prepare_flask_request(request)
auth = saml.init_saml_auth(req)
auth.process_response()
current_app.logger.debug( auth.get_attributes() )
current_app.logger.debug(auth.get_attributes())
errors = auth.get_errors()
if len(errors) == 0:
session['samlUserdata'] = auth.get_attributes()
@ -1006,7 +1136,7 @@ def saml_authorized():
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']:
'RelayState']:
return redirect(auth.redirect_to(request.form['RelayState']))
if current_app.config.get('SAML_ATTRIBUTE_USERNAME', False):
username = session['samlUserdata'][
@ -1038,7 +1168,7 @@ def saml_authorized():
admin_group_name = current_app.config.get('SAML_GROUP_ADMIN_NAME',
None)
operator_group_name = current_app.config.get('SAML_GROUP_OPERATOR_NAME',
None)
None)
group_to_account_mapping = create_group_to_account_mapping()
if email_attribute_name in session['samlUserdata']:
@ -1079,13 +1209,13 @@ def saml_authorized():
account.add_user(user)
history = History(msg='Adding {0} to account {1}'.format(
user.username, account.name),
created_by='SAML Assertion')
created_by='SAML Assertion')
history.add()
for account in user_accounts - saml_accounts:
account.remove_user(user)
history = History(msg='Removing {0} from account {1}'.format(
user.username, account.name),
created_by='SAML Assertion')
created_by='SAML Assertion')
history.add()
if admin_attribute_name and 'true' in session['samlUserdata'].get(
admin_attribute_name, []):
@ -1099,7 +1229,7 @@ def saml_authorized():
user.role_id = Role.query.filter_by(name='User').first().id
history = History(msg='Demoting {0} to user'.format(
user.username),
created_by='SAML Assertion')
created_by='SAML Assertion')
history.add()
user.plain_text_password = None
user.update_profile()
@ -1143,15 +1273,16 @@ def uplift_to_admin(user):
user.role_id = Role.query.filter_by(name='Administrator').first().id
history = History(msg='Promoting {0} to administrator'.format(
user.username),
created_by='SAML Assertion')
created_by='SAML Assertion')
history.add()
def uplift_to_operator(user):
if user.role.name != 'Operator':
user.role_id = Role.query.filter_by(name='Operator').first().id
history = History(msg='Promoting {0} to operator'.format(
user.username),
created_by='SAML Assertion')
created_by='SAML Assertion')
history.add()

View File

@ -9,6 +9,8 @@ from flask_login import current_user, login_required, login_manager
from ..models.user import User, Anonymous
from ..models.setting import Setting
from .index import password_policy_check
user_bp = Blueprint('user',
__name__,
@ -79,12 +81,23 @@ def profile():
.format(current_user.username)
}), 400)
(password_policy_pass, password_policy) = password_policy_check(current_user.get_user_info_by_username(), new_password)
if not password_policy_pass:
if request.data:
return make_response(
jsonify({
'status': 'error',
'msg': password_policy['password'],
}), 400)
return render_template('user_profile.html', error_messages=password_policy)
user = User(username=current_user.username,
plain_text_password=new_password,
firstname=firstname,
lastname=lastname,
email=email,
reload_info=False)
user.update_profile()
return render_template('user_profile.html')

View File

@ -15,28 +15,40 @@ def azure_oauth():
session['azure_token'] = token
return token
authlib_params = {
'client_id': Setting().get('azure_oauth_key'),
'client_secret': Setting().get('azure_oauth_secret'),
'api_base_url': Setting().get('azure_oauth_api_url'),
'request_token_url': None,
'client_kwargs': {'scope': Setting().get('azure_oauth_scope')},
'fetch_token': fetch_azure_token,
}
server_metadata_url = Setting().get('azure_oauth_metadata_url')
if isinstance(server_metadata_url, str) and len(server_metadata_url.strip()) > 0:
authlib_params['server_metadata_url'] = server_metadata_url
else:
authlib_params['access_token_url'] = Setting().get('azure_oauth_token_url')
authlib_params['authorize_url'] = Setting().get('azure_oauth_authorize_url')
azure = authlib_oauth_client.register(
'azure',
client_id=Setting().get('azure_oauth_key'),
client_secret=Setting().get('azure_oauth_secret'),
api_base_url=Setting().get('azure_oauth_api_url'),
request_token_url=None,
access_token_url=Setting().get('azure_oauth_token_url'),
authorize_url=Setting().get('azure_oauth_authorize_url'),
client_kwargs={'scope': Setting().get('azure_oauth_scope')},
fetch_token=fetch_azure_token,
**authlib_params
)
@current_app.route('/azure/authorized')
def azure_authorized():
session['azure_oauthredir'] = url_for('.azure_authorized',
_external=True,
_scheme='https')
use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL')
params = {'_external': True}
if isinstance(use_ssl, bool):
params['_scheme'] = 'https' if use_ssl else 'http'
session['azure_oauthredir'] = url_for('.azure_authorized', **params)
token = azure.authorize_access_token()
if token is None:
return 'Access denied: reason=%s error=%s' % (
request.args['error'], request.args['error_description'])
session['azure_token'] = (token)
return redirect(url_for('index.login', _external=True, _scheme='https'))
return redirect(url_for('index.login', **params))
return azure

View File

@ -15,28 +15,42 @@ def github_oauth():
session['github_token'] = token
return token
authlib_params = {
'client_id': Setting().get('github_oauth_key'),
'client_secret': Setting().get('github_oauth_secret'),
'request_token_params': {'scope': Setting().get('github_oauth_scope')},
'api_base_url': Setting().get('github_oauth_api_url'),
'request_token_url': None,
'client_kwargs': {'scope': Setting().get('github_oauth_scope')},
'fetch_token': fetch_github_token,
'update_token': update_token
}
server_metadata_url = Setting().get('github_oauth_metadata_url')
if isinstance(server_metadata_url, str) and len(server_metadata_url.strip()) > 0:
authlib_params['server_metadata_url'] = server_metadata_url
else:
authlib_params['access_token_url'] = Setting().get('github_oauth_token_url')
authlib_params['authorize_url'] = Setting().get('github_oauth_authorize_url')
github = authlib_oauth_client.register(
'github',
client_id=Setting().get('github_oauth_key'),
client_secret=Setting().get('github_oauth_secret'),
request_token_params={'scope': Setting().get('github_oauth_scope')},
api_base_url=Setting().get('github_oauth_api_url'),
request_token_url=None,
access_token_url=Setting().get('github_oauth_token_url'),
authorize_url=Setting().get('github_oauth_authorize_url'),
client_kwargs={'scope': Setting().get('github_oauth_scope')},
fetch_token=fetch_github_token,
update_token=update_token)
**authlib_params
)
@current_app.route('/github/authorized')
def github_authorized():
session['github_oauthredir'] = url_for('.github_authorized',
_external=True)
use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL')
params = {'_external': True}
if isinstance(use_ssl, bool):
params['_scheme'] = 'https' if use_ssl else 'http'
session['github_oauthredir'] = url_for('.github_authorized', **params)
token = github.authorize_access_token()
if token is None:
return 'Access denied: reason=%s error=%s' % (
request.args['error'], request.args['error_description'])
session['github_token'] = (token)
return redirect(url_for('index.login'))
session['github_token'] = token
return redirect(url_for('index.login', **params))
return github

View File

@ -15,30 +15,43 @@ def google_oauth():
session['google_token'] = token
return token
authlib_params = {
'client_id': Setting().get('google_oauth_client_id'),
'client_secret': Setting().get('google_oauth_client_secret'),
'api_base_url': Setting().get('google_base_url'),
'request_token_url': None,
'client_kwargs': {'scope': Setting().get('google_oauth_scope')},
'fetch_token': fetch_google_token,
'update_token': update_token
}
server_metadata_url = Setting().get('google_oauth_metadata_url')
if isinstance(server_metadata_url, str) and len(server_metadata_url.strip()) > 0:
authlib_params['server_metadata_url'] = server_metadata_url
else:
authlib_params['access_token_url'] = Setting().get('google_token_url')
authlib_params['authorize_url'] = Setting().get('google_authorize_url')
google = authlib_oauth_client.register(
'google',
client_id=Setting().get('google_oauth_client_id'),
client_secret=Setting().get('google_oauth_client_secret'),
api_base_url=Setting().get('google_base_url'),
request_token_url=None,
access_token_url=Setting().get('google_token_url'),
authorize_url=Setting().get('google_authorize_url'),
client_kwargs={'scope': Setting().get('google_oauth_scope')},
fetch_token=fetch_google_token,
update_token=update_token)
**authlib_params
)
@current_app.route('/google/authorized')
def google_authorized():
session['google_oauthredir'] = url_for(
'.google_authorized', _external=True)
use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL')
params = {'_external': True}
if isinstance(use_ssl, bool):
params['_scheme'] = 'https' if use_ssl else 'http'
session['google_oauthredir'] = url_for('.google_authorized', **params)
token = google.authorize_access_token()
if token is None:
return 'Access denied: reason=%s error=%s' % (
request.args['error_reason'],
request.args['error_description']
)
session['google_token'] = (token)
return redirect(url_for('index.login'))
session['google_token'] = token
return redirect(url_for('index.login', **params))
return google

View File

@ -15,28 +15,41 @@ def oidc_oauth():
session['oidc_token'] = token
return token
authlib_params = {
'client_id': Setting().get('oidc_oauth_key'),
'client_secret': Setting().get('oidc_oauth_secret'),
'api_base_url': Setting().get('oidc_oauth_api_url'),
'request_token_url': None,
'client_kwargs': {'scope': Setting().get('oidc_oauth_scope')},
'fetch_token': fetch_oidc_token,
'update_token': update_token
}
server_metadata_url = Setting().get('oidc_oauth_metadata_url')
if isinstance(server_metadata_url, str) and len(server_metadata_url.strip()) > 0:
authlib_params['server_metadata_url'] = server_metadata_url
else:
authlib_params['access_token_url'] = Setting().get('oidc_oauth_token_url')
authlib_params['authorize_url'] = Setting().get('oidc_oauth_authorize_url')
oidc = authlib_oauth_client.register(
'oidc',
client_id=Setting().get('oidc_oauth_key'),
client_secret=Setting().get('oidc_oauth_secret'),
api_base_url=Setting().get('oidc_oauth_api_url'),
request_token_url=None,
access_token_url=Setting().get('oidc_oauth_token_url'),
authorize_url=Setting().get('oidc_oauth_authorize_url'),
server_metadata_url=Setting().get('oidc_oauth_metadata_url'),
client_kwargs={'scope': Setting().get('oidc_oauth_scope')},
fetch_token=fetch_oidc_token,
update_token=update_token)
**authlib_params
)
@current_app.route('/oidc/authorized')
def oidc_authorized():
session['oidc_oauthredir'] = url_for('.oidc_authorized',
_external=True)
use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL')
params = {'_external': True}
if isinstance(use_ssl, bool):
params['_scheme'] = 'https' if use_ssl else 'http'
session['oidc_oauthredir'] = url_for('.oidc_authorized', **params)
token = oidc.authorize_access_token()
if token is None:
return 'Access denied: reason=%s error=%s' % (
request.args['error'], request.args['error_description'])
session['oidc_token'] = (token)
return redirect(url_for('index.login'))
session['oidc_token'] = token
return redirect(url_for('index.login', **params))
return oidc
return oidc

View File

@ -0,0 +1,801 @@
let AuthenticationSettingsModel = function (user_data, api_url, csrf_token, selector) {
let self = this;
let target = null;
self.api_url = api_url;
self.csrf_token = csrf_token;
self.selector = selector;
self.loading = false;
self.saving = false;
self.saved = false;
self.save_failed = false;
self.messages = [];
self.messages_class = 'info';
self.tab_active = '';
self.tab_default = 'local';
let defaults = {
// Local Authentication Settings
local_db_enabled: true,
signup_enabled: true,
pwd_enforce_characters: 0,
pwd_min_len: 10,
pwd_min_lowercase: 3,
pwd_min_uppercase: 2,
pwd_min_digits: 2,
pwd_min_special: 1,
pwd_enforce_complexity: 0,
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: 0,
// Google OAuth2 Settings
google_oauth_enabled: false,
google_oauth_client_id: '',
google_oauth_client_secret: '',
google_oauth_scope: '',
google_base_url: '',
google_oauth_auto_configure: true,
google_oauth_metadata_url: '',
google_token_url: '',
google_authorize_url: '',
// GitHub OAuth2 Settings
github_oauth_enabled: false,
github_oauth_key: '',
github_oauth_secret: '',
github_oauth_scope: '',
github_oauth_api_url: '',
github_oauth_auto_configure: false,
github_oauth_metadata_url: '',
github_oauth_token_url: '',
github_oauth_authorize_url: '',
// Azure AD OAuth2 Settings
azure_oauth_enabled: false,
azure_oauth_key: '',
azure_oauth_secret: '',
azure_oauth_scope: '',
azure_oauth_api_url: '',
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: '',
azure_group_accounts_name_re: '',
azure_group_accounts_description: '',
azure_group_accounts_description_re: '',
// OIDC OAuth2 Settings
oidc_oauth_enabled: false,
oidc_oauth_key: '',
oidc_oauth_secret: '',
oidc_oauth_scope: '',
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: '',
oidc_oauth_email: '',
oidc_oauth_firstname: '',
oidc_oauth_last_name: '',
oidc_oauth_account_name_property: '',
oidc_oauth_account_description_property: '',
}
self.init = function (autoload) {
self.loading = ko.observable(self.loading);
self.saving = ko.observable(self.saving);
self.saved = ko.observable(self.saved);
self.save_failed = ko.observable(self.save_failed);
self.messages = ko.observableArray(self.messages);
self.messages_class = ko.observable(self.messages_class);
self.tab_active = ko.observable(self.tab_active);
self.tab_default = ko.observable(self.tab_default);
self.update(user_data);
let el = null;
if (typeof selector !== 'undefined') {
el = $(selector)
}
if (el !== null && el.length > 0) {
target = el;
ko.applyBindings(self, el[0]);
} else {
ko.applyBindings(self);
}
if (self.hasHash()) {
self.activateTab(self.getHash());
} else {
self.activateDefaultTab();
}
self.setupListeners();
self.setupValidation();
if (autoload) {
self.load();
}
}
self.load = function () {
self.loading(true);
$.ajax({
url: self.api_url,
type: 'POST',
data: {_csrf_token: csrf_token},
dataType: 'json',
success: self.onDataLoaded
});
}
self.save = function () {
if (!target.valid()) {
return false;
}
self.saving(true);
$.ajax({
url: self.api_url,
type: 'POST',
data: {_csrf_token: csrf_token, commit: 1, data: ko.toJSON(self)},
dataType: 'json',
success: self.onDataSaved
});
}
self.update = function (instance) {
for (const [key, value] of Object.entries($.extend(defaults, instance))) {
if (ko.isObservable(self[key])) {
self[key](value);
} else {
self[key] = ko.observable(value);
}
}
}
self.setupListeners = function () {
if ('onhashchange' in window) {
$(window).bind('hashchange', self.onHashChange);
}
}
self.destroyListeners = function () {
if ('onhashchange' in window) {
$(window).unbind('hashchange', self.onHashChange);
}
}
self.setupValidation = function () {
let uuidRegExp = /^([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})|[0-9]+$/i;
let footerErrorElements = [
'input#local_db_enabled',
];
let errorCheckSelectors = [
'input.error:not([disabled])',
'select.error:not([disabled])',
'textarea.error:not([disabled])',
];
let errorCheckQuery = errorCheckSelectors.join(',');
let tabs = target.find('.tab-content > *[data-tab]')
let onElementChanged = function (event) {
target.valid();
}
let auth_enabled = function (value, element, params) {
let enabled = 0;
if (self.local_db_enabled()) {
enabled++;
}
if (self.ldap_enabled()) {
enabled++;
}
if (self.google_oauth_enabled()) {
enabled++;
}
if (self.github_oauth_enabled()) {
enabled++;
}
if (self.azure_oauth_enabled()) {
enabled++;
}
if (self.oidc_oauth_enabled()) {
enabled++;
}
return enabled > 0;
};
let ldap_exclusive = function (value, element, params) {
let enabled = 0;
if (self.ldap_sg_enabled() === 1) {
enabled++;
}
if (self.autoprovisioning() === 1) {
enabled++;
}
return enabled < 2;
}
let uuid = function (value, element, params) {
return uuidRegExp.test(value);
}
let local_enabled = function (element) {
return self.local_db_enabled();
};
let ldap_enabled = function (element) {
return self.ldap_enabled();
};
let google_oauth_enabled = function (element) {
return self.google_oauth_enabled();
};
let github_oauth_enabled = function (element) {
return self.github_oauth_enabled();
};
let azure_oauth_enabled = function (element) {
return self.azure_oauth_enabled();
};
let oidc_oauth_enabled = function (element) {
return self.oidc_oauth_enabled();
};
let enforce_characters = function (element) {
return self.local_db_enabled() === 1 && self.pwd_enforce_characters() === 1;
};
let enforce_complexity = function (element) {
return self.local_db_enabled() === 1 && self.pwd_enforce_complexity() === 1;
};
let ldap_type_openldap = function (element) {
return self.ldap_enabled() && self.ldap_type() === 'ldap';
};
let ldap_type_ad = function (element) {
return self.ldap_enabled() && self.ldap_type() === 'ad';
};
let ldap_sg_enabled = function (element) {
return self.ldap_enabled() === 1 && self.ldap_sg_enabled() === 1;
}
let ldap_ap_enabled = function (element) {
return self.ldap_enabled() === 1 && self.autoprovisioning() === 1;
}
let azure_gs_enabled = function (element) {
return self.azure_oauth_enabled() === 1 && self.azure_sg_enabled() === 1;
}
let azure_gas_enabled = function (element) {
return self.azure_oauth_enabled() && self.azure_group_accounts_enabled();
}
let google_oauth_auto_configure_enabled = function (element) {
return self.google_oauth_enabled() && self.google_oauth_auto_configure();
}
let google_oauth_auto_configure_disabled = function (element) {
return self.google_oauth_enabled() && !self.google_oauth_auto_configure();
}
let github_oauth_auto_configure_enabled = function (element) {
return self.github_oauth_enabled() && self.github_oauth_auto_configure();
}
let github_oauth_auto_configure_disabled = function (element) {
return self.github_oauth_enabled() && !self.github_oauth_auto_configure();
}
let azure_oauth_auto_configure_enabled = function (element) {
return self.azure_oauth_enabled() && self.azure_oauth_auto_configure();
}
let azure_oauth_auto_configure_disabled = function (element) {
return self.azure_oauth_enabled() && !self.azure_oauth_auto_configure();
}
let oidc_oauth_auto_configure_enabled = function (element) {
return self.oidc_oauth_enabled() && self.oidc_oauth_auto_configure();
}
let oidc_oauth_auto_configure_disabled = function (element) {
return self.oidc_oauth_enabled() && !self.oidc_oauth_auto_configure();
}
jQuery.validator.addMethod('auth_enabled', auth_enabled, 'At least one authentication method must be enabled.');
jQuery.validator.addMethod('ldap_exclusive', ldap_exclusive, 'The LDAP group security and role auto-provisioning features are mutually exclusive.');
jQuery.validator.addMethod('uuid', uuid, 'A valid UUID is required.');
target.validate({
ignore: '',
errorPlacement: function (error, element) {
let useFooter = false;
for (let i = 0; i < footerErrorElements.length; i++) {
if (element.is(footerErrorElements[i])) {
useFooter = true;
}
}
if (useFooter) {
target.find('.card-footer > .error').append(error);
} else if (element.is('input[type=radio]')) {
error.insertAfter(element.parents('div.radio'));
} else {
element.after(error);
}
},
showErrors: function (errorMap, errorList) {
this.defaultShowErrors();
tabs.each(function (index, tab) {
tab = $(tab);
let tabId = tab.data('tab');
let tabLink = target.find('.nav-tabs > li > a[data-tab="' + tabId + '"]');
if (tab.find(errorCheckQuery).length > 0) {
tabLink.addClass('error');
} else {
tabLink.removeClass('error');
}
});
},
rules: {
local_db_enabled: 'auth_enabled',
ldap_enabled: 'auth_enabled',
google_oauth_enabled: 'auth_enabled',
github_oauth_enabled: 'auth_enabled',
azure_oauth_enabled: 'auth_enabled',
oidc_oauth_enabled: 'auth_enabled',
pwd_min_len: {
required: enforce_characters,
digits: true,
min: 1,
max: 64,
},
pwd_min_lowercase: {
required: enforce_characters,
digits: true,
min: 0,
max: 64,
},
pwd_min_uppercase: {
required: enforce_characters,
digits: true,
min: 0,
max: 64,
},
pwd_min_digits: {
required: enforce_characters,
digits: true,
min: 0,
max: 64,
},
pwd_min_special: {
required: enforce_characters,
digits: true,
min: 0,
max: 64,
},
pwd_min_complexity: {
required: enforce_complexity,
digits: true,
min: 1,
max: 1000,
},
ldap_type: ldap_enabled,
ldap_uri: {
required: ldap_enabled,
minlength: 11,
maxlength: 255,
},
ldap_base_dn: {
required: ldap_enabled,
minlength: 4,
maxlength: 255,
},
ldap_admin_username: {
required: ldap_type_openldap,
minlength: 4,
maxlength: 255,
},
ldap_admin_password: {
required: ldap_type_openldap,
minlength: 1,
maxlength: 255,
},
ldap_domain: {
required: ldap_type_ad,
minlength: 1,
maxlength: 255,
},
ldap_filter_basic: {
required: ldap_enabled,
minlength: 3,
maxlength: 1000,
},
ldap_filter_username: {
required: ldap_enabled,
minlength: 1,
maxlength: 100,
},
ldap_filter_group: {
required: ldap_type_openldap,
minlength: 3,
maxlength: 100,
},
ldap_filter_groupname: {
required: ldap_type_openldap,
minlength: 1,
maxlength: 100,
},
ldap_sg_enabled: {
required: ldap_enabled,
ldap_exclusive: true,
},
ldap_admin_group: {
required: ldap_sg_enabled,
minlength: 3,
maxlength: 100,
},
ldap_operator_group: {
required: ldap_sg_enabled,
minlength: 3,
maxlength: 100,
},
ldap_user_group: {
required: ldap_sg_enabled,
minlength: 3,
maxlength: 100,
},
autoprovisioning: {
required: ldap_enabled,
ldap_exclusive: true,
},
autoprovisioning_attribute: {
required: ldap_ap_enabled,
minlength: 1,
maxlength: 100,
},
urn_value: {
required: ldap_ap_enabled,
minlength: 1,
maxlength: 100,
},
purge: ldap_enabled,
google_oauth_client_id: {
required: google_oauth_enabled,
minlength: 1,
maxlength: 255,
},
google_oauth_client_secret: {
required: google_oauth_enabled,
minlength: 1,
maxlength: 255,
},
google_oauth_scope: {
required: google_oauth_enabled,
minlength: 1,
maxlength: 255,
},
google_base_url: {
required: google_oauth_enabled,
minlength: 1,
maxlength: 255,
url: true,
},
google_oauth_metadata_url: {
required: google_oauth_auto_configure_enabled,
minlength: 1,
maxlength: 255,
url: true,
},
google_token_url: {
required: google_oauth_auto_configure_disabled,
minlength: 1,
maxlength: 255,
url: true,
},
google_authorize_url: {
required: google_oauth_auto_configure_disabled,
minlength: 1,
maxlength: 255,
url: true,
},
github_oauth_key: {
required: github_oauth_enabled,
minlength: 1,
maxlength: 255,
},
github_oauth_secret: {
required: github_oauth_enabled,
minlength: 1,
maxlength: 255,
},
github_oauth_scope: {
required: github_oauth_enabled,
minlength: 1,
maxlength: 255,
},
github_oauth_api_url: {
required: github_oauth_enabled,
minlength: 1,
maxlength: 255,
url: true,
},
github_oauth_metadata_url: {
required: github_oauth_auto_configure_enabled,
minlength: 1,
maxlength: 255,
url: true,
},
github_oauth_token_url: {
required: github_oauth_auto_configure_disabled,
minlength: 1,
maxlength: 255,
url: true,
},
github_oauth_authorize_url: {
required: github_oauth_auto_configure_disabled,
minlength: 1,
maxlength: 255,
url: true,
},
azure_oauth_key: {
required: azure_oauth_enabled,
minlength: 1,
maxlength: 255,
uuid: true,
},
azure_oauth_secret: {
required: azure_oauth_enabled,
minlength: 1,
maxlength: 255,
},
azure_oauth_scope: {
required: azure_oauth_enabled,
minlength: 1,
maxlength: 255,
},
azure_oauth_api_url: {
required: azure_oauth_enabled,
minlength: 1,
maxlength: 255,
url: true,
},
azure_oauth_metadata_url: {
required: azure_oauth_auto_configure_enabled,
minlength: 1,
maxlength: 255,
url: true,
},
azure_oauth_token_url: {
required: azure_oauth_auto_configure_disabled,
minlength: 1,
maxlength: 255,
url: true,
},
azure_oauth_authorize_url: {
required: azure_oauth_auto_configure_disabled,
minlength: 1,
maxlength: 255,
url: true,
},
azure_sg_enabled: azure_oauth_enabled,
azure_admin_group: {
uuid: azure_gs_enabled,
},
azure_operator_group: {
uuid: azure_gs_enabled,
},
azure_user_group: {
uuid: azure_gs_enabled,
},
azure_group_accounts_enabled: azure_oauth_enabled,
azure_group_accounts_name: {
required: azure_gas_enabled,
minlength: 1,
maxlength: 255,
},
azure_group_accounts_name_re: {
required: azure_gas_enabled,
minlength: 1,
maxlength: 255,
},
azure_group_accounts_description: {
required: azure_gas_enabled,
minlength: 1,
maxlength: 255,
},
azure_group_accounts_description_re: {
required: azure_gas_enabled,
minlength: 1,
maxlength: 255,
},
oidc_oauth_key: {
required: oidc_oauth_enabled,
minlength: 1,
maxlength: 255,
},
oidc_oauth_secret: {
required: oidc_oauth_enabled,
minlength: 1,
maxlength: 255,
},
oidc_oauth_scope: {
required: oidc_oauth_enabled,
minlength: 1,
maxlength: 255,
},
oidc_oauth_api_url: {
required: oidc_oauth_enabled,
minlength: 1,
maxlength: 255,
url: true,
},
oidc_oauth_metadata_url: {
required: oidc_oauth_auto_configure_enabled,
minlength: 1,
maxlength: 255,
url: true,
},
oidc_oauth_token_url: {
required: oidc_oauth_auto_configure_disabled,
minlength: 1,
maxlength: 255,
url: true,
},
oidc_oauth_authorize_url: {
required: oidc_oauth_auto_configure_disabled,
minlength: 1,
maxlength: 255,
url: true,
},
oidc_oauth_logout_url: {
required: oidc_oauth_enabled,
minlength: 1,
maxlength: 255,
url: true,
},
oidc_oauth_username: {
required: oidc_oauth_enabled,
minlength: 1,
maxlength: 255,
},
oidc_oauth_email: {
required: oidc_oauth_enabled,
minlength: 1,
maxlength: 255,
},
oidc_oauth_firstname: {
minlength: 0,
maxlength: 255,
},
oidc_oauth_last_name: {
minlength: 0,
maxlength: 255,
},
oidc_oauth_account_name_property: {
minlength: 0,
maxlength: 255,
},
oidc_oauth_account_description_property: {
minlength: 0,
maxlength: 255,
},
},
messages: {
ldap_sg_enabled: {
ldap_exclusive: 'The LDAP group security feature is mutually exclusive with the LDAP role auto-provisioning feature.',
},
autoprovisioning: {
ldap_exclusive: 'The LDAP role auto-provisioning feature is mutually exclusive with the LDAP group security feature.',
},
},
});
target.find('input, select, textarea, label').on('change,keyup,blur,click', onElementChanged);
target.valid();
}
self.activateTab = function (tab) {
$('[role="tablist"] a.nav-link').blur();
self.tab_active(tab);
window.location.hash = tab;
}
self.activateDefaultTab = function () {
self.activateTab(self.tab_default());
}
self.getHash = function () {
return window.location.hash.substring(1);
}
self.hasHash = function () {
return window.location.hash.length > 1;
}
self.onDataLoaded = function (result) {
if (result.status == 0) {
self.messages_class('danger');
self.messages(result.messages);
self.loading(false);
return false;
}
self.update(result.data);
self.messages_class('info');
self.messages(result.messages);
self.loading(false);
}
self.onDataSaved = function (result) {
if (result.status == 0) {
self.saved(false);
self.save_failed(true);
self.messages_class('danger');
self.messages(result.messages);
self.saving(false);
return false;
}
self.update(result.data);
self.saved(true);
self.save_failed(false);
self.messages_class('info');
self.messages(result.messages);
self.saving(false);
}
self.onHashChange = function (event) {
let hash = window.location.hash.trim();
if (hash.length > 1) {
self.activateTab(hash.substring(1));
} else {
self.activateDefaultTab();
}
}
self.onSaveClick = function (model, event) {
self.save();
return false;
}
self.onTabClick = function (model, event) {
self.activateTab($(event.target).data('tab'));
return false;
}
}

View File

@ -96,7 +96,7 @@
</div>
<!-- /.card-header -->
<div class="card-body">
<p>Users on the right have access to manage records in all domains
<p>Users on the right have access to manage records in all zones
associated with the account.
</p>
<p>Click on users to move between columns.</p>
@ -113,12 +113,12 @@
</div>
<!-- /.card-body -->
<div class="card-body">
<p>Domains on the right are associated with the account. Red marked domain names are
<p>Zones on the right are associated with the account. Red marked zone names are
already associated with other accounts.
Moving already associated domains to this account will overwrite the previous
Moving already associated zones to this account will overwrite the previous
associated account.
</p>
<p>Hover over the red domain names to show the associated account. Click on domains to
<p>Hover over the red zone names to show the associated account. Click on zones to
move between columns.</p>
<div class="form-group col-2">
<select multiple="multiple" class="form-control" id="account_domains"
@ -168,12 +168,12 @@
<!-- /.card-header -->
<div class="card-body">
<p>
An account allows grouping of domains belonging to a particular entity, such as a
An account allows grouping of zones belonging to a particular entity, such as a
customer or
department.
</p>
<p>
A domain can be assigned to an account upon domain creation or through the domain
A zone can be assigned to an account upon zone creation or through the zone
administration
page.
</p>
@ -242,6 +242,6 @@
}
addMultiSelect("#account_multi_user", "Username")
addMultiSelect("#account_domains", "Domain")
addMultiSelect("#account_domains", "Zone")
</script>
{% endblock %}

View File

@ -72,7 +72,7 @@
<!-- /.card-header -->
<div class="card-body key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
<p>This key will be linked to the accounts on the right,</p>
<p>thus granting access to domains owned by the selected accounts.</p>
<p>thus granting access to zones owned by the selected accounts.</p>
<p>Click on accounts to move between the columns.</p>
<div class="form-group col-2">
<select multiple="multiple" class="form-control" id="key_multi_account"
@ -87,12 +87,12 @@
</div>
<!-- /.card-body -->
<div class="card-header key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
<h3 class="card-title">Domain Access Control</h3>
<h3 class="card-title">Zone Access Control</h3>
</div>
<!-- /.card-header -->
<div class="card-body key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
<p>This key will have access to the domains on the right.</p>
<p>Click on domains to move between the columns.</p>
<p>This key will have access to the zones on the right.</p>
<p>Click on zones to move between the columns.</p>
<div class="form-group col-2">
<select multiple="multiple" class="form-control" id="key_multi_domain"
name="key_multi_domain">
@ -131,7 +131,7 @@
<p>Fill in all the fields in the form to the left.</p>
<p><strong>Role</strong> The role of the key.</p>
<p><strong>Description</strong> The key description.</p>
<p><strong>Access Control</strong> The domains or accounts which the key has access to.</p>
<p><strong>Access Control</strong> The zones or accounts which the key has access to.</p>
</div>
<!-- /.card-body -->
</div>
@ -154,13 +154,13 @@
var warn_modal = $("#modal_warning");
if (selectedRole != "User" && selectedDomains > 0 && selectedAccounts > 0) {
var warning = "Administrator and Operators have access to all domains. Your domain an account selection won't be saved.";
var warning = "Administrator and Operators have access to all zones. Your zone an account selection won't be saved.";
e.preventDefault(e);
warn_modal.modal('show');
}
if (selectedRole == "User" && selectedDomains == 0 && selectedAccounts == 0) {
var warning = "User role must have at least one account or one domain bound. None selected.";
var warning = "User role must have at least one account or one zone bound. None selected.";
e.preventDefault(e);
warn_modal.modal('show');
}
@ -203,8 +203,8 @@
}
});
$("#key_multi_domain").multiSelect({
selectableHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Domain Name'>",
selectionHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Domain Name'>",
selectableHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Zone Name'>",
selectionHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Zone Name'>",
afterInit: function (ms) {
var that = this,
$selectableSearch = that.$selectableUl.prev(),

View File

@ -135,9 +135,9 @@
<div class="card-body">
<p>Fill in all the fields to the in the form to the left.</p>
{% if create %}
<p><strong>Newly created users do not have access to any domains.</strong> You will need
<p><strong>Newly created users do not have access to any zones.</strong> You will need
to grant
access to the user once it is created via the domain management buttons on the
access to the user once it is created via the zone management buttons on the
dashboard.
</p>
{% else %}

View File

@ -38,7 +38,7 @@
<!-- /.card-header -->
<div class="card-body">
<div class="callout callout-info">
<p>This tool can be used to search for domains, records, and comments via the PDNS
<p>This tool can be used to search for zones, records, and comments via the PDNS
API.</p>
</div>
<!-- /.callout -->

View File

@ -39,8 +39,8 @@
</div>
{% endif %}
</div>
<div class="card-body clearfix">
<form id="history-search-form" autocomplete="off">
<form id="history-search-form" autocomplete="off">
<div class="card-body clearfix">
<ul class="nav nav-tabs" id="custom-content-below-tab" role="tablist">
<li class="nav-item">
<a class="nav-link active" href="#tabs-act" data-toggle="pill" role="tab">
@ -49,7 +49,7 @@
</li>
<li class="nav-item">
<a class="nav-link" href="#tabs-domain" data-toggle="pill" role="tab">
Search By Domain
Search By Zone
</a>
</li>
<li class="nav-item">
@ -71,13 +71,13 @@
</div>
<div class="tab-pane" id="tabs-domain">
<td>
<label>Domain Name</label>
<label>Zone Name</label>
</td>
<td>
<div class="autocomplete" style="width:250px;">
<input type="text" class="form-control" id="domain_name_filter"
name="domain_name_filter"
placeholder="Enter * to search for any domain" value="">
placeholder="Enter * to search for any zone" value="">
</div>
</td>
<td>
@ -144,65 +144,64 @@
</td>
</div>
</td>
</form>
</div>
</div>
</div>
<div class="card-body">
<table id="Filters-Table">
<thead>
<th>Filters</th>
</thead>
<tbody>
<tr>
<td><label>Changed by: &nbsp</label></td>
<td>
<div class="autocomplete" style="width:250px;">
<input type="text" style=" border:1px solid #d2d6de; width:250px; height: 34px;"
id="user_name_filter" name="user_name_filter" value="">
</div>
</td>
</tr>
<tr>
<td style="position: relative; top:10px;">
<label>Minimum date: &nbsp</label>
</td>
<td style="position: relative; top:10px;">
<input type="text" id="min" name="min" class="datepicker" autocomplete="off"
style=" border:1px solid #d2d6de; width:250px; height: 34px;">
</td>
</tr>
<tr>
<td style="position: relative; top:20px;">
<label>Maximum date: &nbsp</label>
</td>
<td style="position: relative; top:20px;">
<input type="text" id="max" name="max" class="datepicker" autocomplete="off"
style=" border:1px solid #d2d6de; width:250px; height: 34px;">
</td>
</tr>
<tr>
<td>&nbsp</td>
</tr>
<tr>
<td>&nbsp</td>
</tr>
<tr>
<td>
<button type="submit" id="search-submit" name="search-submit"
class="btn btn-primary button-filter"><i class="fa fa-search"></i>&nbsp;Search
</button>
</td>
<td>
<button id="clear-filters" name="clear-filters"
class="btn btn-warning button-clearf"><i class="fa fa-trash"></i>&nbsp;Clear
Filters
</button>
</td>
</tr>
</tbody>
</table>
</form>
</div>
<div class="card-body">
<table id="Filters-Table">
<thead>
<th>Filters</th>
</thead>
<tbody>
<tr>
<td><label>Changed by: &nbsp</label></td>
<td>
<div class="autocomplete" style="width:250px;">
<input type="text" style=" border:1px solid #d2d6de; width:250px; height: 34px;"
id="user_name_filter" name="user_name_filter" value="">
</div>
</td>
</tr>
<tr>
<td style="position: relative; top:10px;">
<label>Minimum date: &nbsp</label>
</td>
<td style="position: relative; top:10px;">
<input type="text" id="min" name="min" class="datepicker" autocomplete="off"
style=" border:1px solid #d2d6de; width:250px; height: 34px;">
</td>
</tr>
<tr>
<td style="position: relative; top:20px;">
<label>Maximum date: &nbsp</label>
</td>
<td style="position: relative; top:20px;">
<input type="text" id="max" name="max" class="datepicker" autocomplete="off"
style=" border:1px solid #d2d6de; width:250px; height: 34px;">
</td>
</tr>
<tr>
<td>&nbsp</td>
</tr>
<tr>
<td>&nbsp</td>
</tr>
<tr>
<td>
<button type="submit" id="search-submit" name="search-submit"
class="btn btn-primary button-filter"><i class="fa fa-search"></i>&nbsp;Search
</button>
</td>
<td>
<button id="clear-filters" name="clear-filters"
class="btn btn-warning button-clearf"><i class="fa fa-trash"></i>&nbsp;Clear
Filters
</button>
</td>
</tr>
</tbody>
</table>
</div>
</form>
<div id="table_from_ajax"></div>
</div>
</section>
@ -447,7 +446,7 @@
$('#auth_name_filter').val('');
$('#user_name_filter').removeAttr('disabled');
canSearch = false;
main_field = "Domain Name"
main_field = "Zone Name"
});
$('#account_tab').click(function () {

View File

@ -48,7 +48,7 @@
<th>Contact</th>
<th>Mail</th>
<th>Member</th>
<th>Domain</th>
<th>Zone(s)</th>
<th>Action</th>
</tr>
</thead>

View File

@ -43,7 +43,7 @@
<th>Id</th>
<th>Role</th>
<th>Description</th>
<th>Domains</th>
<th>Zones</th>
<th>Accounts</th>
<th>Actions</th>
</tr>

View File

@ -143,7 +143,7 @@
var modal = $("#modal_revoke");
var username = $(this).prop('id');
var info = "Are you sure you want to revoke all privileges for user " + username +
"? They will not able to access any domain.";
"? They will not able to access any zone.";
modal.find('.modal-body p').text(info);
modal.find('#button_revoke_confirm').click(function () {
var postdata = {

File diff suppressed because it is too large Load Diff

View File

@ -101,14 +101,22 @@
</a>
</li>
{% endif %}
{% if current_user.role.name in ['Administrator', 'Operator'] %}
<li class="nav-header">Administration</li>
<li class="{{ 'nav-item active' if active_page == 'admin_global_search' else 'nav-item' }}">
<a href="{{ url_for('admin.global_search') }}" class="nav-link">
<i class="nav-icon fa-solid fa-search"></i>
<p>Global Search</p>
<li class="nav-header">Administration</li>
<li class="{{ 'nav-item active' if active_page == 'admin_global_search' else 'nav-item' }}">
<a href="{{ url_for('admin.global_search') }}" class="nav-link">
<i class="nav-icon fa-solid fa-search"></i>
<p>Global Search</p>
</a>
</li>
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
<li class="{{ 'nav-item active' if active_page == 'admin_history' else 'nav-item' }}">
<a href="{{ url_for('admin.history') }}" class="nav-link">
<i class="nav-icon fa-solid fa-timeline"></i>
<p>Activity</p>
</a>
</li>
{% endif %}
{% if current_user.role.name in ['Administrator', 'Operator'] %}
<li class="{{ 'nav-item active' if active_page == 'server_statistics' else 'nav-item' }}">
<a href="{{ url_for('admin.server_statistics') }}" class="nav-link">
<i class="nav-icon fa-solid fa-chart-simple"></i>
@ -121,12 +129,6 @@
<p>Server Configuration</p>
</a>
</li>
<li class="{{ 'nav-item active' if active_page == 'admin_history' else 'nav-item' }}">
<a href="{{ url_for('admin.history') }}" class="nav-link">
<i class="nav-icon fa-solid fa-timeline"></i>
<p>Activity</p>
</a>
</li>
<li class="{{ 'nav-item active' if active_page == 'admin_domain_template' else 'nav-item' }}">
<a href="{{ url_for('admin.templates') }}" class="nav-link">
<i class="nav-icon fa-solid fa-clone"></i>
@ -189,14 +191,6 @@
{% endif %}
</ul>
</li>
{% elif SETTING.get('allow_user_view_history') %}
<li class="nav-header">Administration</li>
<li class="{{ 'nav-item active' if active_page == 'admin_history' else 'nav-item' }}">
<a href="{{ url_for('admin.history') }}" class="nav-link">
<i class="nav-icon fa-solid fa-calendar-alt"></i>
<p>History</p>
</a>
</li>
{% endif %}
</ul>
{% endif %}
@ -234,7 +228,7 @@
<footer class="main-footer">
<strong><a href="https://github.com/PowerDNS-Admin/PowerDNS-Admin">PowerDNS-Admin</a></strong> - A PowerDNS web
interface with advanced features.
<span class="float-right">Version 0.4.0</span>
<span class="float-right">Version 0.4.1</span>
</footer>
</div>
<!-- ./wrapper -->

View File

@ -269,7 +269,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Sync Domains from backend</h4>
<h4 class="modal-title">Sync Zones from backend</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>

View File

@ -76,11 +76,16 @@
<div class="radio">
<label>
<input type="radio" name="radio_type" id="radio_type_secondary"
value="secondary">
value="slave">
Secondary
</label>
</div>
</div>
<div class="form-group" style="display: none;" id="domain_master_address_div">
<input type="text" class="form-control" name="domain_master_address"
id="domain_master_address"
placeholder="Enter valid Primary Server IP addresses (separated by commas)">
</div>
<div class="form-group">
<label for="domain_template">Zone Template</label>
<select class="form-control" id="domain_template" name="domain_template">
@ -90,11 +95,6 @@
{% endfor %}
</select>
</div>
<div class="form-group" style="display: none;" id="domain_primary_address_div">
<input type="text" class="form-control" name="domain_primary_address"
id="domain_primary_address"
placeholder="Enter valid Primary Server IP addresses (separated by commas)">
</div>
<div class="form-group">
<label>SOA-EDIT-API</label>
<div class="radio">
@ -162,7 +162,7 @@
<dt>Account</dt>
<dd>Specifies the PowerDNS account value to use for the zone.</dd>
<dt>Zone Type</dt>
<dd>The type decides how the domain will be replicated across multiple DNS servers.
<dd>The type decides how the zone will be replicated across multiple DNS servers.
<ul>
<li>
<strong>Native</strong> - The server will not perform any Primary or Secondary
@ -186,7 +186,7 @@
<dt>SOA-EDIT-API</dt>
<dd>The SOA-EDIT-API setting defines how the SOA serial number will be updated after a
change is
made to the domain.
made to the zone.
<ul>
<li>
<strong>DEFAULT</strong> - Generate a soa serial of YYYYMMDD01. If the current serial
@ -228,10 +228,10 @@
<script>
$("input[name=radio_type]").change(function () {
var type = $(this).val();
if (type == "secondary") {
$("#domain_primary_address_div").show();
if (type == "slave") {
$("#domain_master_address_div").show();
} else {
$("#domain_primary_address_div").hide();
$("#domain_master_address_div").hide();
}
});
</script>
@ -273,4 +273,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -15,7 +15,7 @@
{% if record_name and record_type %}
Record changelog: <b>{{ record_name}} &nbsp {{ record_type }}</b>
{% else %}
Domain changelog: <b>{{ domain.name | pretty_domain_name }}</b>
Zone changelog: <b>{{ domain.name | pretty_domain_name }}</b>
{% endif %}
</h1>
</div>
@ -41,7 +41,7 @@
<div class="card-body">
<button type="button" class="btn btn-primary float-left button_show_records" id="{{ domain.name }}">
<i class="fa-solid fa-arrow-left"></i>
&nbsp;Manage Domain {{ domain.name }}
&nbsp;Manage Zone {{ domain.name }}
</button>
</div>
<div class="card-body">
@ -125,4 +125,4 @@
}
</script>
{% endblock %}
{% endblock %}

View File

@ -37,7 +37,7 @@
<div class="form-group">
<label for="domainid">Zone</label>
<select id=domainid class="form-control" style="width:15em;">
<option value="0">- Select Domain -</option>
<option value="0">- Select Zone -</option>
{% for domain in domainss %}
<option value="{{ domain.id }}">{{ domain.name }}</option>
{% endfor %}
@ -83,7 +83,7 @@
$(document.body).on("click", ".button_delete", function (e) {
e.stopPropagation();
if ($("#domainid").val() == 0) {
showErrorModal("Please select domain to remove.");
showErrorModal("Please select zone to remove.");
return;
}

View File

@ -148,11 +148,11 @@
<div class="row">
<div class="col-12">
<p>Users on the right have access to manage the records in
the {{ domain.name | pretty_domain_name }} domain.</p>
the {{ domain.name | pretty_domain_name }} zone.</p>
<p>Click on users to move from between columns.</p>
<p>
Users in <font style="color: red;">red</font> are Administrators
and already have access to <b>ALL</b> domains.
and already have access to <b>ALL</b> zones.
</p>
</div>
</div>
@ -197,7 +197,7 @@
</div>
<!-- /.card-header -->
<div class="card-body">
<p>The type decides how the domain will be replicated across multiple DNS servers.</p>
<p>The type decides how the zone will be replicated across multiple DNS servers.</p>
<ul>
<li>
Native - PowerDNS will not perform any replication. Use this if you only have one
@ -214,19 +214,20 @@
zone transfers (AXFRs) from other servers configured as primaries.
</li>
</ul>
<b>New Domain Type Setting:</b>
<b>New Zone Type Setting:</b>
<form method="post" action="{{ url_for('domain.change_type', domain_name=domain.name) }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<select name="domain_type" class="form-control" style="width:15em;">
<option selected value="0">- Unchanged -</option>
<option value="native">Native</option>
<option value="primary">Primary</option>
<option value="secondary">Secondary</option>
<option value="0">- Unchanged -</option>
{% for type in ["native", "master", "slave"] %}
<option {% if zone_type == type %}selected{% endif %} value="{{ type }}">{{ type | format_zone_type }}</option>
{% endfor %}
</select><br/>
<div class="form-group" style="display: none;" id="domain_primary_address_div">
<input type="text" class="form-control" name="domain_primary_address"
id="domain_primary_address"
placeholder="Enter valid Primary Server IP addresses (separated by commas)">
<div class="form-group" style="{% if zone_type != 'slave' %}display: none;{% endif %}" id="domain_master_address_div">
<input type="text" class="form-control" name="domain_master_address"
id="domain_master_address"
placeholder="Enter valid Primary Server IP addresses (separated by commas)"
value="{{ masters }}">
</div>
<button type="submit" title="Update Zone Type" class="btn btn-primary" id="change_type">
<i class="fa-solid fa-floppy-disk"></i>&nbsp;Update Zone Type
@ -251,7 +252,7 @@
<div class="card-body">
<p>The SOA-EDIT-API setting defines how the SOA serial number will be updated after a change
is made
to the domain.</p>
to the zone.</p>
<ul>
<li>
DEFAULT - Generate a soa serial of YYYYMMDD01. If the current serial is lower than
@ -276,10 +277,9 @@
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<select name="soa_edit_api" class="form-control" style="width:15em;">
<option selected value="0">- Unchanged -</option>
<option>DEFAULT</option>
<option>INCREASE</option>
<option>EPOCH</option>
<option>OFF</option>
{% for edit_type in ["DEFAULT", "INCREASE", "EPOCH", "OFF"] %}
<option {% if soa_edit_api == edit_type %}selected{% endif %}>{{ edit_type }}</option>
{% endfor %}
</select><br/>
<button type="submit" title="Update SOA-EDIT-API" class="btn btn-primary"
id="change_soa_edit_api">
@ -303,9 +303,9 @@
</div>
<!-- /.card-header -->
<div class="card-body">
<p>This function is used to remove a domain from PowerDNS-Admin <b>AND</b> PowerDNS. All
<p>This function is used to remove a zone from PowerDNS-Admin <b>AND</b> PowerDNS. All
records and
user privileges associated with this domain will also be removed. This change cannot be
user privileges associated with this zone will also be removed. This change cannot be
reverted.</p>
<button type="button" title="Delete Zone" class="btn btn-danger float-left delete_domain"
id="{{ domain.name }}">
@ -402,7 +402,7 @@
applyChanges(postdata, $SCRIPT_ROOT + '/domain/' + domain + '/manage-setting', true);
});
// handle deletion of domain
// handle deletion of zone
$(document.body).on('click', '.delete_domain', function () {
var modal = $("#modal_delete_domain");
var domain = $(this).prop('id');
@ -419,13 +419,13 @@
modal.modal('show');
});
// domain primary address input handeling
// zone primary address input handeling
$("select[name=domain_type]").change(function () {
var type = $(this).val();
if (type == "secondary") {
$("#domain_primary_address_div").show();
if (type == "slave") {
$("#domain_master_address_div").show();
} else {
$("#domain_primary_address_div").hide();
$("#domain_master_address_div").hide();
}
});

View File

@ -34,13 +34,13 @@
<div class="nav-tabs-custom mb-2">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" href="#tabs-personal" data-toggle="tab">
<a class="nav-link {{ 'active' if not error_messages else '' }}" href="#tabs-personal" data-toggle="tab">
Personal Info
</a>
</li>
{% if session['authentication_type'] == 'LOCAL' %}
<li class="nav-item">
<a class="nav-link" href="#tabs-password" data-toggle="tab">
<a class="nav-link {{ 'active' if 'password' in error_messages else '' }}" href="#tabs-password" data-toggle="tab">
Change Password
</a>
</li>
@ -57,7 +57,8 @@
<!-- /.nav-tabs-custom -->
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-personal">
<div class="tab-pane fade {{ 'show active' if not error_messages else '' }}"
id="tabs-personal">
<form role="form" method="post" action="{{ user_profile }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="form-group">
@ -91,7 +92,8 @@
<!-- /.tab-pane -->
{% if session['authentication_type'] == 'LOCAL' %}
<div class="tab-pane fade" id="tabs-password">
<div class="tab-pane fade {{ 'show active' if 'password' in error_messages else '' }}"
id="tabs-password">
{% if not current_user.password %}
Your account password is managed via LDAP which isn't supported to
change here.
@ -101,8 +103,15 @@
value="{{ csrf_token() }}">
<div class="form-group">
<label for="password">New Password</label>
<input type="password" class="form-control" name="password"
<input type="password" class="form-control {{ 'is-invalid' if 'password' in error_messages else '' }}"
name="password"
id="newpassword">
{% if 'password' in error_messages %}
<div class="invalid-feedback">
<i class="fas fa-exclamation-triangle"></i>
{{ error_messages['password'] }}
</div>
{% endif %}
</div>
<div class="form-group">
<label for="rpassword">Re-type New Password</label>

View File

@ -17,7 +17,7 @@ bravado-core==5.17.1
certifi==2022.12.7
cffi==1.15.1
configobj==5.0.8
cryptography==36.0.2
cryptography==39.0.2 # fixes CVE-2023-0286, CVE-2023-23931
cssmin==0.2.0
dnspython>=2.3.0
flask_session_captcha==1.3.0
@ -25,7 +25,7 @@ gunicorn==20.1.0
itsdangerous==2.1.2
jsonschema[format]>=2.5.1,<4.0.0 # until https://github.com/Yelp/bravado-core/pull/385
lima==0.5
lxml==4.6.5
--use-feature=no-binary-enable-wheel-cache lxml==4.9.0
mysqlclient==2.0.1
passlib==1.7.4
#pyOpenSSL==22.1.0
@ -33,7 +33,7 @@ pyasn1==0.4.8
pyotp==2.8.0
pytest==7.2.1
python-ldap==3.4.3
python3-saml==1.14.0
python3-saml==1.15.0
pytimeparse==1.1.8
pytz==2022.7.1
qrcode==7.3.1
@ -43,3 +43,6 @@ webcolors==1.12
werkzeug==2.1.2
zipp==3.11.0
rcssmin==1.1.1
zxcvbn==4.4.28
psycopg2==2.9.5
setuptools==65.5.1 # fixes CVE-2022-40897

View File

@ -29,6 +29,6 @@ with app.app_context():
sys.exit(1)
### Start the update process
app.logger.info('Update domains from nameserver API')
app.logger.info('Update zones from nameserver API')
Domain().update()

View File

@ -42,7 +42,7 @@
resolved "https://registry.yarnpkg.com/@foliojs-fork/restructure/-/restructure-2.0.2.tgz#73759aba2aff1da87b7c4554e6839c70d43c92b4"
integrity sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==
"@fortawesome/fontawesome-free@6.3.0":
"@fortawesome/fontawesome-free@6.3.0", "@fortawesome/fontawesome-free@^5.15.4":
version "6.3.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.3.0.tgz#b5877182692a6f7a39d1108837bec24247ba4bd7"
integrity sha512-qVtd5i1Cc7cdrqnTWqTObKQHjPWAiRwjUPaXObaeNPcy7+WKxJumGBx66rfSFgK6LNpIasVKkEgW8oyf0tmPLA==
@ -1026,7 +1026,7 @@ jquery-ui-dist@^1.13.0, jquery-ui-dist@^1.13.2:
dependencies:
jquery ">=1.8.0 <4.0.0"
jquery-validation@^1.19.3:
jquery-validation@^1.19.3, jquery-validation@^1.19.5:
version "1.19.5"
resolved "https://registry.yarnpkg.com/jquery-validation/-/jquery-validation-1.19.5.tgz#557495b7cad79716897057c4447ad3cd76fda811"
integrity sha512-X2SmnPq1mRiDecVYL8edWx+yTBZDyC8ohWXFhXdtqFHgU9Wd4KHkvcbCoIZ0JaSaumzS8s2gXSkP8F7ivg/8ZQ==
@ -1081,6 +1081,11 @@ jtimeout@^3.2.0:
dependencies:
jquery ">=1.7.1 <4.0.0"
knockout@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/knockout/-/knockout-3.5.1.tgz#62c81e81843bea2008fd23c575edd9ca978e75cf"
integrity sha512-wRJ9I4az0QcsH7A4v4l0enUpkS++MBx0BnL/68KaLzJg7x1qmbjSlwEoCNol7KTYZ+pmtI7Eh2J0Nu6/2Z5J/Q==
levn@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"