mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2024-11-14 01:20:27 +00:00
Merge branch 'release/0.4.1'
This commit is contained in:
commit
a8895ffe7a
2
.github/workflows/mega-linter.yml
vendored
2
.github/workflows/mega-linter.yml
vendored
@ -4,8 +4,10 @@
|
||||
name: MegaLinter
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches-ignore:
|
||||
- "*"
|
||||
- "dev"
|
||||
- "main"
|
||||
- "master"
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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:
|
||||
|
@ -11,6 +11,7 @@ RUN apt-get update -y \
|
||||
libffi-dev \
|
||||
libldap2-dev \
|
||||
libmariadb-dev-compat \
|
||||
libpq-dev \
|
||||
libsasl2-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
|
@ -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 \
|
||||
|
@ -38,7 +38,7 @@
|
||||
|
||||
## Using PowerDNS-Admin
|
||||
|
||||
- Setting up a domain
|
||||
- Setting up a zone
|
||||
- Adding a record
|
||||
- <whatever else>
|
||||
|
||||
|
@ -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**
|
||||
|
@ -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.
|
||||
|
17
docs/wiki/configuration/basic_settings.md
Normal file
17
docs/wiki/configuration/basic_settings.md
Normal 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.
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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": {
|
||||
|
@ -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'),
|
||||
|
@ -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))
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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'}
|
||||
|
@ -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'
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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() +
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 = {
|
||||
|
@ -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({
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 %}
|
||||
|
@ -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(),
|
||||
|
@ -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 %}
|
||||
|
@ -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 -->
|
||||
|
@ -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:  </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:  </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:  </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> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </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> Search
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<button id="clear-filters" name="clear-filters"
|
||||
class="btn btn-warning button-clearf"><i class="fa fa-trash"></i> 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:  </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:  </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:  </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> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </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> Search
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<button id="clear-filters" name="clear-filters"
|
||||
class="btn btn-warning button-clearf"><i class="fa fa-trash"></i> 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 () {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
@ -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 -->
|
||||
|
@ -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">×</span>
|
||||
</button>
|
||||
|
@ -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 %}
|
||||
|
@ -15,7 +15,7 @@
|
||||
{% if record_name and record_type %}
|
||||
Record changelog: <b>{{ record_name}}   {{ 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>
|
||||
Manage Domain {{ domain.name }}
|
||||
Manage Zone {{ domain.name }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@ -125,4 +125,4 @@
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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> 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();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
@ -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()
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user