mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2024-11-08 14:40:27 +00:00
Merge pull request #586 from ngoduykhanh/code_refactor
Refactoring the code
This commit is contained in:
commit
0d2eeecce6
112
.dockerignore
Normal file
112
.dockerignore
Normal file
@ -0,0 +1,112 @@
|
||||
### OSX ###
|
||||
*.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
.pytest_cache/
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Flask stuff:
|
||||
flask/
|
||||
instance/settings.py
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule.*
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
npm-debug.log
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitattributes
|
||||
.gitignore
|
||||
|
||||
# Vscode
|
||||
.vscode
|
||||
*.code-workspace
|
||||
|
||||
# Others
|
||||
.lgtm.yml
|
||||
.travis.yml
|
16
.env
16
.env
@ -1,16 +0,0 @@
|
||||
ENVIRONMENT=development
|
||||
|
||||
PDA_DB_HOST=powerdns-admin-mysql
|
||||
PDA_DB_NAME=powerdns_admin
|
||||
PDA_DB_USER=powerdns_admin
|
||||
PDA_DB_PASSWORD=changeme
|
||||
PDA_DB_PORT=3306
|
||||
|
||||
PDNS_DB_HOST=pdns-mysql
|
||||
PDNS_DB_NAME=pdns
|
||||
PDNS_DB_USER=pdns
|
||||
PDNS_DB_PASSWORD=changeme
|
||||
|
||||
PDNS_HOST=pdns-server
|
||||
PDNS_API_KEY=changeme
|
||||
PDNS_WEBSERVER_ALLOW_FROM=0.0.0.0
|
17
.gitignore
vendored
17
.gitignore
vendored
@ -25,22 +25,17 @@ nosetests.xml
|
||||
|
||||
flask
|
||||
config.py
|
||||
configs/production.py
|
||||
logfile.log
|
||||
settings.json
|
||||
advanced_settings.json
|
||||
idp.crt
|
||||
log.txt
|
||||
|
||||
db_repository/*
|
||||
upload/avatar/*
|
||||
tmp/*
|
||||
.ropeproject
|
||||
.sonarlint/*
|
||||
pdns.db
|
||||
idp.crt
|
||||
*.bak
|
||||
db_repository/*
|
||||
tmp/*
|
||||
|
||||
node_modules
|
||||
|
||||
.webassets-cache
|
||||
app/static/generated
|
||||
.webassets-cache
|
||||
.venv*
|
||||
.pytest_cache
|
||||
|
2
.yarnrc
2
.yarnrc
@ -1 +1 @@
|
||||
--*.modules-folder "./app/static/node_modules"
|
||||
--*.modules-folder "./powerdnsadmin/static/node_modules"
|
||||
|
155
README.md
155
README.md
@ -20,17 +20,17 @@ A PowerDNS web interface with advanced features.
|
||||
- limited API for manipulating zones and records
|
||||
|
||||
### Running PowerDNS-Admin
|
||||
There are several ways to run PowerDNS-Admin. Following is a simple way to start PowerDNS-Admin with docker in development environment which has PowerDNS-Admin, PowerDNS server and MySQL Back-End Database.
|
||||
There are several ways to run PowerDNS-Admin. Following is a simple way to start PowerDNS-Admin using Docker
|
||||
|
||||
Step 1: Changing configuration
|
||||
|
||||
The configuration file for development environment is located at `configs/development.py`, you can override some configs by editing the `.env` file.
|
||||
|
||||
Step 2: Build docker images
|
||||
Step 1: Build docker image
|
||||
|
||||
```$ docker-compose build```
|
||||
|
||||
Step 3: Start docker containers
|
||||
Step 2: Change the configuration
|
||||
|
||||
Edit the `docker-compose.yml` file to update the database connection string in `SQLALCHEMY_DATABASE_URI`.
|
||||
|
||||
Step 3: Start docker container
|
||||
|
||||
```$ docker-compose up```
|
||||
|
||||
@ -39,143 +39,4 @@ You can now access PowerDNS-Admin at url http://localhost:9191
|
||||
**NOTE:** For other methods to run PowerDNS-Admin, please take look at WIKI pages.
|
||||
|
||||
### Screenshots
|
||||
![dashboard](https://user-images.githubusercontent.com/6447444/44068603-0d2d81f6-9fa5-11e8-83af-14e2ad79e370.png)
|
||||
|
||||
### Running tests
|
||||
|
||||
**NOTE:** Tests will create `__pycache__` folders which will be owned by root, which might be issue during rebuild
|
||||
|
||||
thus (e.g. invalid tar headers message) when such situation occurs, you need to remove those folders as root
|
||||
|
||||
1. Build images
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-test.yml build
|
||||
```
|
||||
|
||||
2. Run tests
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-test.yml up
|
||||
```
|
||||
|
||||
3. Rerun tests
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-test.yml down
|
||||
```
|
||||
|
||||
To teardown previous environment
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-test.yml up
|
||||
```
|
||||
|
||||
To run tests again
|
||||
|
||||
### API Usage
|
||||
|
||||
1. run docker image docker-compose up, go to UI http://localhost:9191, at http://localhost:9191/swagger is swagger API specification
|
||||
2. click to register user, type e.g. user: admin and password: admin
|
||||
3. login to UI in settings enable allow domain creation for users,
|
||||
now you can create and manage domains with admin account and also ordinary users
|
||||
4. Encode your user and password to base64, in our example we have user admin and password admin so in linux cmd line we type:
|
||||
|
||||
```
|
||||
someuser@somehost:~$echo -n 'admin:admin'|base64
|
||||
YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
we use generated output in basic authentication, we authenticate as user,
|
||||
with basic authentication, we can create/delete/get zone and create/delete/get/update apikeys
|
||||
|
||||
creating domain:
|
||||
|
||||
```
|
||||
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/zones --data '{"name": "yourdomain.com.", "kind": "NATIVE", "nameservers": ["ns1.mydomain.com."]}'
|
||||
```
|
||||
|
||||
creating apikey which has Administrator role, apikey can have also User role, when creating such apikey you have to specify also domain for which apikey is valid:
|
||||
|
||||
```
|
||||
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/apikeys --data '{"description": "masterkey","domains":[], "role": "Administrator"}'
|
||||
```
|
||||
|
||||
call above will return response like this:
|
||||
|
||||
```
|
||||
[{"description": "samekey", "domains": [], "role": {"name": "Administrator", "id": 1}, "id": 2, "plain_key": "aGCthP3KLAeyjZI"}]
|
||||
```
|
||||
|
||||
we take plain_key and base64 encode it, this is the only time we can get API key in plain text and save it somewhere:
|
||||
|
||||
```
|
||||
someuser@somehost:~$echo -n 'aGCthP3KLAeyjZI'|base64
|
||||
YUdDdGhQM0tMQWV5alpJ
|
||||
```
|
||||
|
||||
We can use apikey for all calls specified in our API specification (it tries to follow powerdns API 1:1, only tsigkeys endpoints are not yet implemented), don't forget to specify Content-Type!
|
||||
|
||||
getting powerdns configuration:
|
||||
|
||||
```
|
||||
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/config
|
||||
```
|
||||
|
||||
creating and updating records:
|
||||
|
||||
```
|
||||
curl -X PATCH -H 'Content-Type: application/json' --data '{"rrsets": [{"name": "test1.yourdomain.com.","type": "A","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "192.0.2.5", "disabled": false} ]},{"name": "test2.yourdomain.com.","type": "AAAA","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "2001:db8::6", "disabled": false} ]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://127.0.0.1:9191/api/v1/servers/localhost/zones/yourdomain.com.
|
||||
```
|
||||
|
||||
getting domain:
|
||||
|
||||
```
|
||||
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
||||
```
|
||||
|
||||
list zone records:
|
||||
|
||||
```
|
||||
curl -H 'Content-Type: application/json' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
||||
```
|
||||
|
||||
add new record:
|
||||
|
||||
```
|
||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.5.4", "disabled": false } ] } ] }' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
||||
```
|
||||
|
||||
update record:
|
||||
|
||||
```
|
||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.2.5", "disabled": false, "name": "test.yourdomain.com.", "ttl": 86400, "type": "A"}]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
||||
```
|
||||
|
||||
delete record:
|
||||
|
||||
```
|
||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "DELETE"}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq
|
||||
```
|
||||
|
||||
### Generate ER diagram
|
||||
|
||||
```
|
||||
apt-get install python-dev graphviz libgraphviz-dev pkg-config
|
||||
```
|
||||
|
||||
```
|
||||
pip install graphviz mysqlclient ERAlchemy
|
||||
```
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
```
|
||||
source .env
|
||||
```
|
||||
|
||||
```
|
||||
eralchemy -i 'mysql://${PDA_DB_USER}:${PDA_DB_PASSWORD}@'$(docker inspect powerdns-admin-mysql|jq -jr '.[0].NetworkSettings.Networks.powerdnsadmin_default.IPAddress')':3306/powerdns_admin' -o /tmp/output.pdf
|
||||
```
|
||||
![dashboard](https://user-images.githubusercontent.com/6447444/44068603-0d2d81f6-9fa5-11e8-83af-14e2ad79e370.png)
|
@ -1,51 +0,0 @@
|
||||
from werkzeug.contrib.fixers import ProxyFix
|
||||
from flask import Flask, request, session, redirect, url_for
|
||||
from flask_login import LoginManager
|
||||
from flask_sqlalchemy import SQLAlchemy as SA
|
||||
from flask_migrate import Migrate
|
||||
from authlib.flask.client import OAuth as AuthlibOAuth
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from flask_seasurf import SeaSurf
|
||||
|
||||
### SYBPATCH ###
|
||||
from app.customboxes import customBoxes
|
||||
### SYBPATCH ###
|
||||
|
||||
# subclass SQLAlchemy to enable pool_pre_ping
|
||||
class SQLAlchemy(SA):
|
||||
def apply_pool_defaults(self, app, options):
|
||||
SA.apply_pool_defaults(self, app, options)
|
||||
options["pool_pre_ping"] = True
|
||||
|
||||
|
||||
from app.assets import assets
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object('config')
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||
csrf = SeaSurf(app)
|
||||
|
||||
assets.init_app(app)
|
||||
|
||||
#### CONFIGURE LOGGER ####
|
||||
from app.lib.log import logger
|
||||
logging = logger('powerdns-admin', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config()
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
db = SQLAlchemy(app) # database
|
||||
migrate = Migrate(app, db) # flask-migrate
|
||||
authlib_oauth_client = AuthlibOAuth(app) # authlib oauth
|
||||
|
||||
if app.config.get('SAML_ENABLED') and app.config.get('SAML_ENCRYPT'):
|
||||
from app.lib import certutil
|
||||
if not certutil.check_certificate():
|
||||
certutil.create_self_signed_cert()
|
||||
|
||||
from app import models
|
||||
|
||||
from app.blueprints.api import api_blueprint
|
||||
|
||||
app.register_blueprint(api_blueprint, url_prefix='/api/v1')
|
||||
|
||||
from app import views
|
@ -1,5 +0,0 @@
|
||||
|
||||
# "boxId":("title","filter")
|
||||
class customBoxes:
|
||||
boxes = {"reverse": (" ", " "), "ip6arpa": ("ip6","%.ip6.arpa"), "inaddrarpa": ("in-addr","%.in-addr.arpa")}
|
||||
order = ["reverse", "ip6arpa", "inaddrarpa"]
|
@ -1,46 +0,0 @@
|
||||
import logging
|
||||
|
||||
class logger(object):
|
||||
def __init__(self, name, level, logfile):
|
||||
self.name = name
|
||||
self.level = level
|
||||
self.logfile = logfile
|
||||
|
||||
def config(self):
|
||||
# define logger and set logging level
|
||||
logger = logging.getLogger()
|
||||
|
||||
if self.level == 'CRITICAL':
|
||||
level = logging.CRITICAL
|
||||
elif self.level == 'ERROR':
|
||||
level = logging.ERROR
|
||||
elif self.level == 'WARNING':
|
||||
level = logging.WARNING
|
||||
elif self.level == 'DEBUG':
|
||||
level = logging.DEBUG
|
||||
else:
|
||||
level = logging.INFO
|
||||
|
||||
logger.setLevel(level)
|
||||
|
||||
# set request requests module log level
|
||||
logging.getLogger("requests").setLevel(logging.CRITICAL)
|
||||
|
||||
if self.logfile:
|
||||
# define handler to log into file
|
||||
file_log_handler = logging.FileHandler(self.logfile)
|
||||
logger.addHandler(file_log_handler)
|
||||
|
||||
# define logging format for file
|
||||
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
file_log_handler.setFormatter(file_formatter)
|
||||
|
||||
# define handler to log into console
|
||||
stderr_log_handler = logging.StreamHandler()
|
||||
logger.addHandler(stderr_log_handler)
|
||||
|
||||
# define logging format for console
|
||||
console_formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] | %(message)s')
|
||||
stderr_log_handler.setFormatter(console_formatter)
|
||||
|
||||
return logging.getLogger(self.name)
|
2391
app/models.py
2391
app/models.py
File diff suppressed because it is too large
Load Diff
108
app/oauth.py
108
app/oauth.py
@ -1,108 +0,0 @@
|
||||
from flask import request, session, redirect, url_for
|
||||
|
||||
from app import app, authlib_oauth_client
|
||||
from app.models import Setting
|
||||
|
||||
# TODO:
|
||||
# - Fix github/google enabling (Currently need to reload the flask app)
|
||||
|
||||
def github_oauth():
|
||||
if not Setting().get('github_oauth_enabled'):
|
||||
return None
|
||||
|
||||
def fetch_github_token():
|
||||
return session.get('github_token')
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@app.route('/github/authorized')
|
||||
def github_authorized():
|
||||
session['github_oauthredir'] = url_for('.github_authorized', _external=True)
|
||||
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('.login'))
|
||||
|
||||
return github
|
||||
|
||||
|
||||
def google_oauth():
|
||||
if not Setting().get('google_oauth_enabled'):
|
||||
return None
|
||||
|
||||
def fetch_google_token():
|
||||
return session.get('google_token')
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@app.route('/google/authorized')
|
||||
def google_authorized():
|
||||
session['google_oauthredir'] = url_for('.google_authorized', _external=True)
|
||||
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('.login'))
|
||||
|
||||
return google
|
||||
|
||||
def oidc_oauth():
|
||||
if not Setting().get('oidc_oauth_enabled'):
|
||||
return None
|
||||
|
||||
def fetch_oidc_token():
|
||||
return session.get('oidc_token')
|
||||
|
||||
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'),
|
||||
client_kwargs={'scope': Setting().get('oidc_oauth_scope')},
|
||||
fetch_token=fetch_oidc_token,
|
||||
)
|
||||
|
||||
@app.route('/oidc/authorized')
|
||||
def oidc_authorized():
|
||||
session['oidc_oauthredir'] = url_for('.oidc_authorized', _external=True)
|
||||
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('.login'))
|
||||
|
||||
return oidc
|
@ -1,120 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "admin_accounts" %}
|
||||
{% block title %}<title>Edit Account - {{ SITE_NAME }}</title>{% endblock %}
|
||||
|
||||
{% block dashboard_stat %}
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Account
|
||||
<small>{% if create %}New account{% else %}{{ account.name }}{% endif %}</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
|
||||
<li><a href="{{ url_for('admin_manageaccount') }}">Accounts</a></li>
|
||||
<li class="active">{% if create %}Add{% else %}Edit{% endif %} account</li>
|
||||
</ol>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">{% if create %}Add{% else %}Edit{% endif %} account</h3>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<!-- form start -->
|
||||
<form role="form" method="post" action="{% if create %}{{ url_for('admin_editaccount') }}{% else %}{{ url_for('admin_editaccount', account_name=account.name) }}{% endif %}">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="create" value="{{ create }}">
|
||||
<div class="box-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<h4><i class="icon fa fa-ban"></i> Error!</h4>
|
||||
{{ error }}
|
||||
</div>
|
||||
<span class="help-block">{{ error }}</span>
|
||||
{% endif %}
|
||||
<div class="form-group has-feedback {% if invalid_accountname or duplicate_accountname %}has-error{% endif %}">
|
||||
<label class="control-label" for="accountname">Name</label>
|
||||
<input type="text" class="form-control" placeholder="Account Name (required)"
|
||||
name="accountname" {% if account %}value="{{ account.name }}"{% endif %} {% if not create %}disabled{% endif %}>
|
||||
<span class="fa fa-cog form-control-feedback"></span>
|
||||
{% if invalid_accountname %}
|
||||
<span class="help-block">Cannot be blank and must only contain alphanumeric characters.</span>
|
||||
{% elif duplicate_accountname %}
|
||||
<span class="help-block">Account name already in use.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="accountdescription">Description</label>
|
||||
<input type="text" class="form-control" placeholder="Account Description (optional)"
|
||||
name="accountdescription" {% if account %}value="{{ account.description }}"{% endif %}>
|
||||
<span class="fa fa-industry form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="accountcontact">Contact Person</label>
|
||||
<input type="text" class="form-control" placeholder="Contact Person (optional)"
|
||||
name="accountcontact" {% if account %}value="{{ account.contact }}"{% endif %}>
|
||||
<span class="fa fa-user form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="accountmail">Mail Address</label>
|
||||
<input type="email" class="form-control" placeholder="Mail Address (optional)"
|
||||
name="accountmail" {% if account %}value="{{ account.mail }}"{% endif %}>
|
||||
<span class="fa fa-envelope form-control-feedback"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Access Control</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>Users on the right have access to manage records in all domains
|
||||
associated with the account.</p>
|
||||
<p>Click on users to move between columns.</p>
|
||||
<div class="form-group col-xs-2">
|
||||
<select multiple="multiple" class="form-control" id="account_multi_user" name="account_multi_user">
|
||||
{% for user in users %}
|
||||
<option {% if user.id in account_user_ids %}selected{% endif %} value="{{ user.username }}">{{ user.username }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-flat btn-primary">{% if create %}Create{% else %}Update{% endif %} Account</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Help with creating a new account</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>
|
||||
An account allows grouping of domains belonging to a particular entity, such as a customer or department.<br/>
|
||||
A domain can be assigned to an account upon domain creation or through the domain administration page.
|
||||
</p>
|
||||
<p>Fill in all the fields to the in the form to the left.</p>
|
||||
<p>
|
||||
<strong>Name</strong> is an account identifier. It will be stored as all lowercase letters (no spaces, special characters etc).<br/>
|
||||
<strong>Description</strong> is a user friendly name for this account.<br/>
|
||||
<strong>Contact person</strong> is the name of a contact person at the account.<br/>
|
||||
<strong>Mail Address</strong> is an e-mail address for the contact person.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
$("#account_multi_user").multiSelect();
|
||||
</script>
|
||||
{% endblock %}
|
@ -1,155 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "admin_users" %}
|
||||
{% block title %}<title>Edit User - {{ SITE_NAME }}</title>{% endblock %}
|
||||
|
||||
{% block dashboard_stat %}
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
User
|
||||
<small>{% if create %}New user{% else %}{{ user.username }}{% endif %}</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
|
||||
<li><a href="{{ url_for('dashboard') }}">Admin</a></li>
|
||||
<li class="active">{% if create %}Add{% else %}Edit{% endif %} user</li>
|
||||
</ol>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">{% if create %}Add{% else %}Edit{% endif %} user</h3>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<!-- form start -->
|
||||
<form role="form" method="post" action="{% if create %}{{ url_for('admin_edituser') }}{% else %}{{ url_for('admin_edituser', user_username=user.username) }}{% endif %}">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="create" value="{{ create }}">
|
||||
<div class="box-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<h4><i class="icon fa fa-ban"></i> Error!</h4>
|
||||
{{ error }}
|
||||
</div>
|
||||
<span class="help-block">{{ error }}</span>
|
||||
{% endif %}
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="firstname">First Name</label>
|
||||
<input type="text" class="form-control" placeholder="First Name"
|
||||
name="firstname" {% if user %}value="{{ user.firstname }}"{% endif %}> <span
|
||||
class="glyphicon glyphicon-user form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="lastname">Last Name</label>
|
||||
<input type="text" class="form-control" placeholder="Last name"
|
||||
name="lastname" {% if user %}value="{{ user.lastname }}"{% endif %}> <span
|
||||
class="glyphicon glyphicon-user form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="email">E-mail address</label>
|
||||
<input type="email" class="form-control" placeholder="Email"
|
||||
name="email" id="email" {% if user %}value="{{ user.email }}"{% endif %}> <span
|
||||
class="glyphicon glyphicon-envelope form-control-feedback"></span>
|
||||
</div>
|
||||
<p class="login-box-msg">Enter the account details below</p>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="username">Username</label>
|
||||
<input type="text" class="form-control" placeholder="Username"
|
||||
name="username" {% if user %}value="{{ user.username }}"{% endif %} {% if not create %}disabled{% endif %}> <span
|
||||
class="glyphicon glyphicon-user form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback {% if blank_password %}has-error{% endif %}">
|
||||
<label class="control-label" for="username">Password</label>
|
||||
<input type="password" class="form-control" placeholder="Password {% if create %}(Required){% else %}(Leave blank to keep unchanged){% endif %}"
|
||||
name="password"> <span
|
||||
class="glyphicon glyphicon-lock form-control-feedback"></span>
|
||||
{% if blank_password %}
|
||||
<span class="help-block">The password cannot be blank.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-flat btn-primary">{% if create %}Create{% else %}Update{% endif %} User</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% if not create %}
|
||||
<div class="box box-secondary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Two Factor Authentication</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>If two factor authentication was configured and is causing problems due to a lost device or technical issue, it can be disabled here.</p>
|
||||
<p>The user will need to reconfigure two factor authentication, to re-enable it.</p>
|
||||
<p><strong>Beware: This could compromise security!</strong></p>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="button" class="btn btn-flat btn-warning button_otp_disable" id="{{ user.username }}" {% if not user.otp_secret %}disabled{% endif %}>Disable Two Factor Authentication</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Help with {% if create %}creating a new{% else%}updating a{% endif %} user</h3>
|
||||
</div>
|
||||
<div class="box-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 to grant access to the user once it is created via the domain management buttons on the dashboard.</p>
|
||||
{% else %}
|
||||
<p><strong>Password</strong> can be left empty to keep the current password.</p>
|
||||
<p><strong>Username</strong> cannot be changed.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
// handle disabling two factor authentication
|
||||
$(document.body).on('click', '.button_otp_disable', function() {
|
||||
var modal = $("#modal_otp_disable");
|
||||
var username = $(this).prop('id');
|
||||
var info = "Are you sure you want to disable two factor authentication for user " + username + "?";
|
||||
modal.find('.modal-body p').text(info);
|
||||
modal.find('#button_otp_disable_confirm').click(function() {
|
||||
var postdata = {'action': 'user_otp_disable', 'data': username, '_csrf_token': '{{ csrf_token() }}'}
|
||||
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manageuser', false, true);
|
||||
})
|
||||
modal.modal('show');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block modals %}
|
||||
<div class="modal fade modal-warning" id="modal_otp_disable">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Confirmation</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-flat btn-default pull-left"
|
||||
data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-flat btn-danger" id="button_otp_disable_confirm">Disable Two Factor Authentication</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,86 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "admin_settings" %}
|
||||
{% block title %}
|
||||
<title>PDNS Settings - {{ SITE_NAME }}</title>
|
||||
{% endblock %} {% block dashboard_stat %}
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Settings <small>PowerDNS-Admin settings</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li><a href="#">Setting</a></li>
|
||||
<li class="active">PDNS</li>
|
||||
</ol>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">PDNS Settings</h3>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<!-- form start -->
|
||||
<form role="form" method="post" data-toggle="validator">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="box-body">
|
||||
{% if not SETTING.get('pdns_api_url') or not SETTING.get('pdns_api_key') or not SETTING.get('pdns_version') %}
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<h4><i class="icon fa fa-ban"></i> Error!</h4>
|
||||
Please complete your PowerDNS API configuration before continuing
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="pdns_api_url">PDNS API URL</label>
|
||||
<input type="url" class="form-control" placeholder="PowerDNS API url" name="pdns_api_url" data-error="Please input a valid PowerDNS API URL" required value="{{ pdns_api_url }}">
|
||||
<span class="help-block with-errors"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="pdns_api_key">PDNS API KEY</label>
|
||||
<input type="password" class="form-control" placeholder="PowerDNS API key" name="pdns_api_key" data-error="Please input a valid PowerDNS API key" required value="{{ pdns_api_key }}">
|
||||
<span class="help-block with-errors"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="pdns_version">PDNS VERSION</label>
|
||||
<input type="text" class="form-control" placeholder="PowerDNS version" name="pdns_version" data-error="Please input PowerDNS version" required value="{{ pdns_version }}">
|
||||
<span class="help-block with-errors"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-flat btn-primary">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Help</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<dl class="dl-horizontal">
|
||||
<p>You must configure the API connection information before PowerDNS-Admin can query your PowerDNS data. Following fields are required:</p>
|
||||
<dt>PDNS API URL</dt>
|
||||
<dd>Your PowerDNS API URL (eg. http://127.0.0.1:8081/).</dd>
|
||||
<dt>PDNS API KEY</dt>
|
||||
<dd>Your PowerDNS API key.</dd>
|
||||
<dt>PDNS VERSION</dt>
|
||||
<dd>Your PowerDNS version number (eg. 4.1.1).</dd>
|
||||
</dl>
|
||||
<p>Find more details at <a href="https://doc.powerdns.com/md/httpapi/README/">https://doc.powerdns.com/md/httpapi/README/</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
{% assets "js_validation" -%}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{%- endassets %}
|
||||
{% endblock %}
|
@ -1,79 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "admin_settings" %}
|
||||
{% block title %}
|
||||
<title>DNS Records Settings - {{ SITE_NAME }}</title>
|
||||
{% endblock %} {% block dashboard_stat %}
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Settings <small>PowerDNS-Admin settings</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li><a href="#">Setting</a></li>
|
||||
<li class="active">Records</li>
|
||||
</ol>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">DNS record Settings</h3>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<!-- form start -->
|
||||
<form role="form" method="post">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="create" value="{{ create }}">
|
||||
<div class="box-body">
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th style="width: 10px">#</th>
|
||||
<th style="width: 40px">Record</th>
|
||||
<th>Forward Zone</th>
|
||||
<th>Reverse Zone</th>
|
||||
</tr>
|
||||
{% for record in f_records %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ record }}</td>
|
||||
<td>
|
||||
<input type="checkbox" id="fr_{{ record|lower }}" name="fr_{{ record|lower }}" class="checkbox" {% if f_records[record] %}checked{% endif %}>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" id="rr_{{ record|lower }}" name="rr_{{ record|lower }}" class="checkbox" {% if r_records[record] %}checked{% endif %}>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-flat btn-primary">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Help</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>Select record types you allow user to edit in the forward zone and reverse zone. Take a look at <a href="https://doc.powerdns.com/authoritative/appendices/types.html">PowerDNS docs</a> for full list of supported record types.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
$('.checkbox').iCheck({
|
||||
checkboxClass : 'icheckbox_square-blue',
|
||||
increaseArea : '20%'
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
@ -1,163 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "new_domain" %}
|
||||
{% block title %}<title>Add Domain - {{ SITE_NAME }}</title>{% endblock %}
|
||||
|
||||
{% block dashboard_stat %}
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Domain
|
||||
<small>Create new</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
|
||||
<li><a href="{{ url_for('dashboard') }}">Domain</a></li>
|
||||
<li class="active">Add Domain</li>
|
||||
</ol>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Create new domain</h3>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<!-- form start -->
|
||||
<form role="form" method="post" action="{{ url_for('domain_add') }}">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="box-body">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="domain_name" id="domain_name" placeholder="Enter a valid domain name (required)">
|
||||
</div>
|
||||
<select name="accountid" class="form-control" style="width:15em;">
|
||||
<option value="0">- No Account -</option>
|
||||
{% for account in accounts %}
|
||||
<option value="{{ account.id }}">{{ account.name }}</option>
|
||||
{% endfor %}
|
||||
</select><br/>
|
||||
<div class="form-group">
|
||||
<label>Type</label>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="radio_type" id="radio_type_native" value="native" checked> Native
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="radio" name="radio_type" id="radio_type_master" value="master"> Master
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="radio" name="radio_type" id="radio_type_slave" value="slave">Slave
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Select a template</label>
|
||||
<select class="form-control" id="domain_template" name="domain_template">
|
||||
<option value="0">No template</option>
|
||||
{% for template in templates %}
|
||||
<option value="{{ template.id }}">{{ template.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</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 master ip addresses (separated by commas)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>SOA-EDIT-API</label>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="radio_type_soa_edit_api" id="radio_default" value="DEFAULT" checked> DEFAULT
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="radio_type_soa_edit_api" id="radio_increase" value="INCREASE"> INCREASE
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="radio_type_soa_edit_api" id="radio_epoch" value="EPOCH"> EPOCH
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="radio_type_soa_edit_api" id="radio_off" value="OFF"> OFF
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
|
||||
<div class="box-footer">
|
||||
<button type="submit" class="btn btn-flat btn-primary">Submit</button>
|
||||
<button type="button" class="btn btn-flat btn-default" onclick="window.location.href='{{ url_for('dashboard') }}'">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Help with creating a new domain</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Domain name</dt>
|
||||
<dd>Enter your domain name in the format of name.tld (eg. powerdns-admin.com). You can also enter sub-domains to create a sub-root zone (eg. sub.powerdns-admin.com) in case you want to delegate sub-domain management to specific users.</dd>
|
||||
<dt>Type</dt>
|
||||
<dd>The type decides how the domain will be replicated across multiple DNS servers.
|
||||
<ul>
|
||||
<li>
|
||||
Native - PowerDNS will not perform any replication. Use this if you only have one PowerDNS server or you handle replication via your backend (MySQL).
|
||||
</li>
|
||||
<li>
|
||||
Master - This PowerDNS server will serve as the master and will send zone transfers (AXFRs) to other servers configured as slaves.
|
||||
</li>
|
||||
<li>
|
||||
Slave - This PowerDNS server will serve as the slave and will request and receive zone transfers (AXFRs) from other servers configured as masters.
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
<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.
|
||||
<ul>
|
||||
<li>
|
||||
DEFAULT - Generate a soa serial of YYYYMMDD01. If the current serial is lower than the generated serial, use the generated serial. If the current serial is higher or equal to the generated serial, increase the current serial by 1.
|
||||
</li>
|
||||
<li>
|
||||
INCREASE - Increase the current serial by 1.
|
||||
</li>
|
||||
<li>
|
||||
EPOCH - Change the serial to the number of seconds since the EPOCH, aka unixtime.
|
||||
</li>
|
||||
<li>
|
||||
OFF - Disable automatic updates of the SOA serial.
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
<p>Find more details at <a href="https://docs.powerdns.com/md/">https://docs.powerdns.com/md/</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
$("input[name=radio_type]").change(function() {
|
||||
var type = $(this).val();
|
||||
if (type == "slave") {
|
||||
$("#domain_master_address_div").show();
|
||||
} else {
|
||||
$("#domain_master_address_div").hide();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -1,272 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}<title>Domain Management - {{ SITE_NAME }}</title>{% endblock %}
|
||||
|
||||
{% block dashboard_stat %}
|
||||
{% if status %}
|
||||
{% if status.get('status') == 'ok' %}
|
||||
<div class="alert alert-success">
|
||||
<strong>Success!</strong> {{ status.get('msg') }}
|
||||
</div>
|
||||
{% elif status.get('status') == 'error' %}
|
||||
<div class="alert alert-danger">
|
||||
{% if status.get('msg') != None %}
|
||||
<strong>Error!</strong> {{ status.get('msg') }}
|
||||
{% else %}
|
||||
<strong>Error!</strong> An undefined error occurred.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Manage domain <small>{{ domain.name }}</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard') }}"><i
|
||||
class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li class="active">Domain Management</li>
|
||||
</ol>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<form method="post" action="{{ url_for('domain_management', domain_name=domain.name) }}">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Domain Access Control</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-2">
|
||||
<p>Users on the right have access to manage the records in
|
||||
the {{ domain.name }} domain.</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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group col-xs-2">
|
||||
<select multiple="multiple" class="form-control" id="domain_multi_user" name="domain_multi_user[]">
|
||||
{% for user in users %}
|
||||
<option {% if user.id in
|
||||
domain_user_ids %}selected{% endif %} value="{{ user.username }}"
|
||||
{% if user.role.name== 'Administrator' %}style="color: red"{% endif %}>{{
|
||||
user.username}}</option> {% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="col-xs-offset-2">
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-flat btn-primary"><i class="fa fa-check"></i> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Account</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="col-xs-12">
|
||||
<div class="form-group">
|
||||
<form method="post" action="{{ url_for('domain_change_account', domain_name=domain.name) }}">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<select name="accountid" class="form-control" style="width:15em;">
|
||||
<option value="0">- No Account -</option>
|
||||
{% for account in accounts %}
|
||||
<option value="{{ account.id }}" {% if domain_account.id == account.id %}selected{% endif %}>{{ account.name }}</option>
|
||||
{% endfor %}
|
||||
</select><br/>
|
||||
<button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api">
|
||||
<i class="fa fa-check"></i> Change account for {{ domain.name }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Auto PTR creation</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p><input type="checkbox" id="{{ domain.name }}" class="auto_ptr_toggle"
|
||||
{% for setting in domain.settings %}{% if setting.setting=='auto_ptr' and setting.value=='True' %}checked{% endif %}{% endfor %} {% if SETTING.get('auto_ptr') %}disabled="True"{% endif %}>
|
||||
Allow automatic reverse pointer creation on record updates?{% if
|
||||
SETTING.get('auto_ptr') %}</br><code>Auto-ptr is enabled globally on the PDA system!</code>{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">DynDNS 2 Settings</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p><input type="checkbox" id="{{ domain.name }}" class="dyndns_on_demand_toggle"
|
||||
{% for setting in domain.settings %}{% if setting.setting=='create_via_dyndns' and setting.value=='True' %}checked{% endif %}{% endfor %}>
|
||||
Allow on-demand creation of records via DynDNS updates?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Change SOA-EDIT-API</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>The SOA-EDIT-API setting defines how the SOA serial number will be updated after a change is made to the domain.</p>
|
||||
<ul>
|
||||
<li>
|
||||
DEFAULT - Generate a soa serial of YYYYMMDD01. If the current serial is lower than the generated serial, use the generated serial. If the current serial is higher or equal to the generated serial, increase the current serial by 1.
|
||||
</li>
|
||||
<li>
|
||||
INCREASE - Increase the current serial by 1.
|
||||
</li>
|
||||
<li>
|
||||
EPOCH - Change the serial to the number of seconds since the EPOCH, aka unixtime.
|
||||
</li>
|
||||
<li>
|
||||
OFF - Disable automatic updates of the SOA serial.
|
||||
</li>
|
||||
</ul>
|
||||
<b>New SOA-EDIT-API Setting:</b>
|
||||
<form method="post" action="{{ url_for('domain_change_soa_edit_api', domain_name=domain.name) }}">
|
||||
<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>
|
||||
</select><br/>
|
||||
<button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api">
|
||||
<i class="fa fa-check"></i> Change SOA-EDIT-API setting for {{ domain.name }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Domain Deletion</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>This function is used to remove a domain from PowerDNS-Admin <b>AND</b> PowerDNS. All records and user privileges associated with this domain will also be removed. This change cannot be reverted.</p>
|
||||
<button type="button" class="btn btn-flat btn-danger pull-left delete_domain" id="{{ domain.name }}">
|
||||
<i class="fa fa-trash"></i> DELETE DOMAIN {{ domain.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
//initialize pretty checkboxes
|
||||
$('.dyndns_on_demand_toggle').iCheck({
|
||||
checkboxClass : 'icheckbox_square-blue',
|
||||
increaseArea : '20%' // optional
|
||||
});
|
||||
$('.auto_ptr_toggle').iCheck({
|
||||
checkboxClass : 'icheckbox_square-blue',
|
||||
increaseArea : '20%' // optional
|
||||
});
|
||||
|
||||
$("#domain_multi_user").multiSelect();
|
||||
|
||||
//handle checkbox toggling
|
||||
$('.dyndns_on_demand_toggle').on('ifToggled', function(event) {
|
||||
var is_checked = $(this).prop('checked');
|
||||
var domain = $(this).prop('id');
|
||||
postdata = {
|
||||
'action' : 'set_setting',
|
||||
'data' : {
|
||||
'setting' : 'create_via_dyndns',
|
||||
'value' : is_checked
|
||||
},
|
||||
'_csrf_token': '{{ csrf_token() }}'
|
||||
};
|
||||
applyChanges(postdata, $SCRIPT_ROOT + '/domain/' + domain + '/managesetting', true);
|
||||
});
|
||||
$('.auto_ptr_toggle').on('ifToggled', function(event) {
|
||||
var is_checked = $(this).prop('checked');
|
||||
var domain = $(this).prop('id');
|
||||
postdata = {
|
||||
'action' : 'set_setting',
|
||||
'data' : {
|
||||
'setting' : 'auto_ptr',
|
||||
'value' : is_checked
|
||||
},
|
||||
'_csrf_token': '{{ csrf_token() }}'
|
||||
};
|
||||
applyChanges(postdata, $SCRIPT_ROOT + '/domain/' + domain + '/managesetting', true);
|
||||
});
|
||||
|
||||
// handle deletion of domain
|
||||
$(document.body).on('click', '.delete_domain', function() {
|
||||
var modal = $("#modal_delete_domain");
|
||||
var domain = $(this).prop('id');
|
||||
var info = "Are you sure you want to delete " + domain + "?";
|
||||
modal.find('.modal-body p').text(info);
|
||||
modal.find('#button_delete_confirm').click(function() {
|
||||
$.post($SCRIPT_ROOT + '/admin/domain/' + domain + '/delete', { '_csrf_token': '{{ csrf_token() }}' }, function() {
|
||||
window.location.href = '{{ url_for('dashboard') }}';
|
||||
});
|
||||
modal.modal('hide');
|
||||
})
|
||||
modal.modal('show');
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block modals %}
|
||||
<div class="modal fade modal-warning" id="modal_delete_domain">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Confirmation</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-flat btn-default pull-left"
|
||||
data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-flat btn-danger" id="button_delete_confirm">
|
||||
Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.modal-content -->
|
||||
</div>
|
||||
<!-- /.modal-dialog -->
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,138 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Log In - {{ SITE_NAME }}</title>
|
||||
<!-- Tell the browser to be responsive to screen width -->
|
||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||
{% assets "css_login" -%}
|
||||
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
||||
{%- endassets %}
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
|
||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body class="hold-transition login-page">
|
||||
<div class="login-box">
|
||||
<div class="login-logo">
|
||||
<a href="{{ url_for('index') }}"><b>PowerDNS</b>-Admin</a>
|
||||
</div>
|
||||
<!-- /.login-logo -->
|
||||
<div class="login-box-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert"
|
||||
aria-hidden="true">×</button>
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="" method="post" data-toggle="validator">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" placeholder="Username" name="username" data-error="Please input your username" required {% if username %}value="{{ username }}"{% endif %}>
|
||||
<span class="help-block with-errors"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" placeholder="Password" name="password" data-error="Please input your password" required {% if password %}value="{{ password }}"{% endif %}>
|
||||
<span class="help-block with-errors"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="otptoken" class="form-control" placeholder="OTP Token" name="otptoken">
|
||||
</div>
|
||||
{% if SETTING.get('ldap_enabled') and SETTING.get('local_db_enabled') %}
|
||||
<div class="form-group">
|
||||
<select class="form-control" name="auth_method">
|
||||
<option value="LOCAL">LOCAL Authentication</option>
|
||||
{% if SETTING.get('login_ldap_first') %}
|
||||
<option value="LDAP" selected="selected">LDAP Authentication</option>
|
||||
{% else %}
|
||||
<option value="LDAP">LDAP Authentication</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
{% elif SETTING.get('ldap_enabled') and not SETTING.get('local_db_enabled') %}
|
||||
<div class="form-group">
|
||||
<input type="hidden" name="auth_method" value="LDAP">
|
||||
</div>
|
||||
{% elif SETTING.get('local_db_enabled') and not SETTING.get('ldap_enabled') %}
|
||||
<div class="form-group">
|
||||
<input type="hidden" name="auth_method" value="LOCAL">
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="form-group">
|
||||
<input type="hidden" name="auth_method" value="LOCAL">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-8">
|
||||
<div class="checkbox icheck">
|
||||
<label>
|
||||
<input type="checkbox" name="remember"> Remember Me
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.col -->
|
||||
<div class="col-xs-4">
|
||||
<button type="submit" class="btn btn-flat btn-primary btn-block">Sign In</button>
|
||||
</div>
|
||||
<!-- /.col -->
|
||||
</div>
|
||||
</form>
|
||||
{% if SETTING.get('google_oauth_enabled') or SETTING.get('github_oauth_enabled') or SETTING.get('oidc_oauth_enabled') %}
|
||||
<div class="social-auth-links text-center">
|
||||
<p>- OR -</p>
|
||||
{% if SETTING.get('oidc_oauth_enabled') %}
|
||||
<a href="{{ url_for('oidc_login') }}" class="btn btn-block btn-social btn-openid btn-flat"><i class="fa fa-openid"></i> Sign in using
|
||||
OpenID Connect</a>
|
||||
{% endif %}
|
||||
{% if SETTING.get('github_oauth_enabled') %}
|
||||
<a href="{{ url_for('github_login') }}" class="btn btn-block btn-social btn-github btn-flat"><i class="fa fa-github"></i> Sign in using
|
||||
Github</a>
|
||||
{% endif %}
|
||||
{% if SETTING.get('google_oauth_enabled') %}
|
||||
<a href="{{ url_for('google_login') }}" class="btn btn-block btn-social btn-google btn-flat"><i class="fa fa-google-plus"></i> Sign in using
|
||||
Google</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if saml_enabled %}
|
||||
<a href="{{ url_for('saml_login') }}">SAML login</a>
|
||||
{% endif %}
|
||||
|
||||
{% if SETTING.get('signup_enabled') %}
|
||||
<br>
|
||||
<a href="{{ url_for('register') }}" class="text-center">Create an account </a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- /.login-box-body -->
|
||||
<div class="login-box-footer">
|
||||
<center><p>Powered by <a href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</a></p></center>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.login-box -->
|
||||
|
||||
{% assets "js_login" -%}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{%- endassets %}
|
||||
{% assets "js_validation" -%}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{%- endassets %}
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
$('input').iCheck({
|
||||
checkboxClass: 'icheckbox_square-blue',
|
||||
radioClass: 'iradio_square-blue',
|
||||
increaseArea: '20%' // optional
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,18 +0,0 @@
|
||||
<!doctype html>
|
||||
<title>Site Maintenance</title>
|
||||
<style>
|
||||
body { text-align: center; padding: 150px; }
|
||||
h1 { font-size: 50px; }
|
||||
body { font: 20px Helvetica, sans-serif; color: #333; }
|
||||
article { display: block; text-align: left; width: 650px; margin: 0 auto; }
|
||||
a { color: #dc8100; text-decoration: none; }
|
||||
a:hover { color: #333; text-decoration: none; }
|
||||
</style>
|
||||
|
||||
<article>
|
||||
<h1>We’ll be back soon!</h1>
|
||||
<div>
|
||||
<p>Sorry for the inconvenience but we’re performing some maintenance at the moment. Please contact the System Administrator if you need more information</a>, otherwise we’ll be back online shortly!</p>
|
||||
<p>— Team</p>
|
||||
</div>
|
||||
</article>
|
@ -1,99 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Register - {{ SITE_NAME }}</title>
|
||||
<!-- Tell the browser to be responsive to screen width -->
|
||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||
{% assets "css_login" -%}
|
||||
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
||||
{%- endassets %}
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
|
||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body class="hold-transition register-page">
|
||||
<div class="register-box">
|
||||
<div class="register-logo">
|
||||
<a href="{{ url_for('index') }}"><b>PowerDNS</b>-Admin</a>
|
||||
</div>
|
||||
<div class="register-box-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert"
|
||||
aria-hidden="true">×</button>
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="login-box-msg">Enter your personal details below</p>
|
||||
<form action="{{ url_for('register') }}" method="post" data-toggle="validator">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="form-group has-feedback">
|
||||
<input type="text" class="form-control" placeholder="First Name" name="firstname" data-error="Please input your first name" required>
|
||||
<span class="glyphicon glyphicon-user form-control-feedback"></span>
|
||||
<span class="help-block with-errors"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<input type="text" class="form-control" placeholder="Last name" name="lastname" data-error="Please input your last name" required>
|
||||
<span class="glyphicon glyphicon-user form-control-feedback"></span>
|
||||
<span class="help-block with-errors"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<input type="email" class="form-control" placeholder="Email" name="email" data-error="Please input your valid email address"
|
||||
pattern="^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$" required>
|
||||
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
|
||||
<span class="help-block with-errors"></span>
|
||||
</div>
|
||||
<p class="login-box-msg">Enter your account details below</p>
|
||||
<div class="form-group has-feedback">
|
||||
<input type="text" class="form-control" placeholder="Username" name="username" data-error="Please input your username" required>
|
||||
<span class="glyphicon glyphicon-user form-control-feedback"></span>
|
||||
<span class="help-block with-errors"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<input type="password" class="form-control" placeholder="Password" id="password" name="password" data-error="Please input your password" required>
|
||||
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<input type="password" class="form-control" placeholder="Retype password" name="rpassword" data-match="#password" data-match-error="Password confirmation does not match" required>
|
||||
<span class="glyphicon glyphicon-log-in form-control-feedback"></span>
|
||||
<span class="help-block with-errors"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-4 pull-left">
|
||||
<button type="button" class="btn btn-flat btn-block" id="button_back">Back</button>
|
||||
</div>
|
||||
<div class="col-xs-4 pull-right">
|
||||
<button type="submit" class="btn btn-flat btn-primary btn-block">Register</button>
|
||||
</div>
|
||||
<!-- /.col -->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- /.form-box -->
|
||||
<div class="login-box-footer">
|
||||
<center><p>Powered by <a href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</a></p></center>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.login-box -->
|
||||
|
||||
{% assets "js_login" -%}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{%- endassets %}
|
||||
{% assets "js_validation" -%}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{%- endassets %}
|
||||
<script>
|
||||
$(function () {
|
||||
$('#button_back').click(function(){
|
||||
window.location.href='{{ url_for('login') }}';
|
||||
})
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,124 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "admin_domain_template" %}
|
||||
{% block title %}<title>Templates - {{ SITE_NAME }}</title>{% endblock %}
|
||||
|
||||
{% block dashboard_stat %}
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Templates
|
||||
<small>List</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('templates') }}"><i class="fa fa-dashboard"></i> Templates</a></li>
|
||||
<li class="active">List</li>
|
||||
</ol>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
{% with errors = get_flashed_messages(category_filter=["error"]) %} {% if errors %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert"
|
||||
aria-hidden="true">×</button>
|
||||
<h4>
|
||||
<i class="icon fa fa-ban"></i> Error!
|
||||
</h4>
|
||||
<div class="alert-message block-message error">
|
||||
<a class="close" href="#">x</a>
|
||||
<ul>
|
||||
{%- for msg in errors %}
|
||||
<li>{{ msg }}</li> {% endfor -%}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% endwith %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">Templates</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<a href="{{ url_for('create_template') }}">
|
||||
<button type="button" class="btn btn-flat btn-primary pull-left">
|
||||
Create Template <i class="fa fa-plus"></i>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<table id="tbl_template_list" class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Number of Records</th>
|
||||
<th width="20%">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for template in templates %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('edit_template', template=template.name) }}"><strong>{{ template.name }}</strong></a>
|
||||
</td>
|
||||
<td>
|
||||
{{ template.description }}
|
||||
</td>
|
||||
<td>
|
||||
{{ template.records|count }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('edit_template', template=template.name) }}">
|
||||
<button type="button" class="btn btn-flat btn-warning button_edit" id="btn_edit">
|
||||
Edit <i class="fa fa-edit"></i>
|
||||
</button>
|
||||
</a>
|
||||
<button type="button" class="btn btn-flat btn-danger button_delete" id="{{template.name}}">
|
||||
Delete <i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- /.box-body -->
|
||||
</div>
|
||||
<!-- /.box -->
|
||||
</div>
|
||||
<!-- /.col -->
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
</section>
|
||||
<!-- /.content -->
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
// set up history data table
|
||||
$("#tbl_template_list").DataTable({
|
||||
"paging" : true,
|
||||
"lengthChange" : true,
|
||||
"searching" : true,
|
||||
"ordering" : true,
|
||||
"info" : false,
|
||||
"autoWidth" : false
|
||||
});
|
||||
// handle delete button
|
||||
$(document.body).on("click", ".button_delete", function(e) {
|
||||
var template = $(this).prop('id');
|
||||
$.post($SCRIPT_ROOT + '/template/' + template + '/delete', { '_csrf_token': '{{ csrf_token() }}' }, function() {
|
||||
window.location.href = '{{ url_for('templates') }}';
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block modals %}
|
||||
{% endblock %}
|
1886
app/views.py
1886
app/views.py
File diff suppressed because it is too large
Load Diff
@ -1,133 +0,0 @@
|
||||
import os
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
# BASIC APP CONFIG
|
||||
SECRET_KEY = 'We are the world'
|
||||
BIND_ADDRESS = '127.0.0.1'
|
||||
PORT = 9191
|
||||
|
||||
# TIMEOUT - for large zones
|
||||
TIMEOUT = 10
|
||||
|
||||
# LOG CONFIG
|
||||
# - For docker, LOG_FILE=''
|
||||
LOG_LEVEL = 'DEBUG'
|
||||
LOG_FILE = 'logfile.log'
|
||||
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
||||
|
||||
# UPLOAD DIRECTORY
|
||||
UPLOAD_DIR = os.path.join(basedir, 'upload')
|
||||
|
||||
# DATABASE CONFIG
|
||||
SQLA_DB_USER = 'pda'
|
||||
SQLA_DB_PASSWORD = 'changeme'
|
||||
SQLA_DB_HOST = '127.0.0.1'
|
||||
SQLA_DB_PORT = 3306
|
||||
SQLA_DB_NAME = 'pda'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||
|
||||
# DATABASE - MySQL
|
||||
SQLALCHEMY_DATABASE_URI = 'mysql://'+SQLA_DB_USER+':'+SQLA_DB_PASSWORD+'@'+SQLA_DB_HOST+':'+str(SQLA_DB_PORT)+'/'+SQLA_DB_NAME
|
||||
|
||||
# DATABASE - SQLite
|
||||
# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
|
||||
|
||||
# SAML Authentication
|
||||
SAML_ENABLED = False
|
||||
SAML_DEBUG = True
|
||||
SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml')
|
||||
##Example for ADFS Metadata-URL
|
||||
SAML_METADATA_URL = 'https://<hostname>/FederationMetadata/2007-06/FederationMetadata.xml'
|
||||
#Cache Lifetime in Seconds
|
||||
SAML_METADATA_CACHE_LIFETIME = 1
|
||||
|
||||
# SAML SSO binding format to use
|
||||
## Default: library default (urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect)
|
||||
#SAML_IDP_SSO_BINDING = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
||||
|
||||
## EntityID of the IdP to use. Only needed if more than one IdP is
|
||||
## in the SAML_METADATA_URL
|
||||
### Default: First (only) IdP in the SAML_METADATA_URL
|
||||
### Example: https://idp.example.edu/idp
|
||||
#SAML_IDP_ENTITY_ID = 'https://idp.example.edu/idp'
|
||||
## NameID format to request
|
||||
### Default: The SAML NameID Format in the metadata if present,
|
||||
### otherwise urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
|
||||
### Example: urn:oid:0.9.2342.19200300.100.1.1
|
||||
#SAML_NAMEID_FORMAT = 'urn:oid:0.9.2342.19200300.100.1.1'
|
||||
|
||||
## Attribute to use for Email address
|
||||
### Default: email
|
||||
### Example: urn:oid:0.9.2342.19200300.100.1.3
|
||||
#SAML_ATTRIBUTE_EMAIL = 'urn:oid:0.9.2342.19200300.100.1.3'
|
||||
|
||||
## Attribute to use for Given name
|
||||
### Default: givenname
|
||||
### Example: urn:oid:2.5.4.42
|
||||
#SAML_ATTRIBUTE_GIVENNAME = 'urn:oid:2.5.4.42'
|
||||
|
||||
## Attribute to use for Surname
|
||||
### Default: surname
|
||||
### Example: urn:oid:2.5.4.4
|
||||
#SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4'
|
||||
|
||||
## Split into Given name and Surname
|
||||
## Useful if your IDP only gives a display name
|
||||
### Default: none
|
||||
### Example: http://schemas.microsoft.com/identity/claims/displayname
|
||||
#SAML_ATTRIBUTE_NAME = 'http://schemas.microsoft.com/identity/claims/displayname'
|
||||
|
||||
## Attribute to use for username
|
||||
### Default: Use NameID instead
|
||||
### Example: urn:oid:0.9.2342.19200300.100.1.1
|
||||
#SAML_ATTRIBUTE_USERNAME = 'urn:oid:0.9.2342.19200300.100.1.1'
|
||||
|
||||
## Attribute to get admin status from
|
||||
### Default: Don't control admin with SAML attribute
|
||||
### Example: https://example.edu/pdns-admin
|
||||
### If set, look for the value 'true' to set a user as an administrator
|
||||
### If not included in assertion, or set to something other than 'true',
|
||||
### the user is set as a non-administrator user.
|
||||
#SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin'
|
||||
|
||||
## Attribute to get group from
|
||||
### Default: Don't use groups from SAML attribute
|
||||
### Example: https://example.edu/pdns-admin-group
|
||||
#SAML_ATTRIBUTE_GROUP = 'https://example.edu/pdns-admin'
|
||||
|
||||
## Group namem to get admin status from
|
||||
### Default: Don't control admin with SAML group
|
||||
### Example: https://example.edu/pdns-admin
|
||||
#SAML_GROUP_ADMIN_NAME = 'powerdns-admin'
|
||||
|
||||
## Attribute to get group to account mappings from
|
||||
### Default: None
|
||||
### If set, the user will be added and removed from accounts to match
|
||||
### what's in the login assertion if they are in the required group
|
||||
#SAML_GROUP_TO_ACCOUNT_MAPPING = 'dev-admins=dev,prod-admins=prod'
|
||||
|
||||
## Attribute to get account names from
|
||||
### Default: Don't control accounts with SAML attribute
|
||||
### If set, the user will be added and removed from accounts to match
|
||||
### what's in the login assertion. Accounts that don't exist will
|
||||
### be created and the user added to them.
|
||||
SAML_ATTRIBUTE_ACCOUNT = 'https://example.edu/pdns-account'
|
||||
|
||||
SAML_SP_ENTITY_ID = 'http://<SAML SP Entity ID>'
|
||||
SAML_SP_CONTACT_NAME = '<contact name>'
|
||||
SAML_SP_CONTACT_MAIL = '<contact mail>'
|
||||
#Configures if SAML tokens should be encrypted.
|
||||
#If enabled a new app certificate will be generated on restart
|
||||
SAML_SIGN_REQUEST = False
|
||||
|
||||
# Configures if you want to request the IDP to sign the message
|
||||
# Default is True
|
||||
#SAML_WANT_MESSAGE_SIGNED = True
|
||||
|
||||
#Use SAML standard logout mechanism retrieved from idp metadata
|
||||
#If configured false don't care about SAML session on logout.
|
||||
#Logout from PowerDNS-Admin only and keep SAML session authenticated.
|
||||
SAML_LOGOUT = False
|
||||
#Configure to redirect to a different url then PowerDNS-Admin login after SAML logout
|
||||
#for example redirect to google.com after successful saml logout
|
||||
#SAML_LOGOUT_URL = 'https://google.com'
|
@ -1,124 +1,94 @@
|
||||
import os
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
basedir = os.path.abspath(os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
# BASIC APP CONFIG
|
||||
SECRET_KEY = 'changeme'
|
||||
LOG_LEVEL = 'DEBUG'
|
||||
LOG_FILE = os.path.join(basedir, 'logs/log.txt')
|
||||
### BASIC APP CONFIG
|
||||
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
||||
# TIMEOUT - for large zones
|
||||
TIMEOUT = 10
|
||||
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
|
||||
BIND_ADDRESS = '0.0.0.0'
|
||||
PORT = 9191
|
||||
|
||||
# UPLOAD DIR
|
||||
UPLOAD_DIR = os.path.join(basedir, 'upload')
|
||||
|
||||
# DATABASE CONFIG FOR MYSQL
|
||||
DB_HOST = os.environ.get('PDA_DB_HOST')
|
||||
DB_PORT = os.environ.get('PDA_DB_PORT', 3306 )
|
||||
DB_NAME = os.environ.get('PDA_DB_NAME')
|
||||
DB_USER = os.environ.get('PDA_DB_USER')
|
||||
DB_PASSWORD = os.environ.get('PDA_DB_PASSWORD')
|
||||
#MySQL
|
||||
SQLALCHEMY_DATABASE_URI = 'mysql://'+DB_USER+':'+DB_PASSWORD+'@'+DB_HOST+':'+ str(DB_PORT) + '/'+DB_NAME
|
||||
SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')
|
||||
### DATABASE CONFIG
|
||||
SQLA_DB_USER = 'pda'
|
||||
SQLA_DB_PASSWORD = 'changeme'
|
||||
SQLA_DB_HOST = '127.0.0.1'
|
||||
SQLA_DB_NAME = 'pda'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||
|
||||
# SAML Authentication
|
||||
### DATABASE - MySQL
|
||||
# SQLALCHEMY_DATABASE_URI = 'mysql://' + SQLA_DB_USER + ':' + SQLA_DB_PASSWORD + '@' + SQLA_DB_HOST + '/' + SQLA_DB_NAME
|
||||
|
||||
### DATABASE - SQLite
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
|
||||
|
||||
# SAML Authnetication
|
||||
SAML_ENABLED = False
|
||||
SAML_DEBUG = True
|
||||
SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml')
|
||||
##Example for ADFS Metadata-URL
|
||||
SAML_METADATA_URL = 'https://<hostname>/FederationMetadata/2007-06/FederationMetadata.xml'
|
||||
#Cache Lifetime in Seconds
|
||||
SAML_METADATA_CACHE_LIFETIME = 1
|
||||
# SAML_DEBUG = True
|
||||
# SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml')
|
||||
# ##Example for ADFS Metadata-URL
|
||||
# SAML_METADATA_URL = 'https://<hostname>/FederationMetadata/2007-06/FederationMetadata.xml'
|
||||
# #Cache Lifetime in Seconds
|
||||
# SAML_METADATA_CACHE_LIFETIME = 1
|
||||
|
||||
# SAML SSO binding format to use
|
||||
## Default: library default (urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect)
|
||||
#SAML_IDP_SSO_BINDING = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
||||
# # SAML SSO binding format to use
|
||||
# ## Default: library default (urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect)
|
||||
# #SAML_IDP_SSO_BINDING = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
||||
|
||||
## EntityID of the IdP to use. Only needed if more than one IdP is
|
||||
## in the SAML_METADATA_URL
|
||||
### Default: First (only) IdP in the SAML_METADATA_URL
|
||||
### Example: https://idp.example.edu/idp
|
||||
#SAML_IDP_ENTITY_ID = 'https://idp.example.edu/idp'
|
||||
## NameID format to request
|
||||
### Default: The SAML NameID Format in the metadata if present,
|
||||
### otherwise urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
|
||||
### Example: urn:oid:0.9.2342.19200300.100.1.1
|
||||
#SAML_NAMEID_FORMAT = 'urn:oid:0.9.2342.19200300.100.1.1'
|
||||
# ## EntityID of the IdP to use. Only needed if more than one IdP is
|
||||
# ## in the SAML_METADATA_URL
|
||||
# ### Default: First (only) IdP in the SAML_METADATA_URL
|
||||
# ### Example: https://idp.example.edu/idp
|
||||
# #SAML_IDP_ENTITY_ID = 'https://idp.example.edu/idp'
|
||||
# ## NameID format to request
|
||||
# ### Default: The SAML NameID Format in the metadata if present,
|
||||
# ### otherwise urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
|
||||
# ### Example: urn:oid:0.9.2342.19200300.100.1.1
|
||||
# #SAML_NAMEID_FORMAT = 'urn:oid:0.9.2342.19200300.100.1.1'
|
||||
|
||||
## Attribute to use for Email address
|
||||
### Default: email
|
||||
### Example: urn:oid:0.9.2342.19200300.100.1.3
|
||||
#SAML_ATTRIBUTE_EMAIL = 'urn:oid:0.9.2342.19200300.100.1.3'
|
||||
# ## Attribute to use for Email address
|
||||
# ### Default: email
|
||||
# ### Example: urn:oid:0.9.2342.19200300.100.1.3
|
||||
# #SAML_ATTRIBUTE_EMAIL = 'urn:oid:0.9.2342.19200300.100.1.3'
|
||||
|
||||
## Attribute to use for Given name
|
||||
### Default: givenname
|
||||
### Example: urn:oid:2.5.4.42
|
||||
#SAML_ATTRIBUTE_GIVENNAME = 'urn:oid:2.5.4.42'
|
||||
# ## Attribute to use for Given name
|
||||
# ### Default: givenname
|
||||
# ### Example: urn:oid:2.5.4.42
|
||||
# #SAML_ATTRIBUTE_GIVENNAME = 'urn:oid:2.5.4.42'
|
||||
|
||||
## Attribute to use for Surname
|
||||
### Default: surname
|
||||
### Example: urn:oid:2.5.4.4
|
||||
#SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4'
|
||||
# ## Attribute to use for Surname
|
||||
# ### Default: surname
|
||||
# ### Example: urn:oid:2.5.4.4
|
||||
# #SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4'
|
||||
|
||||
## Split into Given name and Surname
|
||||
## Useful if your IDP only gives a display name
|
||||
### Default: none
|
||||
### Example: http://schemas.microsoft.com/identity/claims/displayname
|
||||
#SAML_ATTRIBUTE_NAME = 'http://schemas.microsoft.com/identity/claims/displayname'
|
||||
# ## Attribute to use for username
|
||||
# ### Default: Use NameID instead
|
||||
# ### Example: urn:oid:0.9.2342.19200300.100.1.1
|
||||
# #SAML_ATTRIBUTE_USERNAME = 'urn:oid:0.9.2342.19200300.100.1.1'
|
||||
|
||||
## Attribute to use for username
|
||||
### Default: Use NameID instead
|
||||
### Example: urn:oid:0.9.2342.19200300.100.1.1
|
||||
#SAML_ATTRIBUTE_USERNAME = 'urn:oid:0.9.2342.19200300.100.1.1'
|
||||
# ## Attribute to get admin status from
|
||||
# ### Default: Don't control admin with SAML attribute
|
||||
# ### Example: https://example.edu/pdns-admin
|
||||
# ### If set, look for the value 'true' to set a user as an administrator
|
||||
# ### If not included in assertion, or set to something other than 'true',
|
||||
# ### the user is set as a non-administrator user.
|
||||
# #SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin'
|
||||
|
||||
## Attribute to get admin status from
|
||||
### Default: Don't control admin with SAML attribute
|
||||
### Example: https://example.edu/pdns-admin
|
||||
### If set, look for the value 'true' to set a user as an administrator
|
||||
### If not included in assertion, or set to something other than 'true',
|
||||
### the user is set as a non-administrator user.
|
||||
#SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin'
|
||||
# ## Attribute to get account names from
|
||||
# ### Default: Don't control accounts with SAML attribute
|
||||
# ### If set, the user will be added and removed from accounts to match
|
||||
# ### what's in the login assertion. Accounts that don't exist will
|
||||
# ### be created and the user added to them.
|
||||
# SAML_ATTRIBUTE_ACCOUNT = 'https://example.edu/pdns-account'
|
||||
|
||||
## Attribute to get group from
|
||||
### Default: Don't use groups from SAML attribute
|
||||
### Example: https://example.edu/pdns-admin-group
|
||||
#SAML_ATTRIBUTE_GROUP = 'https://example.edu/pdns-admin'
|
||||
|
||||
## Group namem to get admin status from
|
||||
### Default: Don't control admin with SAML group
|
||||
### Example: https://example.edu/pdns-admin
|
||||
#SAML_GROUP_ADMIN_NAME = 'powerdns-admin'
|
||||
|
||||
## Attribute to get group to account mappings from
|
||||
### Default: None
|
||||
### If set, the user will be added and removed from accounts to match
|
||||
### what's in the login assertion if they are in the required group
|
||||
#SAML_GROUP_TO_ACCOUNT_MAPPING = 'dev-admins=dev,prod-admins=prod'
|
||||
|
||||
## Attribute to get account names from
|
||||
### Default: Don't control accounts with SAML attribute
|
||||
### If set, the user will be added and removed from accounts to match
|
||||
### what's in the login assertion. Accounts that don't exist will
|
||||
### be created and the user added to them.
|
||||
SAML_ATTRIBUTE_ACCOUNT = 'https://example.edu/pdns-account'
|
||||
|
||||
SAML_SP_ENTITY_ID = 'http://<SAML SP Entity ID>'
|
||||
SAML_SP_CONTACT_NAME = '<contact name>'
|
||||
SAML_SP_CONTACT_MAIL = '<contact mail>'
|
||||
#Configures if SAML tokens should be encrypted.
|
||||
#If enabled a new app certificate will be generated on restart
|
||||
SAML_SIGN_REQUEST = False
|
||||
|
||||
# Configures if you want to request the IDP to sign the message
|
||||
# Default is True
|
||||
#SAML_WANT_MESSAGE_SIGNED = True
|
||||
|
||||
#Use SAML standard logout mechanism retrieved from idp metadata
|
||||
#If configured false don't care about SAML session on logout.
|
||||
#Logout from PowerDNS-Admin only and keep SAML session authenticated.
|
||||
SAML_LOGOUT = False
|
||||
#Configure to redirect to a different url then PowerDNS-Admin login after SAML logout
|
||||
#for example redirect to google.com after successful saml logout
|
||||
#SAML_LOGOUT_URL = 'https://google.com'
|
||||
# SAML_SP_ENTITY_ID = 'http://<SAML SP Entity ID>'
|
||||
# SAML_SP_CONTACT_NAME = '<contact name>'
|
||||
# SAML_SP_CONTACT_MAIL = '<contact mail>'
|
||||
# #Cofigures if SAML tokens should be encrypted.
|
||||
# #If enabled a new app certificate will be generated on restart
|
||||
# SAML_SIGN_REQUEST = False
|
||||
# #Use SAML standard logout mechanism retreived from idp metadata
|
||||
# #If configured false don't care about SAML session on logout.
|
||||
# #Logout from PowerDNS-Admin only and keep SAML session authenticated.
|
||||
# SAML_LOGOUT = False
|
||||
# #Configure to redirect to a different url then PowerDNS-Admin login after SAML logout
|
||||
# #for example redirect to google.com after successful saml logout
|
||||
# #SAML_LOGOUT_URL = 'https://google.com'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# defaults for Docker image
|
||||
# Defaults for Docker image
|
||||
BIND_ADDRESS='0.0.0.0'
|
||||
PORT=80
|
||||
|
||||
@ -6,11 +6,8 @@ legal_envvars = (
|
||||
'SECRET_KEY',
|
||||
'BIND_ADDRESS',
|
||||
'PORT',
|
||||
'TIMEOUT',
|
||||
'LOG_LEVEL',
|
||||
'LOG_FILE',
|
||||
'SALT',
|
||||
'UPLOAD_DIR',
|
||||
'SQLALCHEMY_TRACK_MODIFICATIONS',
|
||||
'SQLALCHEMY_DATABASE_URI',
|
||||
'SAML_ENABLED',
|
||||
@ -42,12 +39,12 @@ legal_envvars = (
|
||||
|
||||
legal_envvars_int = (
|
||||
'PORT',
|
||||
'TIMEOUT',
|
||||
'SAML_METADATA_CACHE_LIFETIME',
|
||||
)
|
||||
|
||||
legal_envvars_bool = (
|
||||
'SQLALCHEMY_TRACK_MODIFICATIONS',
|
||||
'HSTS_ENABLED',
|
||||
'SAML_ENABLED',
|
||||
'SAML_DEBUG',
|
||||
'SAML_SIGN_REQUEST',
|
||||
@ -66,5 +63,3 @@ for v in legal_envvars:
|
||||
if v in legal_envvars_int:
|
||||
ret = int(ret)
|
||||
sys.modules[__name__].__dict__[v] = ret
|
||||
|
||||
|
136
configs/test.py
136
configs/test.py
@ -1,131 +1,25 @@
|
||||
import os
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
basedir = os.path.abspath(os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
# BASIC APP CONFIG
|
||||
SECRET_KEY = 'changeme'
|
||||
LOG_LEVEL = 'DEBUG'
|
||||
LOG_FILE = os.path.join(basedir, 'logs/log.txt')
|
||||
### BASIC APP CONFIG
|
||||
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
||||
# TIMEOUT - for large zones
|
||||
TIMEOUT = 10
|
||||
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
|
||||
BIND_ADDRESS = '0.0.0.0'
|
||||
PORT = 9191
|
||||
HSTS_ENABLED = False
|
||||
|
||||
# UPLOAD DIR
|
||||
UPLOAD_DIR = os.path.join(basedir, 'upload')
|
||||
TEST_USER_PASSWORD = 'test'
|
||||
TEST_USER = 'test'
|
||||
TEST_ADMIN_USER = 'admin'
|
||||
TEST_ADMIN_PASSWORD = 'admin'
|
||||
TEST_USER_APIKEY = 'wewdsfewrfsfsdf'
|
||||
TEST_ADMIN_APIKEY = 'nghnbnhtghrtert'
|
||||
# DATABASE CONFIG FOR MYSQL
|
||||
# DB_HOST = os.environ.get('PDA_DB_HOST')
|
||||
# DB_PORT = os.environ.get('PDA_DB_PORT', 3306 )
|
||||
# DB_NAME = os.environ.get('PDA_DB_NAME')
|
||||
# DB_USER = os.environ.get('PDA_DB_USER')
|
||||
# DB_PASSWORD = os.environ.get('PDA_DB_PASSWORD')
|
||||
# #MySQL
|
||||
# SQLALCHEMY_DATABASE_URI = 'mysql://'+DB_USER+':'+DB_PASSWORD+'@'+DB_HOST+':'+ str(DB_PORT) + '/'+DB_NAME
|
||||
# SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')
|
||||
### DATABASE - SQLite
|
||||
TEST_DB_LOCATION = '/tmp/testing.sqlite'
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///{0}'.format(TEST_DB_LOCATION)
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
# SAML Authentication
|
||||
# SAML Authnetication
|
||||
SAML_ENABLED = False
|
||||
SAML_DEBUG = True
|
||||
SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml')
|
||||
##Example for ADFS Metadata-URL
|
||||
SAML_METADATA_URL = 'https://<hostname>/FederationMetadata/2007-06/FederationMetadata.xml'
|
||||
#Cache Lifetime in Seconds
|
||||
SAML_METADATA_CACHE_LIFETIME = 1
|
||||
|
||||
# SAML SSO binding format to use
|
||||
## Default: library default (urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect)
|
||||
#SAML_IDP_SSO_BINDING = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
||||
|
||||
## EntityID of the IdP to use. Only needed if more than one IdP is
|
||||
## in the SAML_METADATA_URL
|
||||
### Default: First (only) IdP in the SAML_METADATA_URL
|
||||
### Example: https://idp.example.edu/idp
|
||||
#SAML_IDP_ENTITY_ID = 'https://idp.example.edu/idp'
|
||||
## NameID format to request
|
||||
### Default: The SAML NameID Format in the metadata if present,
|
||||
### otherwise urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
|
||||
### Example: urn:oid:0.9.2342.19200300.100.1.1
|
||||
#SAML_NAMEID_FORMAT = 'urn:oid:0.9.2342.19200300.100.1.1'
|
||||
|
||||
## Attribute to use for Email address
|
||||
### Default: email
|
||||
### Example: urn:oid:0.9.2342.19200300.100.1.3
|
||||
#SAML_ATTRIBUTE_EMAIL = 'urn:oid:0.9.2342.19200300.100.1.3'
|
||||
|
||||
## Attribute to use for Given name
|
||||
### Default: givenname
|
||||
### Example: urn:oid:2.5.4.42
|
||||
#SAML_ATTRIBUTE_GIVENNAME = 'urn:oid:2.5.4.42'
|
||||
|
||||
## Attribute to use for Surname
|
||||
### Default: surname
|
||||
### Example: urn:oid:2.5.4.4
|
||||
#SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4'
|
||||
|
||||
## Split into Given name and Surname
|
||||
## Useful if your IDP only gives a display name
|
||||
### Default: none
|
||||
### Example: http://schemas.microsoft.com/identity/claims/displayname
|
||||
#SAML_ATTRIBUTE_NAME = 'http://schemas.microsoft.com/identity/claims/displayname'
|
||||
|
||||
## Attribute to use for username
|
||||
### Default: Use NameID instead
|
||||
### Example: urn:oid:0.9.2342.19200300.100.1.1
|
||||
#SAML_ATTRIBUTE_USERNAME = 'urn:oid:0.9.2342.19200300.100.1.1'
|
||||
|
||||
## Attribute to get admin status from
|
||||
### Default: Don't control admin with SAML attribute
|
||||
### Example: https://example.edu/pdns-admin
|
||||
### If set, look for the value 'true' to set a user as an administrator
|
||||
### If not included in assertion, or set to something other than 'true',
|
||||
### the user is set as a non-administrator user.
|
||||
#SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin'
|
||||
|
||||
## Attribute to get group from
|
||||
### Default: Don't use groups from SAML attribute
|
||||
### Example: https://example.edu/pdns-admin-group
|
||||
#SAML_ATTRIBUTE_GROUP = 'https://example.edu/pdns-admin'
|
||||
|
||||
## Group namem to get admin status from
|
||||
### Default: Don't control admin with SAML group
|
||||
### Example: https://example.edu/pdns-admin
|
||||
#SAML_GROUP_ADMIN_NAME = 'powerdns-admin'
|
||||
|
||||
## Attribute to get group to account mappings from
|
||||
### Default: None
|
||||
### If set, the user will be added and removed from accounts to match
|
||||
### what's in the login assertion if they are in the required group
|
||||
#SAML_GROUP_TO_ACCOUNT_MAPPING = 'dev-admins=dev,prod-admins=prod'
|
||||
|
||||
## Attribute to get account names from
|
||||
### Default: Don't control accounts with SAML attribute
|
||||
### If set, the user will be added and removed from accounts to match
|
||||
### what's in the login assertion. Accounts that don't exist will
|
||||
### be created and the user added to them.
|
||||
SAML_ATTRIBUTE_ACCOUNT = 'https://example.edu/pdns-account'
|
||||
|
||||
SAML_SP_ENTITY_ID = 'http://<SAML SP Entity ID>'
|
||||
SAML_SP_CONTACT_NAME = '<contact name>'
|
||||
SAML_SP_CONTACT_MAIL = '<contact mail>'
|
||||
#Configures if SAML tokens should be encrypted.
|
||||
#If enabled a new app certificate will be generated on restart
|
||||
SAML_SIGN_REQUEST = False
|
||||
|
||||
# Configures if you want to request the IDP to sign the message
|
||||
# Default is True
|
||||
#SAML_WANT_MESSAGE_SIGNED = True
|
||||
|
||||
#Use SAML standard logout mechanism retrieved from idp metadata
|
||||
#If configured false don't care about SAML session on logout.
|
||||
#Logout from PowerDNS-Admin only and keep SAML session authenticated.
|
||||
SAML_LOGOUT = False
|
||||
#Configure to redirect to a different url then PowerDNS-Admin login after SAML logout
|
||||
#for example redirect to google.com after successful saml logout
|
||||
#SAML_LOGOUT_URL = 'https://google.com'
|
||||
# TEST SAMPLE DATA
|
||||
TEST_USER = 'test'
|
||||
TEST_USER_PASSWORD = 'test'
|
||||
TEST_ADMIN_USER = 'admin'
|
||||
TEST_ADMIN_PASSWORD = 'admin'
|
||||
TEST_USER_APIKEY = 'wewdsfewrfsfsdf'
|
||||
TEST_ADMIN_APIKEY = 'nghnbnhtghrtert'
|
@ -4,47 +4,31 @@ services:
|
||||
powerdns-admin:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/PowerDNS-Admin/Dockerfile.test
|
||||
args:
|
||||
- ENVIRONMENT=test
|
||||
dockerfile: docker-test/Dockerfile
|
||||
image: powerdns-admin-test
|
||||
env_file:
|
||||
- ./env-test
|
||||
container_name: powerdns-admin-test
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
ports:
|
||||
- "9191:9191"
|
||||
volumes:
|
||||
# Code
|
||||
- .:/powerdns-admin/
|
||||
- "./configs/test.py:/powerdns-admin/config.py"
|
||||
- powerdns-admin-assets3:/powerdns-admin/logs
|
||||
- ./app/static/custom:/powerdns-admin/app/static/custom
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: 50m
|
||||
- "9191:80"
|
||||
networks:
|
||||
- default
|
||||
env_file:
|
||||
- ./docker-test/env
|
||||
depends_on:
|
||||
- pdns-server
|
||||
|
||||
pdns-server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/PowerDNS-Admin/Dockerfile.pdns.test
|
||||
dockerfile: docker-test/Dockerfile.pdns
|
||||
image: pdns-server-test
|
||||
ports:
|
||||
- "5053:53"
|
||||
- "5053:53/udp"
|
||||
- "8081:8081"
|
||||
networks:
|
||||
- default
|
||||
env_file:
|
||||
- ./env-test
|
||||
- ./docker-test/env
|
||||
|
||||
networks:
|
||||
default:
|
||||
|
||||
volumes:
|
||||
powerdns-admin-assets3:
|
||||
|
@ -1,114 +1,20 @@
|
||||
version: "2.1"
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
powerdns-admin:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/PowerDNS-Admin/Dockerfile
|
||||
args:
|
||||
- ENVIRONMENT=${ENVIRONMENT}
|
||||
image: powerdns-admin
|
||||
container_name: powerdns-admin
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
dockerfile: docker/Dockerfile
|
||||
image: powerdns-admin:latest
|
||||
container_name: powerdns_admin
|
||||
ports:
|
||||
- "9191:9191"
|
||||
volumes:
|
||||
# Code
|
||||
- .:/powerdns-admin/
|
||||
- "./configs/${ENVIRONMENT}.py:/powerdns-admin/config.py"
|
||||
# Assets dir volume
|
||||
- powerdns-admin-assets:/powerdns-admin/app/static
|
||||
- powerdns-admin-assets2:/powerdns-admin/node_modules
|
||||
- powerdns-admin-assets3:/powerdns-admin/logs
|
||||
- ./app/static/custom:/powerdns-admin/app/static/custom
|
||||
- "9191:80"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: 50m
|
||||
networks:
|
||||
- default
|
||||
environment:
|
||||
- ENVIRONMENT=${ENVIRONMENT}
|
||||
- PDA_DB_HOST=${PDA_DB_HOST}
|
||||
- PDA_DB_NAME=${PDA_DB_NAME}
|
||||
- PDA_DB_USER=${PDA_DB_USER}
|
||||
- PDA_DB_PASSWORD=${PDA_DB_PASSWORD}
|
||||
- PDA_DB_PORT=${PDA_DB_PORT}
|
||||
- PDNS_HOST=${PDNS_HOST}
|
||||
- PDNS_API_KEY=${PDNS_API_KEY}
|
||||
- FLASK_APP=/powerdns-admin/app/__init__.py
|
||||
depends_on:
|
||||
powerdns-admin-mysql:
|
||||
condition: service_healthy
|
||||
|
||||
powerdns-admin-mysql:
|
||||
image: mysql/mysql-server:5.7
|
||||
hostname: ${PDA_DB_HOST}
|
||||
container_name: powerdns-admin-mysql
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
expose:
|
||||
- 3306
|
||||
volumes:
|
||||
- powerdns-admin-mysql-data:/var/lib/mysql
|
||||
networks:
|
||||
- default
|
||||
environment:
|
||||
- MYSQL_DATABASE=${PDA_DB_NAME}
|
||||
- MYSQL_USER=${PDA_DB_USER}
|
||||
- MYSQL_PASSWORD=${PDA_DB_PASSWORD}
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
pdns-server:
|
||||
image: psitrax/powerdns
|
||||
hostname: ${PDNS_HOST}
|
||||
ports:
|
||||
- "5053:53"
|
||||
- "5053:53/udp"
|
||||
networks:
|
||||
- default
|
||||
command: --api=yes --api-key=${PDNS_API_KEY} --webserver-address=0.0.0.0 --webserver-allow-from=0.0.0.0/0
|
||||
environment:
|
||||
- MYSQL_HOST=${PDNS_DB_HOST}
|
||||
- MYSQL_USER=${PDNS_DB_USER}
|
||||
- MYSQL_PASS=${PDNS_DB_PASSWORD}
|
||||
- PDNS_API_KEY=${PDNS_API_KEY}
|
||||
- PDNS_WEBSERVER_ALLOW_FROM=${PDNS_WEBSERVER_ALLOW_FROM}
|
||||
depends_on:
|
||||
pdns-mysql:
|
||||
condition: service_healthy
|
||||
|
||||
pdns-mysql:
|
||||
image: mysql/mysql-server:5.7
|
||||
hostname: ${PDNS_DB_HOST}
|
||||
container_name: ${PDNS_DB_HOST}
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
expose:
|
||||
- 3306
|
||||
volumes:
|
||||
- powerdns-mysql-data:/var/lib/mysql
|
||||
networks:
|
||||
- default
|
||||
environment:
|
||||
- MYSQL_DATABASE=${PDNS_DB_NAME}
|
||||
- MYSQL_USER=${PDNS_DB_USER}
|
||||
- MYSQL_PASSWORD=${PDNS_DB_PASSWORD}
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
networks:
|
||||
default:
|
||||
|
||||
volumes:
|
||||
powerdns-mysql-data:
|
||||
powerdns-admin-mysql-data:
|
||||
powerdns-admin-assets:
|
||||
powerdns-admin-assets2:
|
||||
powerdns-admin-assets3:
|
||||
- SQLALCHEMY_DATABASE_URI=mysql://pda:changeme@host.docker.internal/pda
|
||||
- GUINCORN_TIMEOUT=60
|
||||
- GUNICORN_WORKERS=2
|
||||
- GUNICORN_LOGLEVEL=DEBUG
|
||||
|
33
docker-test/Dockerfile
Normal file
33
docker-test/Dockerfile
Normal file
@ -0,0 +1,33 @@
|
||||
FROM debian:stretch-slim
|
||||
LABEL maintainer="k@ndk.name"
|
||||
|
||||
ENV LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8
|
||||
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y --no-install-recommends apt-transport-https locales locales-all python3-pip python3-setuptools python3-dev curl libsasl2-dev libldap2-dev libssl-dev libxml2-dev libxslt1-dev libxmlsec1-dev libffi-dev build-essential libmariadb-dev-compat \
|
||||
&& curl -sL https://deb.nodesource.com/setup_10.x | bash - \
|
||||
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||
&& apt-get update -y \
|
||||
&& apt-get install -y nodejs yarn \
|
||||
&& apt-get clean -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# We copy just the requirements.txt first to leverage Docker cache
|
||||
COPY ./requirements.txt /app/requirements.txt
|
||||
|
||||
WORKDIR /app
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
COPY . /app
|
||||
COPY ./docker/entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
ENV FLASK_APP=powerdnsadmin/__init__.py
|
||||
RUN yarn install --pure-lockfile --production \
|
||||
&& yarn cache clean \
|
||||
&& flask assets build
|
||||
|
||||
COPY ./docker-test/wait-for-pdns.sh /opt
|
||||
RUN chmod u+x /opt/wait-for-pdns.sh
|
||||
CMD ["/opt/wait-for-pdns.sh", "/usr/local/bin/pytest","--capture=no","-vv"]
|
@ -2,8 +2,8 @@ FROM ubuntu:latest
|
||||
|
||||
RUN apt-get update && apt-get install -y pdns-backend-sqlite3 pdns-server sqlite3
|
||||
|
||||
COPY ./docker/PowerDNS-Admin/pdns.sqlite.sql /data/pdns.sql
|
||||
ADD ./docker/PowerDNS-Admin/start.sh /data/
|
||||
COPY ./docker-test/pdns.sqlite.sql /data/pdns.sql
|
||||
ADD ./docker-test/start.sh /data/
|
||||
|
||||
RUN rm -f /etc/powerdns/pdns.d/pdns.simplebind.conf
|
||||
RUN rm -f /etc/powerdns/pdns.d/bind.conf
|
5
docker-test/env
Normal file
5
docker-test/env
Normal file
@ -0,0 +1,5 @@
|
||||
PDNS_PROTO=http
|
||||
PDNS_PORT=8081
|
||||
PDNS_HOST=pdns-server
|
||||
PDNS_API_KEY=changeme
|
||||
PDNS_WEBSERVER_ALLOW_FROM=0.0.0.0/0
|
33
docker/Dockerfile
Normal file
33
docker/Dockerfile
Normal file
@ -0,0 +1,33 @@
|
||||
FROM debian:stretch-slim
|
||||
LABEL maintainer="k@ndk.name"
|
||||
|
||||
ENV LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8
|
||||
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y --no-install-recommends apt-transport-https locales locales-all python3-pip python3-setuptools python3-dev curl libsasl2-dev libldap2-dev libssl-dev libxml2-dev libxslt1-dev libxmlsec1-dev libffi-dev build-essential libmariadb-dev-compat \
|
||||
&& curl -sL https://deb.nodesource.com/setup_10.x | bash - \
|
||||
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||
&& apt-get update -y \
|
||||
&& apt-get install -y nodejs yarn \
|
||||
&& apt-get clean -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# We copy just the requirements.txt first to leverage Docker cache
|
||||
COPY ./requirements.txt /app/requirements.txt
|
||||
|
||||
WORKDIR /app
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
COPY . /app
|
||||
COPY ./docker/entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
ENV FLASK_APP=powerdnsadmin/__init__.py
|
||||
RUN yarn install --pure-lockfile --production \
|
||||
&& yarn cache clean \
|
||||
&& flask assets build
|
||||
|
||||
EXPOSE 80/tcp
|
||||
ENTRYPOINT ["entrypoint.sh"]
|
||||
CMD ["gunicorn","powerdnsadmin:create_app()"]
|
@ -1,48 +0,0 @@
|
||||
FROM ubuntu:16.04
|
||||
MAINTAINER Khanh Ngo "k@ndk.name"
|
||||
ARG ENVIRONMENT=development
|
||||
ENV ENVIRONMENT=${ENVIRONMENT}
|
||||
WORKDIR /powerdns-admin
|
||||
|
||||
RUN apt-get update -y
|
||||
RUN apt-get install -y apt-transport-https
|
||||
|
||||
RUN apt-get install -y locales locales-all
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US.UTF-8
|
||||
|
||||
RUN apt-get install -y python3-pip python3-dev supervisor curl mysql-client
|
||||
|
||||
# Install node 10.x
|
||||
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
|
||||
|
||||
# Install yarn
|
||||
RUN apt-get update -y
|
||||
RUN apt-get install -y yarn
|
||||
|
||||
# Install Netcat for DB healthcheck
|
||||
RUN apt-get install -y netcat
|
||||
|
||||
# lib for building mysql db driver
|
||||
RUN apt-get install -y libmysqlclient-dev
|
||||
|
||||
# lib for building ldap and ssl-based application
|
||||
RUN apt-get install -y libsasl2-dev libldap2-dev libssl-dev
|
||||
|
||||
# lib for building python3-saml
|
||||
RUN apt-get install -y libxml2-dev libxslt1-dev libxmlsec1-dev libffi-dev pkg-config
|
||||
|
||||
COPY ./requirements.txt /powerdns-admin/requirements.txt
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
ADD ./supervisord.conf /etc/supervisord.conf
|
||||
ADD . /powerdns-admin/
|
||||
COPY ./configs/${ENVIRONMENT}.py /powerdns-admin/config.py
|
||||
COPY ./docker/PowerDNS-Admin/entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
@ -1,46 +0,0 @@
|
||||
FROM ubuntu:16.04
|
||||
MAINTAINER Khanh Ngo "k@ndk.name"
|
||||
ARG ENVIRONMENT=development
|
||||
ENV ENVIRONMENT=${ENVIRONMENT}
|
||||
WORKDIR /powerdns-admin
|
||||
|
||||
RUN apt-get update -y
|
||||
RUN apt-get install -y apt-transport-https
|
||||
|
||||
RUN apt-get install -y locales locales-all
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US.UTF-8
|
||||
|
||||
RUN apt-get install -y python3-pip python3-dev supervisor curl mysql-client
|
||||
|
||||
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
|
||||
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
|
||||
|
||||
# Install yarn
|
||||
RUN apt-get update -y
|
||||
RUN apt-get install -y yarn
|
||||
|
||||
# Install Netcat for DB healthcheck
|
||||
RUN apt-get install -y netcat
|
||||
|
||||
# lib for building mysql db driver
|
||||
RUN apt-get install -y libmysqlclient-dev
|
||||
|
||||
# lib for building ldap and ssl-based application
|
||||
RUN apt-get install -y libsasl2-dev libldap2-dev libssl-dev
|
||||
|
||||
# lib for building python3-saml
|
||||
RUN apt-get install -y libxml2-dev libxslt1-dev libxmlsec1-dev libffi-dev pkg-config
|
||||
|
||||
COPY ./requirements.txt /powerdns-admin/requirements.txt
|
||||
COPY ./docker/PowerDNS-Admin/wait-for-pdns.sh /opt
|
||||
RUN chmod u+x /opt/wait-for-pdns.sh
|
||||
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
CMD ["/opt/wait-for-pdns.sh", "/usr/local/bin/pytest","--capture=no","-vv"]
|
@ -1,71 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
|
||||
# == Vars
|
||||
#
|
||||
DB_MIGRATION_DIR='/powerdns-admin/migrations'
|
||||
if [[ -z ${PDNS_PROTO} ]];
|
||||
then PDNS_PROTO="http"
|
||||
fi
|
||||
|
||||
if [[ -z ${PDNS_PORT} ]];
|
||||
then PDNS_PORT=8081
|
||||
fi
|
||||
|
||||
|
||||
|
||||
# Wait for us to be able to connect to MySQL before proceeding
|
||||
echo "===> Waiting for $PDA_DB_HOST MySQL service"
|
||||
until nc -zv \
|
||||
$PDA_DB_HOST \
|
||||
$PDA_DB_PORT;
|
||||
do
|
||||
echo "MySQL ($PDA_DB_HOST) is unavailable - sleeping"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
|
||||
echo "===> DB management"
|
||||
# Go in Workdir
|
||||
cd /powerdns-admin
|
||||
|
||||
if [ ! -d "${DB_MIGRATION_DIR}" ]; then
|
||||
echo "---> Running DB Init"
|
||||
flask db init --directory ${DB_MIGRATION_DIR}
|
||||
flask db migrate -m "Init DB" --directory ${DB_MIGRATION_DIR}
|
||||
flask db upgrade --directory ${DB_MIGRATION_DIR}
|
||||
./init_data.py
|
||||
|
||||
else
|
||||
echo "---> Running DB Migration"
|
||||
set +e
|
||||
flask db migrate -m "Upgrade DB Schema" --directory ${DB_MIGRATION_DIR}
|
||||
flask db upgrade --directory ${DB_MIGRATION_DIR}
|
||||
set -e
|
||||
fi
|
||||
|
||||
echo "===> Update PDNS API connection info"
|
||||
# initial setting if not available in the DB
|
||||
mysql -h${PDA_DB_HOST} -u${PDA_DB_USER} -p${PDA_DB_PASSWORD} -P${PDA_DB_PORT} ${PDA_DB_NAME} -e "INSERT INTO setting (name, value) SELECT * FROM (SELECT 'pdns_api_url', '${PDNS_PROTO}://${PDNS_HOST}:${PDNS_PORT}') AS tmp WHERE NOT EXISTS (SELECT name FROM setting WHERE name = 'pdns_api_url') LIMIT 1;"
|
||||
mysql -h${PDA_DB_HOST} -u${PDA_DB_USER} -p${PDA_DB_PASSWORD} -P${PDA_DB_PORT} ${PDA_DB_NAME} -e "INSERT INTO setting (name, value) SELECT * FROM (SELECT 'pdns_api_key', '${PDNS_API_KEY}') AS tmp WHERE NOT EXISTS (SELECT name FROM setting WHERE name = 'pdns_api_key') LIMIT 1;"
|
||||
|
||||
# update pdns api setting if .env is changed.
|
||||
mysql -h${PDA_DB_HOST} -u${PDA_DB_USER} -p${PDA_DB_PASSWORD} -P${PDA_DB_PORT} ${PDA_DB_NAME} -e "UPDATE setting SET value='${PDNS_PROTO}://${PDNS_HOST}:${PDNS_PORT}' WHERE name='pdns_api_url';"
|
||||
mysql -h${PDA_DB_HOST} -u${PDA_DB_USER} -p${PDA_DB_PASSWORD} -P${PDA_DB_PORT} ${PDA_DB_NAME} -e "UPDATE setting SET value='${PDNS_API_KEY}' WHERE name='pdns_api_key';"
|
||||
|
||||
echo "===> Assets management"
|
||||
echo "---> Running Yarn"
|
||||
chown -R www-data:www-data /powerdns-admin/app/static
|
||||
chown -R www-data:www-data /powerdns-admin/node_modules
|
||||
su -s /bin/bash -c 'yarn install --pure-lockfile' www-data
|
||||
|
||||
echo "---> Running Flask assets"
|
||||
chown -R www-data:www-data /powerdns-admin/logs
|
||||
su -s /bin/bash -c 'flask assets build' www-data
|
||||
|
||||
|
||||
echo "===> Start supervisor"
|
||||
/usr/bin/supervisord -c /etc/supervisord.conf
|
@ -1,32 +0,0 @@
|
||||
FROM debian:stretch-slim
|
||||
LABEL maintainer="k@ndk.name"
|
||||
|
||||
ENV LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 LANGUAGE=en_US.UTF-8
|
||||
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y --no-install-recommends apt-transport-https locales locales-all python3-pip python3-setuptools python3-dev curl libsasl2-dev libldap2-dev libssl-dev libxml2-dev libxslt1-dev libxmlsec1-dev libffi-dev build-essential libmariadb-dev-compat \
|
||||
&& curl -sL https://deb.nodesource.com/setup_10.x | bash - \
|
||||
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
||||
&& apt-get update -y \
|
||||
&& apt-get install -y nodejs yarn \
|
||||
&& apt-get clean -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /opt/powerdns-admin
|
||||
|
||||
COPY . .
|
||||
RUN pip3 install -r requirements.txt \
|
||||
&& pip3 install psycopg2-binary \
|
||||
&& yarn install --pure-lockfile \
|
||||
&& cp config_template.py config.py \
|
||||
&& flask assets build \
|
||||
&& rm config.py
|
||||
|
||||
COPY ./docker/Production/entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
ENV FLASK_APP=app/__init__.py
|
||||
EXPOSE 80/tcp
|
||||
ENTRYPOINT ["entrypoint.sh"]
|
||||
CMD ["gunicorn","app:app"]
|
@ -1,15 +1,13 @@
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
cd /opt/powerdns-admin
|
||||
cd /app
|
||||
|
||||
GUNICORN_TIMEOUT="${GUINCORN_TIMEOUT:-120}"
|
||||
GUNICORN_WORKERS="${GUNICORN_WORKERS:-4}"
|
||||
GUNICORN_LOGLEVEL="${GUNICORN_LOGLEVEL:-info}"
|
||||
BIND_ADDRESS="${BIND_ADDRESS:-0.0.0.0:80}"
|
||||
|
||||
if [ ! -f ./config.py ]; then
|
||||
cat ./config_template.py ./docker/Production/config_docker.py > ./config.py
|
||||
fi
|
||||
cat ./powerdnsadmin/default_config.py ./configs/docker_config.py > ./powerdnsadmin/docker_config.py
|
||||
|
||||
GUNICORN_ARGS="-t ${GUNICORN_TIMEOUT} --workers ${GUNICORN_WORKERS} --bind ${BIND_ADDRESS} --log-level ${GUNICORN_LOGLEVEL}"
|
||||
if [ "$1" == gunicorn ]; then
|
105
docs/API.md
Normal file
105
docs/API.md
Normal file
@ -0,0 +1,105 @@
|
||||
### API Usage
|
||||
|
||||
1. Run docker image docker-compose up, go to UI http://localhost:9191, at http://localhost:9191/swagger is swagger API specification
|
||||
2. Click to register user, type e.g. user: admin and password: admin
|
||||
3. Login to UI in settings enable allow domain creation for users, now you can create and manage domains with admin account and also ordinary users
|
||||
4. Encode your user and password to base64, in our example we have user admin and password admin so in linux cmd line we type:
|
||||
|
||||
```
|
||||
$ echo -n 'admin:admin'|base64
|
||||
YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
we use generated output in basic authentication, we authenticate as user,
|
||||
with basic authentication, we can create/delete/get zone and create/delete/get/update apikeys
|
||||
|
||||
creating domain:
|
||||
|
||||
```
|
||||
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/zones --data '{"name": "yourdomain.com.", "kind": "NATIVE", "nameservers": ["ns1.mydomain.com."]}'
|
||||
```
|
||||
|
||||
creating apikey which has Administrator role, apikey can have also User role, when creating such apikey you have to specify also domain for which apikey is valid:
|
||||
|
||||
```
|
||||
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/apikeys --data '{"description": "masterkey","domains":[], "role": "Administrator"}'
|
||||
```
|
||||
|
||||
call above will return response like this:
|
||||
|
||||
```
|
||||
[{"description": "samekey", "domains": [], "role": {"name": "Administrator", "id": 1}, "id": 2, "plain_key": "aGCthP3KLAeyjZI"}]
|
||||
```
|
||||
|
||||
we take plain_key and base64 encode it, this is the only time we can get API key in plain text and save it somewhere:
|
||||
|
||||
```
|
||||
$ echo -n 'aGCthP3KLAeyjZI'|base64
|
||||
YUdDdGhQM0tMQWV5alpJ
|
||||
```
|
||||
|
||||
We can use apikey for all calls specified in our API specification (it tries to follow powerdns API 1:1, only tsigkeys endpoints are not yet implemented), don't forget to specify Content-Type!
|
||||
|
||||
getting powerdns configuration:
|
||||
|
||||
```
|
||||
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/config
|
||||
```
|
||||
|
||||
creating and updating records:
|
||||
|
||||
```
|
||||
curl -X PATCH -H 'Content-Type: application/json' --data '{"rrsets": [{"name": "test1.yourdomain.com.","type": "A","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "192.0.2.5", "disabled": false} ]},{"name": "test2.yourdomain.com.","type": "AAAA","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "2001:db8::6", "disabled": false} ]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://127.0.0.1:9191/api/v1/servers/localhost/zones/yourdomain.com.
|
||||
```
|
||||
|
||||
getting domain:
|
||||
|
||||
```
|
||||
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
||||
```
|
||||
|
||||
list zone records:
|
||||
|
||||
```
|
||||
curl -H 'Content-Type: application/json' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
||||
```
|
||||
|
||||
add new record:
|
||||
|
||||
```
|
||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.5.4", "disabled": false } ] } ] }' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
||||
```
|
||||
|
||||
update record:
|
||||
|
||||
```
|
||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.2.5", "disabled": false, "name": "test.yourdomain.com.", "ttl": 86400, "type": "A"}]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
||||
```
|
||||
|
||||
delete record:
|
||||
|
||||
```
|
||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "DELETE"}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq
|
||||
```
|
||||
|
||||
### Generate ER diagram
|
||||
|
||||
```
|
||||
apt-get install python-dev graphviz libgraphviz-dev pkg-config
|
||||
```
|
||||
|
||||
```
|
||||
pip install graphviz mysqlclient ERAlchemy
|
||||
```
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
```
|
||||
source .env
|
||||
```
|
||||
|
||||
```
|
||||
eralchemy -i 'mysql://${PDA_DB_USER}:${PDA_DB_PASSWORD}@'$(docker inspect powerdns-admin-mysql|jq -jr '.[0].NetworkSettings.Networks.powerdnsadmin_default.IPAddress')':3306/powerdns_admin' -o /tmp/output.pdf
|
||||
```
|
24
docs/running_tests.md
Normal file
24
docs/running_tests.md
Normal file
@ -0,0 +1,24 @@
|
||||
### Running tests
|
||||
|
||||
**NOTE:** Tests will create `__pycache__` folders which will be owned by root, which might be issue during rebuild
|
||||
|
||||
thus (e.g. invalid tar headers message) when such situation occurs, you need to remove those folders as root
|
||||
|
||||
1. Build images
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-test.yml build
|
||||
```
|
||||
|
||||
2. Run tests
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-test.yml up
|
||||
```
|
||||
|
||||
3. To teardown the test environment
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-test.yml down
|
||||
docker-compose -f docker-compose-test.yml rm
|
||||
```
|
10
env-test
10
env-test
@ -1,10 +0,0 @@
|
||||
PDNS_DB_HOST=pdns-mysql
|
||||
PDNS_DB_NAME=pdns
|
||||
PDNS_DB_USER=pdns
|
||||
PDNS_DB_PASSWORD=changeme
|
||||
|
||||
PDNS_PROTO=http
|
||||
PDNS_PORT=8081
|
||||
PDNS_HOST=pdns-server
|
||||
PDNS_API_KEY=changeme
|
||||
PDNS_WEBSERVER_ALLOW_FROM=0.0.0.0/0
|
20
init_data.py
20
init_data.py
@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from app import db
|
||||
from app.models import Role, DomainTemplate
|
||||
|
||||
admin_role = Role(name='Administrator', description='Administrator')
|
||||
user_role = Role(name='User', description='User')
|
||||
|
||||
template_1 = DomainTemplate(name='basic_template_1', description='Basic Template #1')
|
||||
template_2 = DomainTemplate(name='basic_template_2', description='Basic Template #2')
|
||||
template_3 = DomainTemplate(name='basic_template_3', description='Basic Template #3')
|
||||
|
||||
db.session.add(admin_role)
|
||||
db.session.add(user_role)
|
||||
|
||||
db.session.add(template_1)
|
||||
db.session.add(template_2)
|
||||
db.session.add(template_3)
|
||||
|
||||
db.session.commit()
|
29
migrations/versions/0fb6d23a4863_remove_user_avatar.py
Normal file
29
migrations/versions/0fb6d23a4863_remove_user_avatar.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""Remove user avatar
|
||||
|
||||
Revision ID: 0fb6d23a4863
|
||||
Revises: 654298797277
|
||||
Create Date: 2019-12-02 10:29:41.945044
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0fb6d23a4863'
|
||||
down_revision = '654298797277'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user') as batch_op:
|
||||
batch_op.drop_column('avatar')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('user', sa.Column('avatar', mysql.VARCHAR(length=128), nullable=True))
|
||||
# ### end Alembic commands ###
|
82
powerdnsadmin/__init__.py
Executable file
82
powerdnsadmin/__init__.py
Executable file
@ -0,0 +1,82 @@
|
||||
import os
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from flask import Flask
|
||||
from flask_seasurf import SeaSurf
|
||||
|
||||
from .lib import utils
|
||||
|
||||
|
||||
def create_app(config=None):
|
||||
from . import models, routes, services
|
||||
from .assets import assets
|
||||
app = Flask(__name__)
|
||||
|
||||
# Proxy
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||
|
||||
# CSRF protection
|
||||
csrf = SeaSurf(app)
|
||||
csrf.exempt(routes.index.dyndns_checkip)
|
||||
csrf.exempt(routes.index.dyndns_update)
|
||||
csrf.exempt(routes.index.saml_authorized)
|
||||
csrf.exempt(routes.api.api_login_create_zone)
|
||||
csrf.exempt(routes.api.api_login_delete_zone)
|
||||
csrf.exempt(routes.api.api_generate_apikey)
|
||||
csrf.exempt(routes.api.api_delete_apikey)
|
||||
csrf.exempt(routes.api.api_update_apikey)
|
||||
csrf.exempt(routes.api.api_zone_subpath_forward)
|
||||
csrf.exempt(routes.api.api_zone_forward)
|
||||
csrf.exempt(routes.api.api_create_zone)
|
||||
|
||||
# Load config from env variables if using docker
|
||||
if os.path.exists(os.path.join(app.root_path, 'docker_config.py')):
|
||||
app.config.from_object('powerdnsadmin.docker_config')
|
||||
else:
|
||||
# Load default configuration
|
||||
app.config.from_object('powerdnsadmin.default_config')
|
||||
|
||||
# Load config file from FLASK_CONF env variable
|
||||
if 'FLASK_CONF' in os.environ:
|
||||
app.config.from_envvar('FLASK_CONF')
|
||||
|
||||
# Load app sepecified configuration
|
||||
if config is not None:
|
||||
if isinstance(config, dict):
|
||||
app.config.update(config)
|
||||
elif config.endswith('.py'):
|
||||
app.config.from_pyfile(config)
|
||||
|
||||
# HSTS
|
||||
if app.config.get('HSTS_ENABLED'):
|
||||
from flask_sslify import SSLify
|
||||
_sslify = SSLify(app)
|
||||
|
||||
# Load app's components
|
||||
assets.init_app(app)
|
||||
models.init_app(app)
|
||||
routes.init_app(app)
|
||||
services.init_app(app)
|
||||
|
||||
# Register filters
|
||||
app.jinja_env.filters['display_record_name'] = utils.display_record_name
|
||||
app.jinja_env.filters['display_master_name'] = utils.display_master_name
|
||||
app.jinja_env.filters['display_second_to_time'] = utils.display_time
|
||||
app.jinja_env.filters[
|
||||
'email_to_gravatar_url'] = utils.email_to_gravatar_url
|
||||
app.jinja_env.filters[
|
||||
'display_setting_state'] = utils.display_setting_state
|
||||
|
||||
# Register context proccessors
|
||||
from .models.setting import Setting
|
||||
|
||||
@app.context_processor
|
||||
def inject_sitename():
|
||||
setting = Setting().get('site_name')
|
||||
return dict(SITE_NAME=setting)
|
||||
|
||||
@app.context_processor
|
||||
def inject_setting():
|
||||
setting = Setting()
|
||||
return dict(SETTING=setting)
|
||||
|
||||
return app
|
@ -1,12 +1,12 @@
|
||||
from functools import wraps
|
||||
from flask import g, redirect, url_for, request, abort
|
||||
|
||||
from app.models import Setting
|
||||
from .models import User, ApiKey
|
||||
import base64
|
||||
from app.lib.log import logging
|
||||
from app.errors import RequestIsNotJSON, NotEnoughPrivileges
|
||||
from app.errors import DomainAccessForbidden
|
||||
import binascii
|
||||
from functools import wraps
|
||||
from flask import g, redirect, url_for, request, abort, current_app, render_template
|
||||
from flask_login import current_user
|
||||
|
||||
from .models import User, ApiKey, Setting, Domain
|
||||
from .lib.errors import RequestIsNotJSON, NotEnoughPrivileges
|
||||
from .lib.errors import DomainAccessForbidden
|
||||
|
||||
|
||||
def admin_role_required(f):
|
||||
@ -15,9 +15,10 @@ def admin_role_required(f):
|
||||
"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if g.user.role.name != 'Administrator':
|
||||
return redirect(url_for('error', code=401))
|
||||
if current_user.role.name != 'Administrator':
|
||||
abort(403)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
@ -27,9 +28,10 @@ def operator_role_required(f):
|
||||
"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if g.user.role.name not in ['Administrator', 'Operator']:
|
||||
return redirect(url_for('error', code=401))
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
abort(403)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
@ -42,14 +44,21 @@ def can_access_domain(f):
|
||||
"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if g.user.role.name not in ['Administrator', 'Operator']:
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
domain_name = kwargs.get('domain_name')
|
||||
user_domain = [d.name for d in g.user.get_domain()]
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
|
||||
if domain_name not in user_domain:
|
||||
return redirect(url_for('error', code=401))
|
||||
if not domain:
|
||||
abort(404)
|
||||
|
||||
valid_access = Domain(id=domain.id).is_valid_access(
|
||||
current_user.id)
|
||||
|
||||
if not valid_access:
|
||||
abort(403)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
@ -61,10 +70,13 @@ def can_configure_dnssec(f):
|
||||
"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if g.user.role.name not in ['Administrator', 'Operator'] and Setting().get('dnssec_admins_only'):
|
||||
return redirect(url_for('error', code=401))
|
||||
if current_user.role.name not in [
|
||||
'Administrator', 'Operator'
|
||||
] and Setting().get('dnssec_admins_only'):
|
||||
abort(403)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
@ -76,9 +88,12 @@ def can_create_domain(f):
|
||||
"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if g.user.role.name not in ['Administrator', 'Operator'] and not Setting().get('allow_user_create_domain'):
|
||||
return redirect(url_for('error', code=401))
|
||||
if current_user.role.name not in [
|
||||
'Administrator', 'Operator'
|
||||
] and not Setting().get('allow_user_create_domain'):
|
||||
abort(403)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
@ -92,38 +107,40 @@ def api_basic_auth(f):
|
||||
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:
|
||||
logging.error('Error: {0}'.format(e))
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(401)
|
||||
|
||||
user = User(
|
||||
username=username,
|
||||
password=password,
|
||||
plain_text_password=password
|
||||
)
|
||||
user = User(username=username,
|
||||
password=password,
|
||||
plain_text_password=password)
|
||||
|
||||
try:
|
||||
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
|
||||
)
|
||||
auth = user.is_validate(method=auth_method,
|
||||
src_ip=request.remote_addr)
|
||||
|
||||
if not auth:
|
||||
logging.error('Checking user password failed')
|
||||
current_app.logger.error('Checking user password failed')
|
||||
abort(401)
|
||||
else:
|
||||
user = User.query.filter(User.username == username).first()
|
||||
g.user = user
|
||||
current_user = user
|
||||
except Exception as e:
|
||||
logging.error('Error: {0}'.format(e))
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(401)
|
||||
else:
|
||||
logging.error('Error: Authorization header missing!')
|
||||
current_app.logger.error('Error: Authorization header missing!')
|
||||
abort(401)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
@ -137,6 +154,7 @@ def is_json(f):
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def api_can_create_domain(f):
|
||||
"""
|
||||
Grant access if:
|
||||
@ -145,11 +163,14 @@ def api_can_create_domain(f):
|
||||
"""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if g.user.role.name not in ['Administrator', 'Operator'] and not Setting().get('allow_user_create_domain'):
|
||||
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"
|
||||
logging.error(msg.format(g.user.username))
|
||||
current_app.logger.error(msg.format(current_user.username))
|
||||
raise NotEnoughPrivileges()
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
@ -161,9 +182,10 @@ def apikey_is_admin(f):
|
||||
def decorated_function(*args, **kwargs):
|
||||
if g.apikey.role.name != 'Administrator':
|
||||
msg = "Apikey {0} does not have enough privileges to create domain"
|
||||
logging.error(msg.format(g.apikey.id))
|
||||
current_app.logger.error(msg.format(g.apikey.id))
|
||||
raise NotEnoughPrivileges()
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
@ -179,6 +201,7 @@ def apikey_can_access_domain(f):
|
||||
if zone_id not in domain_names:
|
||||
raise DomainAccessForbidden()
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
@ -189,29 +212,41 @@ 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))
|
||||
abort(401)
|
||||
except TypeError as e:
|
||||
logging.error('Error: {0}'.format(e))
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(401)
|
||||
|
||||
apikey = ApiKey(
|
||||
key=apikey_val
|
||||
)
|
||||
apikey = ApiKey(key=apikey_val)
|
||||
apikey.plain_text_password = apikey_val
|
||||
|
||||
try:
|
||||
auth_method = 'LOCAL'
|
||||
auth = apikey.is_validate(
|
||||
method=auth_method,
|
||||
src_ip=request.remote_addr
|
||||
)
|
||||
auth = apikey.is_validate(method=auth_method,
|
||||
src_ip=request.remote_addr)
|
||||
|
||||
g.apikey = auth
|
||||
except Exception as e:
|
||||
logging.error('Error: {0}'.format(e))
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(401)
|
||||
else:
|
||||
logging.error('Error: API key header missing!')
|
||||
current_app.logger.error('Error: API key header missing!')
|
||||
abort(401)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def dyndns_login_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if current_user.is_authenticated is False:
|
||||
return render_template('dyndns.html', response='badauth'), 200
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
25
powerdnsadmin/default_config.py
Normal file
25
powerdnsadmin/default_config.py
Normal file
@ -0,0 +1,25 @@
|
||||
import os
|
||||
basedir = os.path.abspath(os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
### BASIC APP CONFIG
|
||||
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
||||
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
|
||||
BIND_ADDRESS = '0.0.0.0'
|
||||
PORT = 9191
|
||||
HSTS_ENABLED = False
|
||||
|
||||
### DATABASE CONFIG
|
||||
SQLA_DB_USER = 'pda'
|
||||
SQLA_DB_PASSWORD = 'changeme'
|
||||
SQLA_DB_HOST = '127.0.0.1'
|
||||
SQLA_DB_NAME = 'pda'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||
|
||||
### DATBASE - MySQL
|
||||
SQLALCHEMY_DATABASE_URI = 'mysql://'+SQLA_DB_USER+':'+SQLA_DB_PASSWORD+'@'+SQLA_DB_HOST+'/'+SQLA_DB_NAME
|
||||
|
||||
### DATABSE - SQLite
|
||||
# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
|
||||
|
||||
# SAML Authnetication
|
||||
SAML_ENABLED = False
|
@ -27,6 +27,15 @@ class DomainNotExists(StructuredException):
|
||||
self.name = name
|
||||
|
||||
|
||||
class DomainAlreadyExists(StructuredException):
|
||||
status_code = 409
|
||||
|
||||
def __init__(self, name=None, message="Domain already exists"):
|
||||
StructuredException.__init__(self)
|
||||
self.message = message
|
||||
self.name = name
|
||||
|
||||
|
||||
class DomainAccessForbidden(StructuredException):
|
||||
status_code = 403
|
||||
|
||||
@ -49,7 +58,7 @@ class ApiKeyNotUsable(StructuredException):
|
||||
status_code = 400
|
||||
|
||||
def __init__(self, name=None, message="Api key must have domains or have \
|
||||
administrative role"):
|
||||
administrative role" ):
|
||||
StructuredException.__init__(self)
|
||||
self.message = message
|
||||
self.name = name
|
@ -1,10 +1,8 @@
|
||||
from app.models import Setting
|
||||
import requests
|
||||
from flask import request
|
||||
import logging as logger
|
||||
from urllib.parse import urljoin
|
||||
from flask import request, current_app
|
||||
|
||||
logging = logger.getLogger(__name__)
|
||||
from ..models import Setting
|
||||
|
||||
|
||||
def forward_request():
|
||||
@ -17,13 +15,13 @@ def forward_request():
|
||||
|
||||
if request.method != 'GET' and request.method != 'DELETE':
|
||||
msg = msg_str.format(request.get_json(force=True))
|
||||
logging.debug(msg)
|
||||
current_app.logger.debug(msg)
|
||||
data = request.get_json(force=True)
|
||||
|
||||
verify = False
|
||||
|
||||
headers = {
|
||||
'user-agent': 'powerdnsadmin/0',
|
||||
'user-agent': 'powerdns-admin/api',
|
||||
'pragma': 'no-cache',
|
||||
'cache-control': 'no-cache',
|
||||
'accept': 'application/json; q=1',
|
||||
@ -32,12 +30,10 @@ def forward_request():
|
||||
|
||||
url = urljoin(pdns_api_url, request.path)
|
||||
|
||||
resp = requests.request(
|
||||
request.method,
|
||||
url,
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
json=data
|
||||
)
|
||||
resp = requests.request(request.method,
|
||||
url,
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
json=data)
|
||||
|
||||
return resp
|
@ -5,60 +5,13 @@ import hashlib
|
||||
import ipaddress
|
||||
import os
|
||||
|
||||
from app import app
|
||||
# from app import app
|
||||
from distutils.version import StrictVersion
|
||||
from urllib.parse import urlparse
|
||||
from datetime import datetime, timedelta
|
||||
from threading import Thread
|
||||
|
||||
from .certutil import KEY_FILE, CERT_FILE
|
||||
import logging as logger
|
||||
|
||||
logging = logger.getLogger(__name__)
|
||||
|
||||
|
||||
if app.config['SAML_ENABLED']:
|
||||
from onelogin.saml2.auth import OneLogin_Saml2_Auth
|
||||
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
|
||||
idp_timestamp = datetime(1970, 1, 1)
|
||||
idp_data = None
|
||||
if 'SAML_IDP_ENTITY_ID' in app.config:
|
||||
idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None), required_sso_binding=app.config['SAML_IDP_SSO_BINDING'])
|
||||
else:
|
||||
idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None))
|
||||
if idp_data is None:
|
||||
print('SAML: IDP Metadata initial load failed')
|
||||
exit(-1)
|
||||
idp_timestamp = datetime.now()
|
||||
|
||||
|
||||
def get_idp_data():
|
||||
global idp_data, idp_timestamp
|
||||
lifetime = timedelta(minutes=app.config['SAML_METADATA_CACHE_LIFETIME'])
|
||||
if idp_timestamp+lifetime < datetime.now():
|
||||
background_thread = Thread(target=retrieve_idp_data)
|
||||
background_thread.start()
|
||||
return idp_data
|
||||
|
||||
|
||||
def retrieve_idp_data():
|
||||
global idp_data, idp_timestamp
|
||||
if 'SAML_IDP_SSO_BINDING' in app.config:
|
||||
new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None), required_sso_binding=app.config['SAML_IDP_SSO_BINDING'])
|
||||
else:
|
||||
new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None))
|
||||
if new_idp_data is not None:
|
||||
idp_data = new_idp_data
|
||||
idp_timestamp = datetime.now()
|
||||
print("SAML: IDP Metadata successfully retrieved from: " + app.config['SAML_METADATA_URL'])
|
||||
else:
|
||||
print("SAML: IDP Metadata could not be retrieved")
|
||||
|
||||
|
||||
if 'TIMEOUT' in app.config.keys():
|
||||
TIMEOUT = app.config['TIMEOUT']
|
||||
else:
|
||||
TIMEOUT = 10
|
||||
|
||||
|
||||
def auth_from_url(url):
|
||||
@ -70,13 +23,16 @@ def auth_from_url(url):
|
||||
return auth
|
||||
|
||||
|
||||
def fetch_remote(remote_url, method='GET', data=None, accept=None, params=None, timeout=None, headers=None):
|
||||
def fetch_remote(remote_url,
|
||||
method='GET',
|
||||
data=None,
|
||||
accept=None,
|
||||
params=None,
|
||||
timeout=None,
|
||||
headers=None):
|
||||
if data is not None and type(data) != str:
|
||||
data = json.dumps(data)
|
||||
|
||||
if timeout is None:
|
||||
timeout = TIMEOUT
|
||||
|
||||
verify = False
|
||||
|
||||
our_headers = {
|
||||
@ -89,29 +45,31 @@ def fetch_remote(remote_url, method='GET', data=None, accept=None, params=None,
|
||||
if headers is not None:
|
||||
our_headers.update(headers)
|
||||
|
||||
r = requests.request(
|
||||
method,
|
||||
remote_url,
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
auth=auth_from_url(remote_url),
|
||||
timeout=timeout,
|
||||
data=data,
|
||||
params=params
|
||||
)
|
||||
r = requests.request(method,
|
||||
remote_url,
|
||||
headers=headers,
|
||||
verify=verify,
|
||||
auth=auth_from_url(remote_url),
|
||||
timeout=timeout,
|
||||
data=data,
|
||||
params=params)
|
||||
try:
|
||||
if r.status_code not in (200, 201, 204, 400, 422):
|
||||
if r.status_code not in (200, 201, 204, 400, 409, 422):
|
||||
r.raise_for_status()
|
||||
except Exception as e:
|
||||
msg = "Returned status {0} and content {1}"
|
||||
logging.error(msg.format(r.status_code, r.content))
|
||||
raise RuntimeError('Error while fetching {0}'.format(remote_url))
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def fetch_json(remote_url, method='GET', data=None, params=None, headers=None):
|
||||
r = fetch_remote(remote_url, method=method, data=data, params=params, headers=headers,
|
||||
def fetch_json(remote_url, method='GET', data=None, params=None, headers=None, timeout=None):
|
||||
r = fetch_remote(remote_url,
|
||||
method=method,
|
||||
data=data,
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
accept='application/json; q=1')
|
||||
|
||||
if method == "DELETE":
|
||||
@ -119,11 +77,14 @@ def fetch_json(remote_url, method='GET', data=None, params=None, headers=None):
|
||||
|
||||
if r.status_code == 204:
|
||||
return {}
|
||||
elif r.status_code == 409:
|
||||
return {'error': 'Resource already exists or conflict', 'http_code': r.status_code}
|
||||
|
||||
try:
|
||||
assert('json' in r.headers['content-type'])
|
||||
assert ('json' in r.headers['content-type'])
|
||||
except Exception as e:
|
||||
raise RuntimeError('Error while fetching {0}'.format(remote_url)) from e
|
||||
raise RuntimeError(
|
||||
'Error while fetching {0}'.format(remote_url)) from e
|
||||
|
||||
# don't use r.json here, as it will read from r.text, which will trigger
|
||||
# content encoding auto-detection in almost all cases, WHICH IS EXTREMELY
|
||||
@ -132,7 +93,8 @@ def fetch_json(remote_url, method='GET', data=None, params=None, headers=None):
|
||||
try:
|
||||
data = json.loads(r.content.decode('utf-8'))
|
||||
except Exception as e:
|
||||
raise RuntimeError('Error while loading JSON data from {0}'.format(remote_url)) from e
|
||||
raise RuntimeError(
|
||||
'Error while loading JSON data from {0}'.format(remote_url)) from e
|
||||
return data
|
||||
|
||||
|
||||
@ -157,14 +119,14 @@ def display_time(amount, units='s', remove_seconds=True):
|
||||
Convert timestamp to normal time format
|
||||
"""
|
||||
amount = int(amount)
|
||||
INTERVALS = [(lambda mlsec:divmod(mlsec, 1000), 'ms'),
|
||||
(lambda seconds:divmod(seconds, 60), 's'),
|
||||
(lambda minutes:divmod(minutes, 60), 'm'),
|
||||
(lambda hours:divmod(hours, 24), 'h'),
|
||||
(lambda days:divmod(days, 7), 'D'),
|
||||
(lambda weeks:divmod(weeks, 4), 'W'),
|
||||
(lambda years:divmod(years, 12), 'M'),
|
||||
(lambda decades:divmod(decades, 10), 'Y')]
|
||||
INTERVALS = [(lambda mlsec: divmod(mlsec, 1000), 'ms'),
|
||||
(lambda seconds: divmod(seconds, 60), 's'),
|
||||
(lambda minutes: divmod(minutes, 60), 'm'),
|
||||
(lambda hours: divmod(hours, 24), 'h'),
|
||||
(lambda days: divmod(days, 7), 'D'),
|
||||
(lambda weeks: divmod(weeks, 4), 'W'),
|
||||
(lambda years: divmod(years, 12), 'M'),
|
||||
(lambda decades: divmod(decades, 10), 'Y')]
|
||||
|
||||
for index_start, (interval, unit) in enumerate(INTERVALS):
|
||||
if unit == units:
|
||||
@ -173,13 +135,14 @@ def display_time(amount, units='s', remove_seconds=True):
|
||||
amount_abrev = []
|
||||
last_index = 0
|
||||
amount_temp = amount
|
||||
for index, (formula, abrev) in enumerate(INTERVALS[index_start: len(INTERVALS)]):
|
||||
for index, (formula,
|
||||
abrev) in enumerate(INTERVALS[index_start:len(INTERVALS)]):
|
||||
divmod_result = formula(amount_temp)
|
||||
amount_temp = divmod_result[0]
|
||||
amount_abrev.append((divmod_result[1], abrev))
|
||||
if divmod_result[1] > 0:
|
||||
last_index = index
|
||||
amount_abrev_partial = amount_abrev[0: last_index + 1]
|
||||
amount_abrev_partial = amount_abrev[0:last_index + 1]
|
||||
amount_abrev_partial.reverse()
|
||||
|
||||
final_string = ''
|
||||
@ -241,50 +204,65 @@ def init_saml_auth(req):
|
||||
settings = {}
|
||||
settings['sp'] = {}
|
||||
if 'SAML_NAMEID_FORMAT' in app.config:
|
||||
settings['sp']['NameIDFormat'] = app.config['SAML_NAMEID_FORMAT']
|
||||
settings['sp']['NameIDFormat'] = app.config['SAML_NAMEID_FORMAT']
|
||||
else:
|
||||
settings['sp']['NameIDFormat'] = idp_data.get('sp', {}).get('NameIDFormat', 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified')
|
||||
settings['sp']['NameIDFormat'] = idp_data.get('sp', {}).get(
|
||||
'NameIDFormat',
|
||||
'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified')
|
||||
settings['sp']['entityId'] = app.config['SAML_SP_ENTITY_ID']
|
||||
if os.path.isfile(CERT_FILE):
|
||||
cert = open(CERT_FILE, "r").readlines()
|
||||
settings['sp']['x509cert'] = "".join(cert)
|
||||
cert = open(CERT_FILE, "r").readlines()
|
||||
settings['sp']['x509cert'] = "".join(cert)
|
||||
if os.path.isfile(KEY_FILE):
|
||||
key = open(KEY_FILE, "r").readlines()
|
||||
settings['sp']['privateKey'] = "".join(key)
|
||||
key = open(KEY_FILE, "r").readlines()
|
||||
settings['sp']['privateKey'] = "".join(key)
|
||||
settings['sp']['assertionConsumerService'] = {}
|
||||
settings['sp']['assertionConsumerService']['binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
||||
settings['sp']['assertionConsumerService']['url'] = own_url+'/saml/authorized'
|
||||
settings['sp']['assertionConsumerService'][
|
||||
'binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
||||
settings['sp']['assertionConsumerService'][
|
||||
'url'] = own_url + '/saml/authorized'
|
||||
settings['sp']['attributeConsumingService'] = {}
|
||||
settings['sp']['singleLogoutService'] = {}
|
||||
settings['sp']['singleLogoutService']['binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||
settings['sp']['singleLogoutService']['url'] = own_url+'/saml/sls'
|
||||
settings['sp']['singleLogoutService'][
|
||||
'binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||
settings['sp']['singleLogoutService']['url'] = own_url + '/saml/sls'
|
||||
settings['idp'] = metadata['idp']
|
||||
settings['strict'] = True
|
||||
settings['debug'] = app.config['SAML_DEBUG']
|
||||
settings['security'] = {}
|
||||
settings['security']['digestAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
|
||||
settings['security'][
|
||||
'digestAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
|
||||
settings['security']['metadataCacheDuration'] = None
|
||||
settings['security']['metadataValidUntil'] = None
|
||||
settings['security']['requestedAuthnContext'] = True
|
||||
settings['security']['signatureAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
|
||||
settings['security'][
|
||||
'signatureAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
|
||||
settings['security']['wantAssertionsEncrypted'] = False
|
||||
settings['security']['wantAttributeStatement'] = True
|
||||
settings['security']['wantNameId'] = True
|
||||
settings['security']['authnRequestsSigned'] = app.config['SAML_SIGN_REQUEST']
|
||||
settings['security']['logoutRequestSigned'] = app.config['SAML_SIGN_REQUEST']
|
||||
settings['security']['logoutResponseSigned'] = app.config['SAML_SIGN_REQUEST']
|
||||
settings['security']['authnRequestsSigned'] = app.config[
|
||||
'SAML_SIGN_REQUEST']
|
||||
settings['security']['logoutRequestSigned'] = app.config[
|
||||
'SAML_SIGN_REQUEST']
|
||||
settings['security']['logoutResponseSigned'] = app.config[
|
||||
'SAML_SIGN_REQUEST']
|
||||
settings['security']['nameIdEncrypted'] = False
|
||||
settings['security']['signMetadata'] = True
|
||||
settings['security']['wantAssertionsSigned'] = True
|
||||
settings['security']['wantMessagesSigned'] = app.config.get('SAML_WANT_MESSAGE_SIGNED', True)
|
||||
settings['security']['wantMessagesSigned'] = app.config.get(
|
||||
'SAML_WANT_MESSAGE_SIGNED', True)
|
||||
settings['security']['wantNameIdEncrypted'] = False
|
||||
settings['contactPerson'] = {}
|
||||
settings['contactPerson']['support'] = {}
|
||||
settings['contactPerson']['support']['emailAddress'] = app.config['SAML_SP_CONTACT_NAME']
|
||||
settings['contactPerson']['support']['givenName'] = app.config['SAML_SP_CONTACT_MAIL']
|
||||
settings['contactPerson']['support']['emailAddress'] = app.config[
|
||||
'SAML_SP_CONTACT_NAME']
|
||||
settings['contactPerson']['support']['givenName'] = app.config[
|
||||
'SAML_SP_CONTACT_MAIL']
|
||||
settings['contactPerson']['technical'] = {}
|
||||
settings['contactPerson']['technical']['emailAddress'] = app.config['SAML_SP_CONTACT_NAME']
|
||||
settings['contactPerson']['technical']['givenName'] = app.config['SAML_SP_CONTACT_MAIL']
|
||||
settings['contactPerson']['technical']['emailAddress'] = app.config[
|
||||
'SAML_SP_CONTACT_NAME']
|
||||
settings['contactPerson']['technical']['givenName'] = app.config[
|
||||
'SAML_SP_CONTACT_MAIL']
|
||||
settings['organization'] = {}
|
||||
settings['organization']['en-US'] = {}
|
||||
settings['organization']['en-US']['displayname'] = 'PowerDNS-Admin'
|
||||
@ -304,11 +282,20 @@ def display_setting_state(value):
|
||||
|
||||
|
||||
def validate_ipaddress(address):
|
||||
try:
|
||||
ip = ipaddress.ip_address(address)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if isinstance(ip, (ipaddress.IPv4Address, ipaddress.IPv6Address)):
|
||||
return [ip]
|
||||
return []
|
||||
try:
|
||||
ip = ipaddress.ip_address(address)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if isinstance(ip, (ipaddress.IPv4Address, ipaddress.IPv6Address)):
|
||||
return [ip]
|
||||
return []
|
||||
|
||||
|
||||
class customBoxes:
|
||||
boxes = {
|
||||
"reverse": (" ", " "),
|
||||
"ip6arpa": ("ip6", "%.ip6.arpa"),
|
||||
"inaddrarpa": ("in-addr", "%.in-addr.arpa")
|
||||
}
|
||||
order = ["reverse", "ip6arpa", "inaddrarpa"]
|
@ -24,7 +24,7 @@ bravado_config = {
|
||||
'use_models': True,
|
||||
}
|
||||
|
||||
dir_path = os.path.dirname(os.path.abspath(__file__))
|
||||
dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
spec_path = os.path.join(dir_path, "swagger-spec.yaml")
|
||||
spec_dict = get_swagger_spec(spec_path)
|
||||
spec = Spec.from_dict(spec_dict, config=bravado_config)
|
23
powerdnsadmin/models/__init__.py
Normal file
23
powerdnsadmin/models/__init__.py
Normal file
@ -0,0 +1,23 @@
|
||||
from flask_migrate import Migrate
|
||||
|
||||
from .base import db
|
||||
from .user import User
|
||||
from .role import Role
|
||||
from .account import Account
|
||||
from .account_user import AccountUser
|
||||
from .server import Server
|
||||
from .history import History
|
||||
from .api_key import ApiKey
|
||||
from .setting import Setting
|
||||
from .domain import Domain
|
||||
from .domain_setting import DomainSetting
|
||||
from .domain_user import DomainUser
|
||||
from .domain_template import DomainTemplate
|
||||
from .domain_template_record import DomainTemplateRecord
|
||||
from .record import Record
|
||||
from .record_entry import RecordEntry
|
||||
|
||||
|
||||
def init_app(app):
|
||||
db.init_app(app)
|
||||
_migrate = Migrate(app, db)
|
200
powerdnsadmin/models/account.py
Normal file
200
powerdnsadmin/models/account.py
Normal file
@ -0,0 +1,200 @@
|
||||
from .base import db
|
||||
from .user import User
|
||||
from .account_user import AccountUser
|
||||
|
||||
|
||||
class Account(db.Model):
|
||||
__tablename__ = 'account'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(40), index=True, unique=True, nullable=False)
|
||||
description = db.Column(db.String(128))
|
||||
contact = db.Column(db.String(128))
|
||||
mail = db.Column(db.String(128))
|
||||
domains = db.relationship("Domain", back_populates="account")
|
||||
|
||||
def __init__(self, name=None, description=None, contact=None, mail=None):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.contact = contact
|
||||
self.mail = mail
|
||||
|
||||
if self.name is not None:
|
||||
self.name = ''.join(c for c in self.name.lower()
|
||||
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
|
||||
|
||||
def __repr__(self):
|
||||
return '<Account {0}r>'.format(self.name)
|
||||
|
||||
def get_name_by_id(self, account_id):
|
||||
"""
|
||||
Convert account_id to account_name
|
||||
"""
|
||||
account = Account.query.filter(Account.id == account_id).first()
|
||||
if account is None:
|
||||
return ''
|
||||
|
||||
return account.name
|
||||
|
||||
def get_id_by_name(self, account_name):
|
||||
"""
|
||||
Convert account_name to account_id
|
||||
"""
|
||||
# Skip actual database lookup for empty queries
|
||||
if account_name is None or account_name == "":
|
||||
return None
|
||||
|
||||
account = Account.query.filter(Account.name == account_name).first()
|
||||
if account is None:
|
||||
return None
|
||||
|
||||
return account.id
|
||||
|
||||
def create_account(self):
|
||||
"""
|
||||
Create a new account
|
||||
"""
|
||||
# Sanity check - account name
|
||||
if self.name == "":
|
||||
return {'status': False, 'msg': 'No account name specified'}
|
||||
|
||||
# check that account name is not already used
|
||||
account = Account.query.filter(Account.name == self.name).first()
|
||||
if account:
|
||||
return {'status': False, 'msg': 'Account already exists'}
|
||||
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
return {'status': True, 'msg': 'Account created successfully'}
|
||||
|
||||
def update_account(self):
|
||||
"""
|
||||
Update an existing account
|
||||
"""
|
||||
# Sanity check - account name
|
||||
if self.name == "":
|
||||
return {'status': False, 'msg': 'No account name specified'}
|
||||
|
||||
# read account and check that it exists
|
||||
account = Account.query.filter(Account.name == self.name).first()
|
||||
if not account:
|
||||
return {'status': False, 'msg': 'Account does not exist'}
|
||||
|
||||
account.description = self.description
|
||||
account.contact = self.contact
|
||||
account.mail = self.mail
|
||||
|
||||
db.session.commit()
|
||||
return {'status': True, 'msg': 'Account updated successfully'}
|
||||
|
||||
def delete_account(self):
|
||||
"""
|
||||
Delete an account
|
||||
"""
|
||||
# unassociate all users first
|
||||
self.grant_privileges([])
|
||||
|
||||
try:
|
||||
Account.query.filter(Account.name == self.name).delete()
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logging.error(
|
||||
'Cannot delete account {0} from DB. DETAIL: {1}'.format(
|
||||
self.username, e))
|
||||
return False
|
||||
|
||||
def get_user(self):
|
||||
"""
|
||||
Get users (id) associated with this account
|
||||
"""
|
||||
user_ids = []
|
||||
query = db.session.query(
|
||||
AccountUser,
|
||||
Account).filter(User.id == AccountUser.user_id).filter(
|
||||
Account.id == AccountUser.account_id).filter(
|
||||
Account.name == self.name).all()
|
||||
for q in query:
|
||||
user_ids.append(q[0].user_id)
|
||||
return user_ids
|
||||
|
||||
def grant_privileges(self, new_user_list):
|
||||
"""
|
||||
Reconfigure account_user table
|
||||
"""
|
||||
account_id = self.get_id_by_name(self.name)
|
||||
|
||||
account_user_ids = self.get_user()
|
||||
new_user_ids = [
|
||||
u.id
|
||||
for u in User.query.filter(User.username.in_(new_user_list)).all()
|
||||
] if new_user_list else []
|
||||
|
||||
removed_ids = list(set(account_user_ids).difference(new_user_ids))
|
||||
added_ids = list(set(new_user_ids).difference(account_user_ids))
|
||||
|
||||
try:
|
||||
for uid in removed_ids:
|
||||
AccountUser.query.filter(AccountUser.user_id == uid).filter(
|
||||
AccountUser.account_id == account_id).delete()
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logging.error(
|
||||
'Cannot revoke user privileges on account {0}. DETAIL: {1}'.
|
||||
format(self.name, e))
|
||||
|
||||
try:
|
||||
for uid in added_ids:
|
||||
au = AccountUser(account_id, uid)
|
||||
db.session.add(au)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logging.error(
|
||||
'Cannot grant user privileges to account {0}. DETAIL: {1}'.
|
||||
format(self.name, e))
|
||||
|
||||
def revoke_privileges_by_id(self, user_id):
|
||||
"""
|
||||
Remove a single user from privilege list based on user_id
|
||||
"""
|
||||
new_uids = [u for u in self.get_user() if u != user_id]
|
||||
users = []
|
||||
for uid in new_uids:
|
||||
users.append(User(id=uid).get_user_info_by_id().username)
|
||||
|
||||
self.grant_privileges(users)
|
||||
|
||||
def add_user(self, user):
|
||||
"""
|
||||
Add a single user to Account by User
|
||||
"""
|
||||
try:
|
||||
au = AccountUser(self.id, user.id)
|
||||
db.session.add(au)
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logging.error(
|
||||
'Cannot add user privileges on account {0}. DETAIL: {1}'.
|
||||
format(self.name, e))
|
||||
return False
|
||||
|
||||
def remove_user(self, user):
|
||||
"""
|
||||
Remove a single user from Account by User
|
||||
"""
|
||||
# TODO: This func is currently used by SAML feature in a wrong way. Fix it
|
||||
try:
|
||||
AccountUser.query.filter(AccountUser.user_id == user.id).filter(
|
||||
AccountUser.account_id == self.id).delete()
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logging.error(
|
||||
'Cannot revoke user privileges on account {0}. DETAIL: {1}'.
|
||||
format(self.name, e))
|
||||
return False
|
17
powerdnsadmin/models/account_user.py
Normal file
17
powerdnsadmin/models/account_user.py
Normal file
@ -0,0 +1,17 @@
|
||||
from .base import db
|
||||
|
||||
|
||||
class AccountUser(db.Model):
|
||||
__tablename__ = 'account_user'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
account_id = db.Column(db.Integer,
|
||||
db.ForeignKey('account.id'),
|
||||
nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
|
||||
def __init__(self, account_id, user_id):
|
||||
self.account_id = account_id
|
||||
self.user_id = user_id
|
||||
|
||||
def __repr__(self):
|
||||
return '<Account_User {0} {1}>'.format(self.account_id, self.user_id)
|
114
powerdnsadmin/models/api_key.py
Normal file
114
powerdnsadmin/models/api_key.py
Normal file
@ -0,0 +1,114 @@
|
||||
import random
|
||||
import string
|
||||
import bcrypt
|
||||
from flask import current_app
|
||||
|
||||
from .base import db, domain_apikey
|
||||
from ..models.role import Role
|
||||
from ..models.domain import Domain
|
||||
|
||||
|
||||
class ApiKey(db.Model):
|
||||
__tablename__ = "apikey"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
key = db.Column(db.String(255), unique=True, nullable=False)
|
||||
description = db.Column(db.String(255))
|
||||
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
|
||||
role = db.relationship('Role', back_populates="apikeys", lazy=True)
|
||||
domains = db.relationship("Domain",
|
||||
secondary=domain_apikey,
|
||||
back_populates="apikeys")
|
||||
|
||||
def __init__(self, key=None, desc=None, role_name=None, domains=[]):
|
||||
self.id = None
|
||||
self.description = desc
|
||||
self.role_name = role_name
|
||||
self.domains[:] = domains
|
||||
if not key:
|
||||
rand_key = ''.join(
|
||||
random.choice(string.ascii_letters + string.digits)
|
||||
for _ in range(15))
|
||||
self.plain_key = rand_key
|
||||
self.key = self.get_hashed_password(rand_key).decode('utf-8')
|
||||
current_app.logger.debug("Hashed key: {0}".format(self.key))
|
||||
else:
|
||||
self.key = key
|
||||
|
||||
def create(self):
|
||||
try:
|
||||
self.role = Role.query.filter(Role.name == self.role_name).first()
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
current_app.logger.error('Can not update api key table. Error: {0}'.format(e))
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
def delete(self):
|
||||
try:
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
msg_str = 'Can not delete api key template. Error: {0}'
|
||||
current_app.logger.error(msg_str.format(e))
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
def update(self, role_name=None, description=None, domains=None):
|
||||
try:
|
||||
if role_name:
|
||||
role = Role.query.filter(Role.name == role_name).first()
|
||||
self.role_id = role.id
|
||||
|
||||
if description:
|
||||
self.description = description
|
||||
|
||||
if domains:
|
||||
domain_object_list = Domain.query \
|
||||
.filter(Domain.name.in_(domains)) \
|
||||
.all()
|
||||
self.domains[:] = domain_object_list
|
||||
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
msg_str = 'Update of apikey failed. Error: {0}'
|
||||
current_app.logger.error(msg_str.format(e))
|
||||
db.session.rollback
|
||||
raise e
|
||||
|
||||
def get_hashed_password(self, plain_text_password=None):
|
||||
# Hash a password for the first time
|
||||
# (Using bcrypt, the salt is saved into the hash itself)
|
||||
if plain_text_password is None:
|
||||
return plain_text_password
|
||||
|
||||
if plain_text_password:
|
||||
pw = plain_text_password
|
||||
else:
|
||||
pw = self.plain_text_password
|
||||
|
||||
return bcrypt.hashpw(pw.encode('utf-8'),
|
||||
current_app.config.get('SALT').encode('utf-8'))
|
||||
|
||||
def check_password(self, hashed_password):
|
||||
# Check hased password. Using bcrypt,
|
||||
# the salt is saved into the hash itself
|
||||
if (self.plain_text_password):
|
||||
return bcrypt.checkpw(self.plain_text_password.encode('utf-8'),
|
||||
hashed_password.encode('utf-8'))
|
||||
return False
|
||||
|
||||
def is_validate(self, method, src_ip=''):
|
||||
"""
|
||||
Validate user credential
|
||||
"""
|
||||
if method == 'LOCAL':
|
||||
passw_hash = self.get_hashed_password(self.plain_text_password)
|
||||
apikey = ApiKey.query \
|
||||
.filter(ApiKey.key == passw_hash.decode('utf-8')) \
|
||||
.first()
|
||||
|
||||
if not apikey:
|
||||
raise Exception("Unauthorized")
|
||||
|
||||
return apikey
|
7
powerdnsadmin/models/base.py
Normal file
7
powerdnsadmin/models/base.py
Normal file
@ -0,0 +1,7 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
domain_apikey = db.Table(
|
||||
'domain_apikey',
|
||||
db.Column('domain_id', db.Integer, db.ForeignKey('domain.id')),
|
||||
db.Column('apikey_id', db.Integer, db.ForeignKey('apikey.id')))
|
808
powerdnsadmin/models/domain.py
Normal file
808
powerdnsadmin/models/domain.py
Normal file
@ -0,0 +1,808 @@
|
||||
import re
|
||||
import traceback
|
||||
from flask import current_app
|
||||
from urllib.parse import urljoin
|
||||
from distutils.util import strtobool
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
from ..lib import utils
|
||||
from .base import db, domain_apikey
|
||||
from .setting import Setting
|
||||
from .user import User
|
||||
from .account import Account
|
||||
from .account import AccountUser
|
||||
from .domain_user import DomainUser
|
||||
from .domain_setting import DomainSetting
|
||||
from .history import History
|
||||
|
||||
|
||||
class Domain(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(255), index=True, unique=True)
|
||||
master = db.Column(db.String(128))
|
||||
type = db.Column(db.String(6), nullable=False)
|
||||
serial = db.Column(db.Integer)
|
||||
notified_serial = db.Column(db.Integer)
|
||||
last_check = db.Column(db.Integer)
|
||||
dnssec = db.Column(db.Integer)
|
||||
account_id = db.Column(db.Integer, db.ForeignKey('account.id'))
|
||||
account = db.relationship("Account", back_populates="domains")
|
||||
settings = db.relationship('DomainSetting', back_populates='domain')
|
||||
apikeys = db.relationship("ApiKey",
|
||||
secondary=domain_apikey,
|
||||
back_populates="domains")
|
||||
|
||||
def __init__(self,
|
||||
id=None,
|
||||
name=None,
|
||||
master=None,
|
||||
type='NATIVE',
|
||||
serial=None,
|
||||
notified_serial=None,
|
||||
last_check=None,
|
||||
dnssec=None,
|
||||
account_id=None):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.master = master
|
||||
self.type = type
|
||||
self.serial = serial
|
||||
self.notified_serial = notified_serial
|
||||
self.last_check = last_check
|
||||
self.dnssec = dnssec
|
||||
self.account_id = account_id
|
||||
# PDNS configs
|
||||
self.PDNS_STATS_URL = Setting().get('pdns_api_url')
|
||||
self.PDNS_API_KEY = Setting().get('pdns_api_key')
|
||||
self.PDNS_VERSION = Setting().get('pdns_version')
|
||||
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
|
||||
|
||||
if StrictVersion(self.PDNS_VERSION) >= StrictVersion('4.0.0'):
|
||||
self.NEW_SCHEMA = True
|
||||
else:
|
||||
self.NEW_SCHEMA = False
|
||||
|
||||
def __repr__(self):
|
||||
return '<Domain {0}>'.format(self.name)
|
||||
|
||||
def add_setting(self, setting, value):
|
||||
try:
|
||||
self.settings.append(DomainSetting(setting=setting, value=value))
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Can not create setting {0} for domain {1}. {2}'.format(
|
||||
setting, self.name, e))
|
||||
return False
|
||||
|
||||
def get_domain_info(self, domain_name):
|
||||
"""
|
||||
Get all domains which has in PowerDNS
|
||||
"""
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
jdata = utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain_name)),
|
||||
headers=headers,
|
||||
timeout=int(
|
||||
Setting().get('pdns_api_timeout')))
|
||||
return jdata
|
||||
|
||||
def get_domains(self):
|
||||
"""
|
||||
Get all domains which has in PowerDNS
|
||||
"""
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
jdata = utils.fetch_json(
|
||||
urljoin(self.PDNS_STATS_URL,
|
||||
self.API_EXTENDED_URL + '/servers/localhost/zones'),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')))
|
||||
return jdata
|
||||
|
||||
def get_id_by_name(self, name):
|
||||
"""
|
||||
Return domain id
|
||||
"""
|
||||
try:
|
||||
domain = Domain.query.filter(Domain.name == name).first()
|
||||
return domain.id
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Domain does not exist. ERROR: {0}'.format(e))
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Fetch zones (domains) 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 {} entrys in PowerDNS-Admin".format(
|
||||
len(list_db_domain)))
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
try:
|
||||
jdata = utils.fetch_json(
|
||||
urljoin(self.PDNS_STATS_URL,
|
||||
self.API_EXTENDED_URL + '/servers/localhost/zones'),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')))
|
||||
list_jdomain = [d['name'].rstrip('.') for d in jdata]
|
||||
current_app.logger.info(
|
||||
"Found {} entrys in PowerDNS server".format(len(list_jdomain)))
|
||||
|
||||
try:
|
||||
# domains 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)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Can not delete domain from DB. DETAIL: {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
|
||||
# update/add new domain
|
||||
for data in jdata:
|
||||
if 'account' in data:
|
||||
account_id = Account().get_id_by_name(data['account'])
|
||||
else:
|
||||
current_app.logger.debug(
|
||||
"No 'account' data found in API result - Unsupported PowerDNS version?"
|
||||
)
|
||||
account_id = None
|
||||
domain = dict_db_domain.get(data['name'].rstrip('.'), None)
|
||||
if domain:
|
||||
self.update_pdns_admin_domain(domain, account_id, data)
|
||||
else:
|
||||
# add new domain
|
||||
self.add_domain_to_powerdns_admin(domain=data)
|
||||
|
||||
current_app.logger.info('Update domain finished')
|
||||
return {
|
||||
'status': 'ok',
|
||||
'msg': 'Domain table has been updated successfully'
|
||||
}
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Can not update domain table. Error: {0}'.format(e))
|
||||
return {'status': 'error', 'msg': 'Can not update domain table'}
|
||||
|
||||
def update_pdns_admin_domain(self, domain, account_id, data):
|
||||
# existing domain, only update if something actually has changed
|
||||
if (domain.master != str(data['masters'])
|
||||
or domain.type != data['kind']
|
||||
or domain.serial != data['serial']
|
||||
or domain.notified_serial != data['notified_serial']
|
||||
or domain.last_check != (1 if data['last_check'] else 0)
|
||||
or domain.dnssec != data['dnssec']
|
||||
or domain.account_id != account_id):
|
||||
|
||||
domain.master = str(data['masters'])
|
||||
domain.type = data['kind']
|
||||
domain.serial = data['serial']
|
||||
domain.notified_serial = data['notified_serial']
|
||||
domain.last_check = 1 if data['last_check'] else 0
|
||||
domain.dnssec = 1 if data['dnssec'] else 0
|
||||
domain.account_id = account_id
|
||||
try:
|
||||
db.session.commit()
|
||||
current_app.logger.info("Updated PDNS-Admin domain {0}".format(
|
||||
domain.name))
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.info("Rolledback Domain {0} {1}".format(
|
||||
domain.name, e))
|
||||
raise
|
||||
|
||||
def add(self,
|
||||
domain_name,
|
||||
domain_type,
|
||||
soa_edit_api,
|
||||
domain_ns=[],
|
||||
domain_master_ips=[],
|
||||
account_name=None):
|
||||
"""
|
||||
Add a domain to power dns
|
||||
"""
|
||||
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
|
||||
if self.NEW_SCHEMA:
|
||||
domain_name = domain_name + '.'
|
||||
domain_ns = [ns + '.' for ns in domain_ns]
|
||||
|
||||
if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]:
|
||||
soa_edit_api = 'DEFAULT'
|
||||
|
||||
elif soa_edit_api == 'OFF':
|
||||
soa_edit_api = ''
|
||||
|
||||
post_data = {
|
||||
"name": domain_name,
|
||||
"kind": domain_type,
|
||||
"masters": domain_master_ips,
|
||||
"nameservers": domain_ns,
|
||||
"soa_edit_api": soa_edit_api,
|
||||
"account": account_name
|
||||
}
|
||||
|
||||
try:
|
||||
jdata = utils.fetch_json(
|
||||
urljoin(self.PDNS_STATS_URL,
|
||||
self.API_EXTENDED_URL + '/servers/localhost/zones'),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='POST',
|
||||
data=post_data)
|
||||
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': jdata['error']}
|
||||
else:
|
||||
current_app.logger.info(
|
||||
'Added domain successfully to PowerDNS: {0}'.format(
|
||||
domain_name))
|
||||
self.add_domain_to_powerdns_admin(domain_dict=post_data)
|
||||
return {'status': 'ok', 'msg': 'Added domain successfully'}
|
||||
except Exception as e:
|
||||
current_app.logger.error('Cannot add domain {0} {1}'.format(
|
||||
domain_name, e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return {'status': 'error', 'msg': 'Cannot add this domain.'}
|
||||
|
||||
def add_domain_to_powerdns_admin(self, domain=None, domain_dict=None):
|
||||
"""
|
||||
Read Domain from PowerDNS and add into PDNS-Admin
|
||||
"""
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
if not domain:
|
||||
try:
|
||||
domain = utils.fetch_json(
|
||||
urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(
|
||||
domain_dict['name'])),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')))
|
||||
except Exception as e:
|
||||
current_app.logger.error('Can not read Domain from PDNS')
|
||||
current_app.logger.error(e)
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
|
||||
if 'account' in domain:
|
||||
account_id = Account().get_id_by_name(domain['account'])
|
||||
else:
|
||||
current_app.logger.debug(
|
||||
"No 'account' data found in API result - Unsupported PowerDNS version?"
|
||||
)
|
||||
account_id = None
|
||||
# add new domain
|
||||
d = Domain()
|
||||
d.name = domain['name'].rstrip('.')
|
||||
d.master = str(domain['masters'])
|
||||
d.type = domain['kind']
|
||||
d.serial = domain['serial']
|
||||
d.notified_serial = domain['notified_serial']
|
||||
d.last_check = domain['last_check']
|
||||
d.dnssec = 1 if domain['dnssec'] else 0
|
||||
d.account_id = account_id
|
||||
db.session.add(d)
|
||||
try:
|
||||
db.session.commit()
|
||||
current_app.logger.info(
|
||||
"Synched PowerDNS Domain to PDNS-Admin: {0}".format(d.name))
|
||||
return {
|
||||
'status': 'ok',
|
||||
'msg': 'Added Domain successfully to PowerDNS-Admin'
|
||||
}
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.info("Rolledback Domain {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 doesnt exist.'}
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
|
||||
if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]:
|
||||
soa_edit_api = 'DEFAULT'
|
||||
|
||||
elif soa_edit_api == 'OFF':
|
||||
soa_edit_api = ''
|
||||
|
||||
post_data = {"soa_edit_api": soa_edit_api, "kind": domain.type}
|
||||
|
||||
try:
|
||||
jdata = utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain.name)),
|
||||
headers=headers,
|
||||
timeout=int(
|
||||
Setting().get('pdns_api_timeout')),
|
||||
method='PUT',
|
||||
data=post_data)
|
||||
if 'error' in jdata.keys():
|
||||
current_app.logger.error(jdata['error'])
|
||||
return {'status': 'error', 'msg': jdata['error']}
|
||||
else:
|
||||
current_app.logger.info(
|
||||
'soa-edit-api changed for domain {0} successfully'.format(
|
||||
domain_name))
|
||||
return {
|
||||
'status': 'ok',
|
||||
'msg': 'soa-edit-api changed successfully'
|
||||
}
|
||||
except Exception as e:
|
||||
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(
|
||||
domain_name))
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg': 'Cannot change soa-edit-api this domain.'
|
||||
}
|
||||
|
||||
def create_reverse_domain(self, domain_name, domain_reverse_name):
|
||||
"""
|
||||
Check the existing reverse lookup domain,
|
||||
if not exists create a new one automatically
|
||||
"""
|
||||
domain_obj = Domain.query.filter(Domain.name == domain_name).first()
|
||||
domain_auto_ptr = DomainSetting.query.filter(
|
||||
DomainSetting.domain == domain_obj).filter(
|
||||
DomainSetting.setting == 'auto_ptr').first()
|
||||
domain_auto_ptr = strtobool(
|
||||
domain_auto_ptr.value) if domain_auto_ptr else False
|
||||
system_auto_ptr = Setting().get('auto_ptr')
|
||||
self.name = domain_name
|
||||
domain_id = self.get_id_by_name(domain_reverse_name)
|
||||
if None == domain_id and \
|
||||
(
|
||||
system_auto_ptr or
|
||||
domain_auto_ptr
|
||||
):
|
||||
result = self.add(domain_reverse_name, 'Master', 'DEFAULT', '', '')
|
||||
self.update()
|
||||
if result['status'] == 'ok':
|
||||
history = History(msg='Add reverse lookup domain {0}'.format(
|
||||
domain_reverse_name),
|
||||
detail=str({
|
||||
'domain_type': 'Master',
|
||||
'domain_master_ips': ''
|
||||
}),
|
||||
created_by='System')
|
||||
history.add()
|
||||
else:
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg': 'Adding reverse lookup domain failed'
|
||||
}
|
||||
domain_user_ids = self.get_user()
|
||||
if len(domain_user_ids) > 0:
|
||||
self.name = domain_reverse_name
|
||||
self.grant_privileges(domain_user_ids)
|
||||
return {
|
||||
'status':
|
||||
'ok',
|
||||
'msg':
|
||||
'New reverse lookup domain created with granted privileges'
|
||||
}
|
||||
return {
|
||||
'status': 'ok',
|
||||
'msg': 'New reverse lookup domain created without users'
|
||||
}
|
||||
return {'status': 'ok', 'msg': 'Reverse lookup domain already exists'}
|
||||
|
||||
def get_reverse_domain_name(self, reverse_host_address):
|
||||
c = 1
|
||||
if re.search('ip6.arpa', reverse_host_address):
|
||||
for i in range(1, 32, 1):
|
||||
address = re.search(
|
||||
'((([a-f0-9]\.){' + str(i) + '})(?P<ipname>.+6.arpa)\.?)',
|
||||
reverse_host_address)
|
||||
if None != self.get_id_by_name(address.group('ipname')):
|
||||
c = i
|
||||
break
|
||||
return re.search(
|
||||
'((([a-f0-9]\.){' + str(c) + '})(?P<ipname>.+6.arpa)\.?)',
|
||||
reverse_host_address).group('ipname')
|
||||
else:
|
||||
for i in range(1, 4, 1):
|
||||
address = re.search(
|
||||
'((([0-9]+\.){' + str(i) + '})(?P<ipname>.+r.arpa)\.?)',
|
||||
reverse_host_address)
|
||||
if None != self.get_id_by_name(address.group('ipname')):
|
||||
c = i
|
||||
break
|
||||
return re.search(
|
||||
'((([0-9]+\.){' + str(c) + '})(?P<ipname>.+r.arpa)\.?)',
|
||||
reverse_host_address).group('ipname')
|
||||
|
||||
def delete(self, domain_name):
|
||||
"""
|
||||
Delete a single domain 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'}
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot delete domain {0}'.format(domain_name))
|
||||
current_app.logger.error(e)
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return {'status': 'error', 'msg': 'Cannot delete domain'}
|
||||
|
||||
def delete_domain_from_powerdns(self, domain_name):
|
||||
"""
|
||||
Delete a single domain name from powerdns
|
||||
"""
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
|
||||
utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain_name)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='DELETE')
|
||||
current_app.logger.info(
|
||||
'Deleted domain successfully from PowerDNS-Entity: {0}'.format(
|
||||
domain_name))
|
||||
return {'status': 'ok', 'msg': 'Delete domain successfully'}
|
||||
|
||||
def delete_domain_from_pdnsadmin(self, domain_name):
|
||||
# Revoke permission before deleting domain
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
domain_user = DomainUser.query.filter(
|
||||
DomainUser.domain_id == domain.id)
|
||||
if domain_user:
|
||||
domain_user.delete()
|
||||
db.session.commit()
|
||||
domain_setting = DomainSetting.query.filter(
|
||||
DomainSetting.domain_id == domain.id)
|
||||
if domain_setting:
|
||||
domain_setting.delete()
|
||||
db.session.commit()
|
||||
domain.apikeys[:] = []
|
||||
db.session.commit()
|
||||
|
||||
# then remove domain
|
||||
Domain.query.filter(Domain.name == domain_name).delete()
|
||||
db.session.commit()
|
||||
current_app.logger.info(
|
||||
"Deleted Domain successfully from pdnsADMIN: {}".format(
|
||||
domain_name))
|
||||
|
||||
def get_user(self):
|
||||
"""
|
||||
Get users (id) who have access to this domain name
|
||||
"""
|
||||
user_ids = []
|
||||
query = db.session.query(
|
||||
DomainUser, Domain).filter(User.id == DomainUser.user_id).filter(
|
||||
Domain.id == DomainUser.domain_id).filter(
|
||||
Domain.name == self.name).all()
|
||||
for q in query:
|
||||
user_ids.append(q[0].user_id)
|
||||
return user_ids
|
||||
|
||||
def grant_privileges(self, new_user_ids):
|
||||
"""
|
||||
Reconfigure domain_user table
|
||||
"""
|
||||
|
||||
domain_id = self.get_id_by_name(self.name)
|
||||
domain_user_ids = self.get_user()
|
||||
|
||||
removed_ids = list(set(domain_user_ids).difference(new_user_ids))
|
||||
added_ids = list(set(new_user_ids).difference(domain_user_ids))
|
||||
|
||||
try:
|
||||
for uid in removed_ids:
|
||||
DomainUser.query.filter(DomainUser.user_id == uid).filter(
|
||||
DomainUser.domain_id == domain_id).delete()
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(
|
||||
'Cannot revoke user privileges on domain {0}. DETAIL: {1}'.
|
||||
format(self.name, e))
|
||||
current_app.logger.debug(print(traceback.format_exc()))
|
||||
|
||||
try:
|
||||
for uid in added_ids:
|
||||
du = DomainUser(domain_id, uid)
|
||||
db.session.add(du)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(
|
||||
'Cannot grant user privileges to domain {0}. DETAIL: {1}'.
|
||||
format(self.name, e))
|
||||
current_app.logger.debug(print(traceback.format_exc()))
|
||||
|
||||
def update_from_master(self, domain_name):
|
||||
"""
|
||||
Update records from Master DNS server
|
||||
"""
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if domain:
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
try:
|
||||
utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}/axfr-retrieve'.format(
|
||||
domain.name)),
|
||||
headers=headers,
|
||||
timeout=int(
|
||||
Setting().get('pdns_api_timeout')),
|
||||
method='PUT')
|
||||
return {
|
||||
'status': 'ok',
|
||||
'msg': 'Update from Master successfully'
|
||||
}
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot update from master. DETAIL: {0}'.format(e))
|
||||
return {
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'There was something wrong, please contact administrator'
|
||||
}
|
||||
else:
|
||||
return {'status': 'error', 'msg': 'This domain doesnot exist'}
|
||||
|
||||
def get_domain_dnssec(self, domain_name):
|
||||
"""
|
||||
Get domain DNSSEC information
|
||||
"""
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if domain:
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
try:
|
||||
jdata = utils.fetch_json(
|
||||
urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}/cryptokeys'.format(
|
||||
domain.name)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='GET')
|
||||
if 'error' in jdata:
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg': 'DNSSEC is not enabled for this domain'
|
||||
}
|
||||
else:
|
||||
return {'status': 'ok', 'dnssec': jdata}
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot get domain dnssec. DETAIL: {0}'.format(e))
|
||||
return {
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'There was something wrong, please contact administrator'
|
||||
}
|
||||
else:
|
||||
return {'status': 'error', 'msg': 'This domain doesnot exist'}
|
||||
|
||||
def enable_domain_dnssec(self, domain_name):
|
||||
"""
|
||||
Enable domain DNSSEC
|
||||
"""
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if domain:
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
try:
|
||||
# Enable API-RECTIFY for domain, BEFORE activating DNSSEC
|
||||
post_data = {"api_rectify": True}
|
||||
jdata = utils.fetch_json(
|
||||
urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain.name)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='PUT',
|
||||
data=post_data)
|
||||
if 'error' in jdata:
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg':
|
||||
'API-RECTIFY could not be enabled for this domain',
|
||||
'jdata': jdata
|
||||
}
|
||||
|
||||
# Activate DNSSEC
|
||||
post_data = {"keytype": "ksk", "active": True}
|
||||
jdata = utils.fetch_json(
|
||||
urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}/cryptokeys'.format(
|
||||
domain.name)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='POST',
|
||||
data=post_data)
|
||||
if 'error' in jdata:
|
||||
return {
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'Cannot enable DNSSEC for this domain. Error: {0}'.
|
||||
format(jdata['error']),
|
||||
'jdata':
|
||||
jdata
|
||||
}
|
||||
|
||||
return {'status': 'ok'}
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot enable dns sec. DETAIL: {}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return {
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'There was something wrong, please contact administrator'
|
||||
}
|
||||
|
||||
else:
|
||||
return {'status': 'error', 'msg': 'This domain does not exist'}
|
||||
|
||||
def delete_dnssec_key(self, domain_name, key_id):
|
||||
"""
|
||||
Remove keys DNSSEC
|
||||
"""
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if domain:
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
try:
|
||||
# Deactivate DNSSEC
|
||||
jdata = utils.fetch_json(
|
||||
urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}/cryptokeys/{1}'.format(
|
||||
domain.name, key_id)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='DELETE')
|
||||
if jdata != True:
|
||||
return {
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'Cannot disable DNSSEC for this domain. Error: {0}'.
|
||||
format(jdata['error']),
|
||||
'jdata':
|
||||
jdata
|
||||
}
|
||||
|
||||
# Disable API-RECTIFY for domain, AFTER deactivating DNSSEC
|
||||
post_data = {"api_rectify": False}
|
||||
jdata = utils.fetch_json(
|
||||
urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain.name)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='PUT',
|
||||
data=post_data)
|
||||
if 'error' in jdata:
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg':
|
||||
'API-RECTIFY could not be disabled for this domain',
|
||||
'jdata': jdata
|
||||
}
|
||||
|
||||
return {'status': 'ok'}
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot delete dnssec key. DETAIL: {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg':
|
||||
'There was something wrong, please contact administrator',
|
||||
'domain': domain.name,
|
||||
'id': key_id
|
||||
}
|
||||
|
||||
else:
|
||||
return {'status': 'error', 'msg': 'This domain doesnot exist'}
|
||||
|
||||
def assoc_account(self, account_id):
|
||||
"""
|
||||
Associate domain with a domain, specified by account id
|
||||
"""
|
||||
domain_name = self.name
|
||||
|
||||
# Sanity check - domain name
|
||||
if domain_name == "":
|
||||
return {'status': False, 'msg': 'No domain 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'}
|
||||
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
|
||||
account_name = Account().get_name_by_id(account_id)
|
||||
|
||||
post_data = {"account": account_name}
|
||||
|
||||
try:
|
||||
jdata = utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain_name)),
|
||||
headers=headers,
|
||||
timeout=int(
|
||||
Setting().get('pdns_api_timeout')),
|
||||
method='PUT',
|
||||
data=post_data)
|
||||
|
||||
if 'error' in jdata.keys():
|
||||
current_app.logger.error(jdata['error'])
|
||||
return {'status': 'error', 'msg': jdata['error']}
|
||||
else:
|
||||
self.update()
|
||||
msg_str = 'Account changed for domain {0} successfully'
|
||||
current_app.logger.info(msg_str.format(domain_name))
|
||||
return {'status': 'ok', 'msg': 'account changed successfully'}
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.debug(e)
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
msg_str = 'Cannot change account for domain {0}'
|
||||
current_app.logger.error(msg_str.format(domain_name))
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg': 'Cannot change account for this domain.'
|
||||
}
|
||||
|
||||
def get_account(self):
|
||||
"""
|
||||
Get current account associated with this domain
|
||||
"""
|
||||
domain = Domain.query.filter(Domain.name == self.name).first()
|
||||
|
||||
return domain.account
|
||||
|
||||
def is_valid_access(self, user_id):
|
||||
"""
|
||||
Check if the user is allowed to access this
|
||||
domain name
|
||||
"""
|
||||
return 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 == user_id,
|
||||
AccountUser.user_id == user_id
|
||||
)).filter(Domain.id == self.id).first()
|
33
powerdnsadmin/models/domain_setting.py
Normal file
33
powerdnsadmin/models/domain_setting.py
Normal file
@ -0,0 +1,33 @@
|
||||
from .base import db
|
||||
|
||||
|
||||
class DomainSetting(db.Model):
|
||||
__tablename__ = 'domain_setting'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
domain_id = db.Column(db.Integer, db.ForeignKey('domain.id'))
|
||||
domain = db.relationship('Domain', back_populates='settings')
|
||||
setting = db.Column(db.String(255), nullable=False)
|
||||
value = db.Column(db.String(255))
|
||||
|
||||
def __init__(self, id=None, setting=None, value=None):
|
||||
self.id = id
|
||||
self.setting = setting
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return '<DomainSetting {0} for {1}>'.format(setting, self.domain.name)
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(self) == type(other) and self.setting == other.setting
|
||||
|
||||
def set(self, value):
|
||||
try:
|
||||
self.value = value
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
'Unable to set DomainSetting value. DETAIL: {0}'.format(e))
|
||||
logging.debug(traceback.format_exc())
|
||||
db.session.rollback()
|
||||
return False
|
65
powerdnsadmin/models/domain_template.py
Normal file
65
powerdnsadmin/models/domain_template.py
Normal file
@ -0,0 +1,65 @@
|
||||
from flask import current_app
|
||||
from .base import db
|
||||
|
||||
|
||||
class DomainTemplate(db.Model):
|
||||
__tablename__ = "domain_template"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(255), index=True, unique=True)
|
||||
description = db.Column(db.String(255))
|
||||
records = db.relationship('DomainTemplateRecord',
|
||||
back_populates='template',
|
||||
cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return '<DomainTemplate {0}>'.format(self.name)
|
||||
|
||||
def __init__(self, name=None, description=None):
|
||||
self.id = None
|
||||
self.name = name
|
||||
self.description = description
|
||||
|
||||
def replace_records(self, records):
|
||||
try:
|
||||
self.records = []
|
||||
for record in records:
|
||||
self.records.append(record)
|
||||
db.session.commit()
|
||||
return {
|
||||
'status': 'ok',
|
||||
'msg': 'Template records have been modified'
|
||||
}
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot create template records Error: {0}'.format(e))
|
||||
db.session.rollback()
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg': 'Can not create template records'
|
||||
}
|
||||
|
||||
def create(self):
|
||||
try:
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
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))
|
||||
db.session.rollback()
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg': 'Can not update domain template table'
|
||||
}
|
||||
|
||||
def delete_template(self):
|
||||
try:
|
||||
self.records = []
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
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))
|
||||
db.session.rollback()
|
||||
return {'status': 'error', 'msg': 'Can not delete domain template'}
|
42
powerdnsadmin/models/domain_template_record.py
Normal file
42
powerdnsadmin/models/domain_template_record.py
Normal file
@ -0,0 +1,42 @@
|
||||
from .base import db
|
||||
|
||||
|
||||
class DomainTemplateRecord(db.Model):
|
||||
__tablename__ = "domain_template_record"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(255))
|
||||
type = db.Column(db.String(64))
|
||||
ttl = db.Column(db.Integer)
|
||||
data = db.Column(db.Text)
|
||||
status = db.Column(db.Boolean)
|
||||
template_id = db.Column(db.Integer, db.ForeignKey('domain_template.id'))
|
||||
template = db.relationship('DomainTemplate', back_populates='records')
|
||||
|
||||
def __repr__(self):
|
||||
return '<DomainTemplateRecord {0}>'.format(self.id)
|
||||
|
||||
def __init__(self,
|
||||
id=None,
|
||||
name=None,
|
||||
type=None,
|
||||
ttl=None,
|
||||
data=None,
|
||||
status=None):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.ttl = ttl
|
||||
self.data = data
|
||||
self.status = status
|
||||
|
||||
def apply(self):
|
||||
try:
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
'Can not update domain template table. Error: {0}'.format(e))
|
||||
db.session.rollback()
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg': 'Can not update domain template table'
|
||||
}
|
17
powerdnsadmin/models/domain_user.py
Normal file
17
powerdnsadmin/models/domain_user.py
Normal file
@ -0,0 +1,17 @@
|
||||
from .base import db
|
||||
|
||||
|
||||
class DomainUser(db.Model):
|
||||
__tablename__ = 'domain_user'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
domain_id = db.Column(db.Integer,
|
||||
db.ForeignKey('domain.id'),
|
||||
nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
|
||||
def __init__(self, domain_id, user_id):
|
||||
self.domain_id = domain_id
|
||||
self.user_id = user_id
|
||||
|
||||
def __repr__(self):
|
||||
return '<Domain_User {0} {1}>'.format(self.domain_id, self.user_id)
|
48
powerdnsadmin/models/history.py
Normal file
48
powerdnsadmin/models/history.py
Normal file
@ -0,0 +1,48 @@
|
||||
from flask import current_app
|
||||
from datetime import datetime
|
||||
|
||||
from .base import db
|
||||
|
||||
|
||||
class History(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
msg = db.Column(db.String(256))
|
||||
# detail = db.Column(db.Text().with_variant(db.Text(length=2**24-2), 'mysql'))
|
||||
detail = db.Column(db.Text())
|
||||
created_by = db.Column(db.String(128))
|
||||
created_on = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __init__(self, id=None, msg=None, detail=None, created_by=None):
|
||||
self.id = id
|
||||
self.msg = msg
|
||||
self.detail = detail
|
||||
self.created_by = created_by
|
||||
|
||||
def __repr__(self):
|
||||
return '<History {0}>'.format(self.msg)
|
||||
|
||||
def add(self):
|
||||
"""
|
||||
Add an event to history table
|
||||
"""
|
||||
h = History()
|
||||
h.msg = self.msg
|
||||
h.detail = self.detail
|
||||
h.created_by = self.created_by
|
||||
db.session.add(h)
|
||||
db.session.commit()
|
||||
|
||||
def remove_all(self):
|
||||
"""
|
||||
Remove all history from DB
|
||||
"""
|
||||
try:
|
||||
db.session.query(History).delete()
|
||||
db.session.commit()
|
||||
current_app.logger.info("Removed all history")
|
||||
return True
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error("Cannot remove history. DETAIL: {0}".format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return False
|
607
powerdnsadmin/models/record.py
Normal file
607
powerdnsadmin/models/record.py
Normal file
@ -0,0 +1,607 @@
|
||||
import traceback
|
||||
import itertools
|
||||
import dns.reversename
|
||||
import dns.inet
|
||||
import dns.name
|
||||
from distutils.version import StrictVersion
|
||||
from flask import current_app
|
||||
from urllib.parse import urljoin
|
||||
from distutils.util import strtobool
|
||||
|
||||
from .. import utils
|
||||
from .base import db
|
||||
from .setting import Setting
|
||||
from .domain import Domain
|
||||
from .domain_setting import DomainSetting
|
||||
|
||||
|
||||
class Record(object):
|
||||
"""
|
||||
This is not a model, it's just an object
|
||||
which be assigned data from PowerDNS API
|
||||
"""
|
||||
def __init__(self, name=None, type=None, status=None, ttl=None, data=None):
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.status = status
|
||||
self.ttl = ttl
|
||||
self.data = data
|
||||
# PDNS configs
|
||||
self.PDNS_STATS_URL = Setting().get('pdns_api_url')
|
||||
self.PDNS_API_KEY = Setting().get('pdns_api_key')
|
||||
self.PDNS_VERSION = Setting().get('pdns_version')
|
||||
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
|
||||
self.PRETTY_IPV6_PTR = Setting().get('pretty_ipv6_ptr')
|
||||
|
||||
if StrictVersion(self.PDNS_VERSION) >= StrictVersion('4.0.0'):
|
||||
self.NEW_SCHEMA = True
|
||||
else:
|
||||
self.NEW_SCHEMA = False
|
||||
|
||||
def get_record_data(self, domain):
|
||||
"""
|
||||
Query domain's DNS records via API
|
||||
"""
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
try:
|
||||
jdata = utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain)),
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
headers=headers)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
"Cannot fetch domain's record data from remote powerdns api. DETAIL: {0}"
|
||||
.format(e))
|
||||
return False
|
||||
|
||||
if self.NEW_SCHEMA:
|
||||
rrsets = jdata['rrsets']
|
||||
for rrset in rrsets:
|
||||
r_name = rrset['name'].rstrip('.')
|
||||
if self.PRETTY_IPV6_PTR: # only if activated
|
||||
if rrset['type'] == 'PTR': # only ptr
|
||||
if 'ip6.arpa' in r_name: # only if v6-ptr
|
||||
r_name = dns.reversename.to_address(
|
||||
dns.name.from_text(r_name))
|
||||
|
||||
rrset['name'] = r_name
|
||||
rrset['content'] = rrset['records'][0]['content']
|
||||
rrset['disabled'] = rrset['records'][0]['disabled']
|
||||
return {'records': rrsets}
|
||||
|
||||
return jdata
|
||||
|
||||
def add(self, domain):
|
||||
"""
|
||||
Add a record to domain
|
||||
"""
|
||||
# validate record first
|
||||
r = self.get_record_data(domain)
|
||||
records = r['records']
|
||||
check = list(filter(lambda check: check['name'] == self.name, records))
|
||||
if check:
|
||||
r = check[0]
|
||||
if r['type'] in ('A', 'AAAA', 'CNAME'):
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg':
|
||||
'Record already exists with type "A", "AAAA" or "CNAME"'
|
||||
}
|
||||
|
||||
# continue if the record is ready to be added
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
|
||||
if self.NEW_SCHEMA:
|
||||
data = {
|
||||
"rrsets": [{
|
||||
"name":
|
||||
self.name.rstrip('.') + '.',
|
||||
"type":
|
||||
self.type,
|
||||
"changetype":
|
||||
"REPLACE",
|
||||
"ttl":
|
||||
self.ttl,
|
||||
"records": [{
|
||||
"content": self.data,
|
||||
"disabled": self.status,
|
||||
}]
|
||||
}]
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"rrsets": [{
|
||||
"name":
|
||||
self.name,
|
||||
"type":
|
||||
self.type,
|
||||
"changetype":
|
||||
"REPLACE",
|
||||
"records": [{
|
||||
"content": self.data,
|
||||
"disabled": self.status,
|
||||
"name": self.name,
|
||||
"ttl": self.ttl,
|
||||
"type": self.type
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
try:
|
||||
jdata = utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='PATCH',
|
||||
data=data)
|
||||
current_app.logger.debug(jdata)
|
||||
return {'status': 'ok', 'msg': 'Record was added successfully'}
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
"Cannot add record {0}/{1}/{2} to domain {3}. DETAIL: {4}".
|
||||
format(self.name, self.type, self.data, domain, e))
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg':
|
||||
'There was something wrong, please contact administrator'
|
||||
}
|
||||
|
||||
def compare(self, domain_name, new_records):
|
||||
"""
|
||||
Compare new records with current powerdns record data
|
||||
Input is a list of hashes (records)
|
||||
"""
|
||||
# get list of current records we have in powerdns
|
||||
current_records = self.get_record_data(domain_name)['records']
|
||||
|
||||
# convert them to list of list (just has [name, type]) instead of list of hash
|
||||
# to compare easier
|
||||
list_current_records = [[x['name'], x['type']]
|
||||
for x in current_records]
|
||||
list_new_records = [[x['name'], x['type']] for x in new_records]
|
||||
|
||||
# get list of deleted records
|
||||
# they are the records which exist in list_current_records but not in list_new_records
|
||||
list_deleted_records = [
|
||||
x for x in list_current_records if x not in list_new_records
|
||||
]
|
||||
|
||||
# convert back to list of hash
|
||||
deleted_records = [
|
||||
x for x in current_records
|
||||
if [x['name'], x['type']] in list_deleted_records and (
|
||||
x['type'] in Setting().get_records_allow_to_edit()
|
||||
and x['type'] != 'SOA')
|
||||
]
|
||||
|
||||
# return a tuple
|
||||
return deleted_records, new_records
|
||||
|
||||
def apply(self, domain, post_records):
|
||||
"""
|
||||
Apply record changes to domain
|
||||
"""
|
||||
records = []
|
||||
for r in post_records:
|
||||
r_name = domain if r['record_name'] in [
|
||||
'@', ''
|
||||
] else r['record_name'] + '.' + domain
|
||||
r_type = r['record_type']
|
||||
if self.PRETTY_IPV6_PTR: # only if activated
|
||||
if self.NEW_SCHEMA: # only if new schema
|
||||
if r_type == 'PTR': # only ptr
|
||||
if ':' in r['record_name']: # dirty ipv6 check
|
||||
r_name = r['record_name']
|
||||
|
||||
r_data = domain if r_type == 'CNAME' and r['record_data'] in [
|
||||
'@', ''
|
||||
] else r['record_data']
|
||||
|
||||
record = {
|
||||
"name": r_name,
|
||||
"type": r_type,
|
||||
"content": r_data,
|
||||
"disabled":
|
||||
True if r['record_status'] == 'Disabled' else False,
|
||||
"ttl": int(r['record_ttl']) if r['record_ttl'] else 3600,
|
||||
}
|
||||
records.append(record)
|
||||
|
||||
deleted_records, new_records = self.compare(domain, records)
|
||||
|
||||
records = []
|
||||
for r in deleted_records:
|
||||
r_name = r['name'].rstrip(
|
||||
'.') + '.' if self.NEW_SCHEMA else r['name']
|
||||
r_type = r['type']
|
||||
if self.PRETTY_IPV6_PTR: # only if activated
|
||||
if self.NEW_SCHEMA: # only if new schema
|
||||
if r_type == 'PTR': # only ptr
|
||||
if ':' in r['name']: # dirty ipv6 check
|
||||
r_name = dns.reversename.from_address(
|
||||
r['name']).to_text()
|
||||
|
||||
record = {
|
||||
"name": r_name,
|
||||
"type": r_type,
|
||||
"changetype": "DELETE",
|
||||
"records": []
|
||||
}
|
||||
records.append(record)
|
||||
|
||||
postdata_for_delete = {"rrsets": records}
|
||||
|
||||
records = []
|
||||
for r in new_records:
|
||||
if self.NEW_SCHEMA:
|
||||
r_name = r['name'].rstrip('.') + '.'
|
||||
r_type = r['type']
|
||||
if self.PRETTY_IPV6_PTR: # only if activated
|
||||
if r_type == 'PTR': # only ptr
|
||||
if ':' in r['name']: # dirty ipv6 check
|
||||
r_name = r['name']
|
||||
|
||||
record = {
|
||||
"name":
|
||||
r_name,
|
||||
"type":
|
||||
r_type,
|
||||
"changetype":
|
||||
"REPLACE",
|
||||
"ttl":
|
||||
r['ttl'],
|
||||
"records": [{
|
||||
"content": r['content'],
|
||||
"disabled": r['disabled'],
|
||||
}]
|
||||
}
|
||||
else:
|
||||
record = {
|
||||
"name":
|
||||
r['name'],
|
||||
"type":
|
||||
r['type'],
|
||||
"changetype":
|
||||
"REPLACE",
|
||||
"records": [{
|
||||
"content": r['content'],
|
||||
"disabled": r['disabled'],
|
||||
"name": r['name'],
|
||||
"ttl": r['ttl'],
|
||||
"type": r['type'],
|
||||
"priority":
|
||||
10, # priority field for pdns 3.4.1. https://doc.powerdns.com/md/authoritative/upgrading/
|
||||
}]
|
||||
}
|
||||
|
||||
records.append(record)
|
||||
|
||||
# Adjustment to add multiple records which described in
|
||||
# https://github.com/ngoduykhanh/PowerDNS-Admin/issues/5#issuecomment-181637576
|
||||
final_records = []
|
||||
records = sorted(records,
|
||||
key=lambda item:
|
||||
(item["name"], item["type"], item["changetype"]))
|
||||
for key, group in itertools.groupby(
|
||||
records, lambda item:
|
||||
(item["name"], item["type"], item["changetype"])):
|
||||
if self.NEW_SCHEMA:
|
||||
r_name = key[0]
|
||||
r_type = key[1]
|
||||
r_changetype = key[2]
|
||||
|
||||
if self.PRETTY_IPV6_PTR: # only if activated
|
||||
if r_type == 'PTR': # only ptr
|
||||
if ':' in r_name: # dirty ipv6 check
|
||||
r_name = dns.reversename.from_address(
|
||||
r_name).to_text()
|
||||
|
||||
new_record = {
|
||||
"name": r_name,
|
||||
"type": r_type,
|
||||
"changetype": r_changetype,
|
||||
"ttl": None,
|
||||
"records": []
|
||||
}
|
||||
for item in group:
|
||||
temp_content = item['records'][0]['content']
|
||||
temp_disabled = item['records'][0]['disabled']
|
||||
if key[1] in ['MX', 'CNAME', 'SRV', 'NS']:
|
||||
if temp_content.strip()[-1:] != '.':
|
||||
temp_content += '.'
|
||||
|
||||
if new_record['ttl'] is None:
|
||||
new_record['ttl'] = item['ttl']
|
||||
new_record['records'].append({
|
||||
"content": temp_content,
|
||||
"disabled": temp_disabled
|
||||
})
|
||||
final_records.append(new_record)
|
||||
|
||||
else:
|
||||
|
||||
final_records.append({
|
||||
"name":
|
||||
key[0],
|
||||
"type":
|
||||
key[1],
|
||||
"changetype":
|
||||
key[2],
|
||||
"records": [{
|
||||
"content": item['records'][0]['content'],
|
||||
"disabled": item['records'][0]['disabled'],
|
||||
"name": key[0],
|
||||
"ttl": item['records'][0]['ttl'],
|
||||
"type": key[1],
|
||||
"priority": 10,
|
||||
} for item in group]
|
||||
})
|
||||
|
||||
postdata_for_new = {"rrsets": final_records}
|
||||
current_app.logger.debug(postdata_for_new)
|
||||
current_app.logger.debug(postdata_for_delete)
|
||||
current_app.logger.info(
|
||||
urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain)))
|
||||
try:
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
jdata1 = utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain)),
|
||||
headers=headers,
|
||||
method='PATCH',
|
||||
data=postdata_for_delete)
|
||||
jdata2 = utils.fetch_json(
|
||||
urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='PATCH',
|
||||
data=postdata_for_new)
|
||||
|
||||
if 'error' in jdata1.keys():
|
||||
current_app.logger.error('Cannot apply record changes.')
|
||||
current_app.logger.debug(jdata1['error'])
|
||||
return {'status': 'error', 'msg': jdata1['error']}
|
||||
elif 'error' in jdata2.keys():
|
||||
current_app.logger.error('Cannot apply record changes.')
|
||||
current_app.logger.debug(jdata2['error'])
|
||||
return {'status': 'error', 'msg': jdata2['error']}
|
||||
else:
|
||||
self.auto_ptr(domain, new_records, deleted_records)
|
||||
self.update_db_serial(domain)
|
||||
current_app.logger.info('Record was applied successfully.')
|
||||
return {
|
||||
'status': 'ok',
|
||||
'msg': 'Record was applied successfully'
|
||||
}
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
"Cannot apply record changes to domain {0}. Error: {1}".format(
|
||||
domain, e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg':
|
||||
'There was something wrong, please contact administrator'
|
||||
}
|
||||
|
||||
def auto_ptr(self, domain, new_records, deleted_records):
|
||||
"""
|
||||
Add auto-ptr records
|
||||
"""
|
||||
domain_obj = Domain.query.filter(Domain.name == domain).first()
|
||||
domain_auto_ptr = DomainSetting.query.filter(
|
||||
DomainSetting.domain == domain_obj).filter(
|
||||
DomainSetting.setting == 'auto_ptr').first()
|
||||
domain_auto_ptr = strtobool(
|
||||
domain_auto_ptr.value) if domain_auto_ptr else False
|
||||
|
||||
system_auto_ptr = Setting().get('auto_ptr')
|
||||
|
||||
if system_auto_ptr or domain_auto_ptr:
|
||||
try:
|
||||
d = Domain()
|
||||
for r in new_records:
|
||||
if r['type'] in ['A', 'AAAA']:
|
||||
r_name = r['name'] + '.'
|
||||
r_content = r['content']
|
||||
reverse_host_address = dns.reversename.from_address(
|
||||
r_content).to_text()
|
||||
domain_reverse_name = d.get_reverse_domain_name(
|
||||
reverse_host_address)
|
||||
d.create_reverse_domain(domain, domain_reverse_name)
|
||||
self.name = dns.reversename.from_address(
|
||||
r_content).to_text().rstrip('.')
|
||||
self.type = 'PTR'
|
||||
self.status = r['disabled']
|
||||
self.ttl = r['ttl']
|
||||
self.data = r_name
|
||||
self.add(domain_reverse_name)
|
||||
for r in deleted_records:
|
||||
if r['type'] in ['A', 'AAAA']:
|
||||
r_content = r['content']
|
||||
reverse_host_address = dns.reversename.from_address(
|
||||
r_content).to_text()
|
||||
domain_reverse_name = d.get_reverse_domain_name(
|
||||
reverse_host_address)
|
||||
self.name = reverse_host_address
|
||||
self.type = 'PTR'
|
||||
self.data = r_content
|
||||
self.delete(domain_reverse_name)
|
||||
return {
|
||||
'status': 'ok',
|
||||
'msg': 'Auto-PTR record was updated successfully'
|
||||
}
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
"Cannot update auto-ptr record changes to domain {0}. Error: {1}"
|
||||
.format(domain, e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return {
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'Auto-PTR creation failed. There was something wrong, please contact administrator.'
|
||||
}
|
||||
|
||||
def delete(self, domain):
|
||||
"""
|
||||
Delete a record from domain
|
||||
"""
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
data = {
|
||||
"rrsets": [{
|
||||
"name": self.name.rstrip('.') + '.',
|
||||
"type": self.type,
|
||||
"changetype": "DELETE",
|
||||
"records": []
|
||||
}]
|
||||
}
|
||||
try:
|
||||
jdata = utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='PATCH',
|
||||
data=data)
|
||||
current_app.logger.debug(jdata)
|
||||
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}"
|
||||
.format(self.name, self.type, self.data, domain, e))
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg':
|
||||
'There was something wrong, please contact administrator'
|
||||
}
|
||||
|
||||
def is_allowed_edit(self):
|
||||
"""
|
||||
Check if record is allowed to edit
|
||||
"""
|
||||
return self.type in Setting().get_records_allow_to_edit()
|
||||
|
||||
def is_allowed_delete(self):
|
||||
"""
|
||||
Check if record is allowed to removed
|
||||
"""
|
||||
return (self.type in Setting().get_records_allow_to_edit()
|
||||
and self.type != 'SOA')
|
||||
|
||||
def exists(self, domain):
|
||||
"""
|
||||
Check if record is present within domain records, and if it's present set self to found record
|
||||
"""
|
||||
jdata = self.get_record_data(domain)
|
||||
jrecords = jdata['records']
|
||||
|
||||
for jr in jrecords:
|
||||
if jr['name'] == self.name and jr['type'] == self.type:
|
||||
self.name = jr['name']
|
||||
self.type = jr['type']
|
||||
self.status = jr['disabled']
|
||||
self.ttl = jr['ttl']
|
||||
self.data = jr['content']
|
||||
self.priority = 10
|
||||
return True
|
||||
return False
|
||||
|
||||
def update(self, domain, content):
|
||||
"""
|
||||
Update single record
|
||||
"""
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
|
||||
if self.NEW_SCHEMA:
|
||||
data = {
|
||||
"rrsets": [{
|
||||
"name":
|
||||
self.name + '.',
|
||||
"type":
|
||||
self.type,
|
||||
"ttl":
|
||||
self.ttl,
|
||||
"changetype":
|
||||
"REPLACE",
|
||||
"records": [{
|
||||
"content": content,
|
||||
"disabled": self.status,
|
||||
}]
|
||||
}]
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"rrsets": [{
|
||||
"name":
|
||||
self.name,
|
||||
"type":
|
||||
self.type,
|
||||
"changetype":
|
||||
"REPLACE",
|
||||
"records": [{
|
||||
"content": content,
|
||||
"disabled": self.status,
|
||||
"name": self.name,
|
||||
"ttl": self.ttl,
|
||||
"type": self.type,
|
||||
"priority": 10
|
||||
}]
|
||||
}]
|
||||
}
|
||||
try:
|
||||
utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='PATCH',
|
||||
data=data)
|
||||
current_app.logger.debug("dyndns data: {0}".format(data))
|
||||
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}".
|
||||
format(self.name, self.type, self.data, domain, e))
|
||||
return {
|
||||
'status': 'error',
|
||||
'msg':
|
||||
'There was something wrong, please contact administrator'
|
||||
}
|
||||
|
||||
def update_db_serial(self, domain):
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
jdata = utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='GET')
|
||||
serial = jdata['serial']
|
||||
|
||||
domain = Domain.query.filter(Domain.name == domain).first()
|
||||
if domain:
|
||||
domain.serial = serial
|
||||
db.session.commit()
|
||||
return {
|
||||
'status': True,
|
||||
'msg': 'Synced local serial for domain name {0}'.format(domain)
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'status': False,
|
||||
'msg':
|
||||
'Could not find domain name {0} in local db'.format(domain)
|
||||
}
|
25
powerdnsadmin/models/record_entry.py
Normal file
25
powerdnsadmin/models/record_entry.py
Normal file
@ -0,0 +1,25 @@
|
||||
class RecordEntry(object):
|
||||
"""
|
||||
This is not a model, it's just an object
|
||||
which will store records entries from PowerDNS API
|
||||
"""
|
||||
def __init__(self,
|
||||
name=None,
|
||||
type=None,
|
||||
status=None,
|
||||
ttl=None,
|
||||
data=None,
|
||||
is_allowed_edit=False):
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.status = status
|
||||
self.ttl = ttl
|
||||
self.data = data
|
||||
self._is_allowed_edit = is_allowed_edit
|
||||
self._is_allowed_delete = is_allowed_edit and self.type != 'SOA'
|
||||
|
||||
def is_allowed_edit(self):
|
||||
return self._is_allowed_edit
|
||||
|
||||
def is_allowed_delete(self):
|
||||
return self._is_allowed_delete
|
23
powerdnsadmin/models/role.py
Normal file
23
powerdnsadmin/models/role.py
Normal file
@ -0,0 +1,23 @@
|
||||
from .base import db
|
||||
|
||||
|
||||
class Role(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(64), index=True, unique=True)
|
||||
description = db.Column(db.String(128))
|
||||
users = db.relationship('User', backref='role', lazy=True)
|
||||
apikeys = db.relationship('ApiKey', back_populates='role', lazy=True)
|
||||
|
||||
def __init__(self, id=None, name=None, description=None):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.description = description
|
||||
|
||||
# allow database autoincrement to do its own ID assignments
|
||||
def __init__(self, name=None, description=None):
|
||||
self.id = None
|
||||
self.name = name
|
||||
self.description = description
|
||||
|
||||
def __repr__(self):
|
||||
return '<Role {0}r>'.format(self.name)
|
64
powerdnsadmin/models/server.py
Normal file
64
powerdnsadmin/models/server.py
Normal file
@ -0,0 +1,64 @@
|
||||
import traceback
|
||||
from flask import current_app
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from ..lib import utils
|
||||
from .base import db
|
||||
from .setting import Setting
|
||||
|
||||
|
||||
class Server(object):
|
||||
"""
|
||||
This is not a model, it's just an object
|
||||
which be assigned data from PowerDNS API
|
||||
"""
|
||||
def __init__(self, server_id=None, server_config=None):
|
||||
self.server_id = server_id
|
||||
self.server_config = server_config
|
||||
# PDNS configs
|
||||
self.PDNS_STATS_URL = Setting().get('pdns_api_url')
|
||||
self.PDNS_API_KEY = Setting().get('pdns_api_key')
|
||||
self.PDNS_VERSION = Setting().get('pdns_version')
|
||||
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
|
||||
|
||||
def get_config(self):
|
||||
"""
|
||||
Get server config
|
||||
"""
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
|
||||
try:
|
||||
jdata = utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/{0}/config'.format(self.server_id)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='GET')
|
||||
return jdata
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
"Can not get server configuration. DETAIL: {0}".format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return []
|
||||
|
||||
def get_statistic(self):
|
||||
"""
|
||||
Get server statistics
|
||||
"""
|
||||
headers = {}
|
||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
||||
|
||||
try:
|
||||
jdata = utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/{0}/statistics'.format(self.server_id)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='GET')
|
||||
return jdata
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
"Can not get server statistics. DETAIL: {0}".format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return []
|
268
powerdnsadmin/models/setting.py
Normal file
268
powerdnsadmin/models/setting.py
Normal file
@ -0,0 +1,268 @@
|
||||
import sys
|
||||
import pytimeparse
|
||||
from ast import literal_eval
|
||||
from distutils.util import strtobool
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
from .base import db
|
||||
|
||||
|
||||
class Setting(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(64))
|
||||
value = db.Column(db.Text())
|
||||
|
||||
defaults = {
|
||||
'maintenance': False,
|
||||
'fullscreen_layout': True,
|
||||
'record_helper': True,
|
||||
'login_ldap_first': True,
|
||||
'default_record_table_size': 15,
|
||||
'default_domain_table_size': 10,
|
||||
'auto_ptr': False,
|
||||
'record_quick_edit': True,
|
||||
'pretty_ipv6_ptr': False,
|
||||
'dnssec_admins_only': False,
|
||||
'allow_user_create_domain': False,
|
||||
'bg_domain_updates': False,
|
||||
'site_name': 'PowerDNS-Admin',
|
||||
'session_timeout': 10,
|
||||
'pdns_api_url': '',
|
||||
'pdns_api_key': '',
|
||||
'pdns_api_timeout': 30,
|
||||
'pdns_version': '4.1.1',
|
||||
'local_db_enabled': True,
|
||||
'signup_enabled': True,
|
||||
'ldap_enabled': False,
|
||||
'ldap_type': 'ldap',
|
||||
'ldap_uri': '',
|
||||
'ldap_base_dn': '',
|
||||
'ldap_admin_username': '',
|
||||
'ldap_admin_password': '',
|
||||
'ldap_filter_basic': '',
|
||||
'ldap_filter_username': '',
|
||||
'ldap_sg_enabled': False,
|
||||
'ldap_admin_group': '',
|
||||
'ldap_operator_group': '',
|
||||
'ldap_user_group': '',
|
||||
'ldap_domain': '',
|
||||
'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/',
|
||||
'oidc_oauth_enabled': False,
|
||||
'oidc_oauth_key': '',
|
||||
'oidc_oauth_secret': '',
|
||||
'oidc_oauth_scope': 'email',
|
||||
'oidc_oauth_api_url': '',
|
||||
'oidc_oauth_token_url': '',
|
||||
'oidc_oauth_authorize_url': '',
|
||||
'forward_records_allow_edit': {
|
||||
'A': True,
|
||||
'AAAA': True,
|
||||
'AFSDB': False,
|
||||
'ALIAS': False,
|
||||
'CAA': True,
|
||||
'CERT': False,
|
||||
'CDNSKEY': False,
|
||||
'CDS': False,
|
||||
'CNAME': True,
|
||||
'DNSKEY': False,
|
||||
'DNAME': False,
|
||||
'DS': False,
|
||||
'HINFO': False,
|
||||
'KEY': False,
|
||||
'LOC': True,
|
||||
'LUA': False,
|
||||
'MX': True,
|
||||
'NAPTR': False,
|
||||
'NS': True,
|
||||
'NSEC': False,
|
||||
'NSEC3': False,
|
||||
'NSEC3PARAM': False,
|
||||
'OPENPGPKEY': False,
|
||||
'PTR': True,
|
||||
'RP': False,
|
||||
'RRSIG': False,
|
||||
'SOA': False,
|
||||
'SPF': True,
|
||||
'SSHFP': False,
|
||||
'SRV': True,
|
||||
'TKEY': False,
|
||||
'TSIG': False,
|
||||
'TLSA': False,
|
||||
'SMIMEA': False,
|
||||
'TXT': True,
|
||||
'URI': False
|
||||
},
|
||||
'reverse_records_allow_edit': {
|
||||
'A': False,
|
||||
'AAAA': False,
|
||||
'AFSDB': False,
|
||||
'ALIAS': False,
|
||||
'CAA': False,
|
||||
'CERT': False,
|
||||
'CDNSKEY': False,
|
||||
'CDS': False,
|
||||
'CNAME': False,
|
||||
'DNSKEY': False,
|
||||
'DNAME': False,
|
||||
'DS': False,
|
||||
'HINFO': False,
|
||||
'KEY': False,
|
||||
'LOC': True,
|
||||
'LUA': False,
|
||||
'MX': False,
|
||||
'NAPTR': False,
|
||||
'NS': True,
|
||||
'NSEC': False,
|
||||
'NSEC3': False,
|
||||
'NSEC3PARAM': False,
|
||||
'OPENPGPKEY': False,
|
||||
'PTR': True,
|
||||
'RP': False,
|
||||
'RRSIG': False,
|
||||
'SOA': False,
|
||||
'SPF': False,
|
||||
'SSHFP': False,
|
||||
'SRV': False,
|
||||
'TKEY': False,
|
||||
'TSIG': False,
|
||||
'TLSA': False,
|
||||
'SMIMEA': False,
|
||||
'TXT': True,
|
||||
'URI': False
|
||||
},
|
||||
'ttl_options': '1 minute,5 minutes,30 minutes,60 minutes,24 hours',
|
||||
}
|
||||
|
||||
def __init__(self, id=None, name=None, value=None):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
# allow database autoincrement to do its own ID assignments
|
||||
def __init__(self, name=None, value=None):
|
||||
self.id = None
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
def set_maintenance(self, mode):
|
||||
maintenance = Setting.query.filter(
|
||||
Setting.name == 'maintenance').first()
|
||||
|
||||
if maintenance is None:
|
||||
value = self.defaults['maintenance']
|
||||
maintenance = Setting(name='maintenance', value=str(value))
|
||||
db.session.add(maintenance)
|
||||
|
||||
mode = str(mode)
|
||||
|
||||
try:
|
||||
if maintenance.value != mode:
|
||||
maintenance.value = mode
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error('Cannot set maintenance to {0}. DETAIL: {1}'.format(
|
||||
mode, e))
|
||||
logging.debug(traceback.format_exec())
|
||||
db.session.rollback()
|
||||
return False
|
||||
|
||||
def toggle(self, setting):
|
||||
current_setting = Setting.query.filter(Setting.name == setting).first()
|
||||
|
||||
if current_setting is None:
|
||||
value = self.defaults[setting]
|
||||
current_setting = Setting(name=setting, value=str(value))
|
||||
db.session.add(current_setting)
|
||||
|
||||
try:
|
||||
if current_setting.value == "True":
|
||||
current_setting.value = "False"
|
||||
else:
|
||||
current_setting.value = "True"
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error('Cannot toggle setting {0}. DETAIL: {1}'.format(
|
||||
setting, e))
|
||||
logging.debug(traceback.format_exec())
|
||||
db.session.rollback()
|
||||
return False
|
||||
|
||||
def set(self, setting, value):
|
||||
current_setting = Setting.query.filter(Setting.name == setting).first()
|
||||
|
||||
if current_setting is None:
|
||||
current_setting = Setting(name=setting, value=None)
|
||||
db.session.add(current_setting)
|
||||
|
||||
value = str(value)
|
||||
|
||||
try:
|
||||
current_setting.value = value
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error('Cannot edit setting {0}. DETAIL: {1}'.format(
|
||||
setting, e))
|
||||
logging.debug(traceback.format_exec())
|
||||
db.session.rollback()
|
||||
return False
|
||||
|
||||
def get(self, setting):
|
||||
if setting in self.defaults:
|
||||
result = self.query.filter(Setting.name == setting).first()
|
||||
if result is not None:
|
||||
return strtobool(result.value) if result.value in [
|
||||
'True', 'False'
|
||||
] else result.value
|
||||
else:
|
||||
return self.defaults[setting]
|
||||
else:
|
||||
logging.error('Unknown setting queried: {0}'.format(setting))
|
||||
|
||||
def get_records_allow_to_edit(self):
|
||||
return list(
|
||||
set(self.get_forward_records_allow_to_edit() +
|
||||
self.get_reverse_records_allow_to_edit()))
|
||||
|
||||
def get_forward_records_allow_to_edit(self):
|
||||
records = self.get('forward_records_allow_edit')
|
||||
f_records = literal_eval(records) if isinstance(records,
|
||||
str) else records
|
||||
r_name = [r for r in f_records if f_records[r]]
|
||||
# Sort alphabetically if python version is smaller than 3.6
|
||||
if sys.version_info[0] < 3 or (sys.version_info[0] == 3
|
||||
and sys.version_info[1] < 6):
|
||||
r_name.sort()
|
||||
return r_name
|
||||
|
||||
def get_reverse_records_allow_to_edit(self):
|
||||
records = self.get('reverse_records_allow_edit')
|
||||
r_records = literal_eval(records) if isinstance(records,
|
||||
str) else records
|
||||
r_name = [r for r in r_records if r_records[r]]
|
||||
# Sort alphabetically if python version is smaller than 3.6
|
||||
if sys.version_info[0] < 3 or (sys.version_info[0] == 3
|
||||
and sys.version_info[1] < 6):
|
||||
r_name.sort()
|
||||
return r_name
|
||||
|
||||
def get_ttl_options(self):
|
||||
return [(pytimeparse.parse(ttl), ttl)
|
||||
for ttl in self.get('ttl_options').split(',')]
|
581
powerdnsadmin/models/user.py
Normal file
581
powerdnsadmin/models/user.py
Normal file
@ -0,0 +1,581 @@
|
||||
import os
|
||||
import base64
|
||||
import bcrypt
|
||||
import ldap
|
||||
import ldap.filter
|
||||
from flask import current_app
|
||||
from flask_login import AnonymousUserMixin
|
||||
|
||||
from .base import db
|
||||
from .role import Role
|
||||
from .domain_user import DomainUser
|
||||
|
||||
|
||||
class Anonymous(AnonymousUserMixin):
|
||||
def __init__(self):
|
||||
self.username = 'Anonymous'
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(64), index=True, unique=True)
|
||||
password = db.Column(db.String(64))
|
||||
firstname = db.Column(db.String(64))
|
||||
lastname = db.Column(db.String(64))
|
||||
email = db.Column(db.String(128))
|
||||
otp_secret = db.Column(db.String(16))
|
||||
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
|
||||
|
||||
def __init__(self,
|
||||
id=None,
|
||||
username=None,
|
||||
password=None,
|
||||
plain_text_password=None,
|
||||
firstname=None,
|
||||
lastname=None,
|
||||
role_id=None,
|
||||
email=None,
|
||||
otp_secret=None,
|
||||
reload_info=True):
|
||||
self.id = id
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.plain_text_password = plain_text_password
|
||||
self.firstname = firstname
|
||||
self.lastname = lastname
|
||||
self.role_id = role_id
|
||||
self.email = email
|
||||
self.otp_secret = otp_secret
|
||||
|
||||
if reload_info:
|
||||
user_info = self.get_user_info_by_id(
|
||||
) if id else self.get_user_info_by_username()
|
||||
|
||||
if user_info:
|
||||
self.id = user_info.id
|
||||
self.username = user_info.username
|
||||
self.firstname = user_info.firstname
|
||||
self.lastname = user_info.lastname
|
||||
self.email = user_info.email
|
||||
self.role_id = user_info.role_id
|
||||
self.otp_secret = user_info.otp_secret
|
||||
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
def is_active(self):
|
||||
return True
|
||||
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
def get_id(self):
|
||||
try:
|
||||
return unicode(self.id) # python 2
|
||||
except NameError:
|
||||
return str(self.id) # python 3
|
||||
|
||||
def __repr__(self):
|
||||
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)
|
||||
|
||||
def verify_totp(self, token):
|
||||
totp = pyotp.TOTP(self.otp_secret)
|
||||
return totp.verify(token)
|
||||
|
||||
def get_hashed_password(self, plain_text_password=None):
|
||||
# Hash a password for the first time
|
||||
# (Using bcrypt, the salt is saved into the hash itself)
|
||||
if plain_text_password is None:
|
||||
return plain_text_password
|
||||
|
||||
pw = plain_text_password if plain_text_password else self.plain_text_password
|
||||
return bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt())
|
||||
|
||||
def check_password(self, hashed_password):
|
||||
# Check hased password. Using bcrypt, the salt is saved into the hash itself
|
||||
if (self.plain_text_password):
|
||||
return bcrypt.checkpw(self.plain_text_password.encode('utf-8'),
|
||||
hashed_password.encode('utf-8'))
|
||||
return False
|
||||
|
||||
def get_user_info_by_id(self):
|
||||
user_info = User.query.get(int(self.id))
|
||||
return user_info
|
||||
|
||||
def get_user_info_by_username(self):
|
||||
user_info = User.query.filter(User.username == self.username).first()
|
||||
return user_info
|
||||
|
||||
def ldap_init_conn(self):
|
||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||
conn = ldap.initialize(Setting().get('ldap_uri'))
|
||||
conn.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
|
||||
conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
|
||||
conn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND)
|
||||
conn.set_option(ldap.OPT_X_TLS_DEMAND, True)
|
||||
conn.set_option(ldap.OPT_DEBUG_LEVEL, 255)
|
||||
conn.protocol_version = ldap.VERSION3
|
||||
return conn
|
||||
|
||||
def ldap_search(self, searchFilter, baseDN):
|
||||
searchScope = ldap.SCOPE_SUBTREE
|
||||
retrieveAttributes = None
|
||||
|
||||
try:
|
||||
conn = self.ldap_init_conn()
|
||||
if Setting().get('ldap_type') == 'ad':
|
||||
conn.simple_bind_s(
|
||||
"{0}@{1}".format(self.username,
|
||||
Setting().get('ldap_domain')),
|
||||
self.password)
|
||||
else:
|
||||
conn.simple_bind_s(Setting().get('ldap_admin_username'),
|
||||
Setting().get('ldap_admin_password'))
|
||||
ldap_result_id = conn.search(baseDN, searchScope, searchFilter,
|
||||
retrieveAttributes)
|
||||
result_set = []
|
||||
|
||||
while 1:
|
||||
result_type, result_data = conn.result(ldap_result_id, 0)
|
||||
if (result_data == []):
|
||||
break
|
||||
else:
|
||||
if result_type == ldap.RES_SEARCH_ENTRY:
|
||||
result_set.append(result_data)
|
||||
return result_set
|
||||
|
||||
except ldap.LDAPError as e:
|
||||
current_app.logger.error(e)
|
||||
current_app.logger.debug('baseDN: {0}'.format(baseDN))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
|
||||
def ldap_auth(self, ldap_username, password):
|
||||
try:
|
||||
conn = self.ldap_init_conn()
|
||||
conn.simple_bind_s(ldap_username, password)
|
||||
return True
|
||||
except ldap.LDAPError as e:
|
||||
current_app.logger.error(e)
|
||||
return False
|
||||
|
||||
def ad_recursive_groups(self, groupDN):
|
||||
"""
|
||||
Recursively list groups belonging to a group. It will allow checking deep in the Active Directory
|
||||
whether a user is allowed to enter or not
|
||||
"""
|
||||
LDAP_BASE_DN = Setting().get('ldap_base_dn')
|
||||
groupSearchFilter = "(&(objectcategory=group)(member=%s))" % ldap.filter.escape_filter_chars(
|
||||
groupDN)
|
||||
result = [groupDN]
|
||||
try:
|
||||
groups = self.ldap_search(groupSearchFilter, LDAP_BASE_DN)
|
||||
for group in groups:
|
||||
result += [group[0][0]]
|
||||
if 'memberOf' in group[0][1]:
|
||||
for member in group[0][1]['memberOf']:
|
||||
result += self.ad_recursive_groups(
|
||||
member.decode("utf-8"))
|
||||
return result
|
||||
except ldap.LDAPError as e:
|
||||
current_app.logger.exception("Recursive AD Group search error")
|
||||
return result
|
||||
|
||||
def is_validate(self, method, src_ip=''):
|
||||
"""
|
||||
Validate user credential
|
||||
"""
|
||||
role_name = 'User'
|
||||
|
||||
if method == 'LOCAL':
|
||||
user_info = User.query.filter(
|
||||
User.username == self.username).first()
|
||||
|
||||
if user_info:
|
||||
if user_info.password and self.check_password(
|
||||
user_info.password):
|
||||
current_app.logger.info(
|
||||
'User "{0}" logged in successfully. Authentication request from {1}'
|
||||
.format(self.username, src_ip))
|
||||
return True
|
||||
current_app.logger.error(
|
||||
'User "{0}" inputted a wrong password. Authentication request from {1}'
|
||||
.format(self.username, src_ip))
|
||||
return False
|
||||
|
||||
current_app.logger.warning(
|
||||
'User "{0}" does not exist. Authentication request from {1}'.
|
||||
format(self.username, src_ip))
|
||||
return False
|
||||
|
||||
if method == 'LDAP':
|
||||
LDAP_TYPE = Setting().get('ldap_type')
|
||||
LDAP_BASE_DN = Setting().get('ldap_base_dn')
|
||||
LDAP_FILTER_BASIC = Setting().get('ldap_filter_basic')
|
||||
LDAP_FILTER_USERNAME = Setting().get('ldap_filter_username')
|
||||
LDAP_ADMIN_GROUP = Setting().get('ldap_admin_group')
|
||||
LDAP_OPERATOR_GROUP = Setting().get('ldap_operator_group')
|
||||
LDAP_USER_GROUP = Setting().get('ldap_user_group')
|
||||
LDAP_GROUP_SECURITY_ENABLED = Setting().get('ldap_sg_enabled')
|
||||
|
||||
# validate AD user password
|
||||
if Setting().get('ldap_type') == 'ad':
|
||||
ldap_username = "{0}@{1}".format(self.username,
|
||||
Setting().get('ldap_domain'))
|
||||
if not self.ldap_auth(ldap_username, self.password):
|
||||
current_app.logger.error(
|
||||
'User "{0}" input a wrong LDAP password. Authentication request from {1}'
|
||||
.format(self.username, src_ip))
|
||||
return False
|
||||
|
||||
searchFilter = "(&({0}={1}){2})".format(LDAP_FILTER_USERNAME,
|
||||
self.username,
|
||||
LDAP_FILTER_BASIC)
|
||||
current_app.logger.debug('Ldap searchFilter {0}'.format(searchFilter))
|
||||
|
||||
ldap_result = self.ldap_search(searchFilter, LDAP_BASE_DN)
|
||||
current_app.logger.debug('Ldap search result: {0}'.format(ldap_result))
|
||||
|
||||
if not ldap_result:
|
||||
current_app.logger.warning(
|
||||
'LDAP User "{0}" does not exist. Authentication request from {1}'
|
||||
.format(self.username, src_ip))
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
ldap_username = ldap.filter.escape_filter_chars(
|
||||
ldap_result[0][0][0])
|
||||
|
||||
if Setting().get('ldap_type') != 'ad':
|
||||
# validate ldap user password
|
||||
if not self.ldap_auth(ldap_username, self.password):
|
||||
current_app.logger.error(
|
||||
'User "{0}" input a wrong LDAP password. Authentication request from {1}'
|
||||
.format(self.username, src_ip))
|
||||
return False
|
||||
|
||||
# check if LDAP_GROUP_SECURITY_ENABLED is True
|
||||
# user can be assigned to ADMIN or USER role.
|
||||
if LDAP_GROUP_SECURITY_ENABLED:
|
||||
try:
|
||||
if LDAP_TYPE == 'ldap':
|
||||
if (self.ldap_search(searchFilter,
|
||||
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(searchFilter,
|
||||
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(searchFilter,
|
||||
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))
|
||||
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':
|
||||
user_ldap_groups = []
|
||||
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'
|
||||
.format(self.username))
|
||||
return False
|
||||
|
||||
for group in [
|
||||
g.decode("utf-8")
|
||||
for g in user_ad_member_of
|
||||
]:
|
||||
user_ldap_groups += self.ad_recursive_groups(
|
||||
group)
|
||||
|
||||
if (LDAP_ADMIN_GROUP in user_ldap_groups):
|
||||
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_ldap_groups):
|
||||
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_ldap_groups):
|
||||
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:
|
||||
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
|
||||
else:
|
||||
current_app.logger.error('Invalid LDAP type')
|
||||
return False
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'LDAP group lookup for user "{0}" has failed. Authentication request from {1}'
|
||||
.format(self.username, src_ip))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error('Wrong LDAP configuration. {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return False
|
||||
|
||||
# create user if not exist in the db
|
||||
if not User.query.filter(User.username == self.username).first():
|
||||
self.firstname = self.username
|
||||
self.lastname = ''
|
||||
try:
|
||||
# try to get user's firstname, lastname and email address from LDAP attributes
|
||||
if LDAP_TYPE == 'ldap':
|
||||
self.firstname = ldap_result[0][0][1]['givenName'][
|
||||
0].decode("utf-8")
|
||||
self.lastname = ldap_result[0][0][1]['sn'][0].decode(
|
||||
"utf-8")
|
||||
self.email = ldap_result[0][0][1]['mail'][0].decode(
|
||||
"utf-8")
|
||||
elif LDAP_TYPE == 'ad':
|
||||
self.firstname = ldap_result[0][0][1]['name'][
|
||||
0].decode("utf-8")
|
||||
self.email = ldap_result[0][0][1]['userPrincipalName'][
|
||||
0].decode("utf-8")
|
||||
except Exception as e:
|
||||
current_app.logger.warning(
|
||||
"Reading ldap data threw an exception {0}".format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
|
||||
# first register user will be in Administrator role
|
||||
if User.query.count() == 0:
|
||||
self.role_id = Role.query.filter_by(
|
||||
name='Administrator').first().id
|
||||
else:
|
||||
self.role_id = Role.query.filter_by(
|
||||
name=role_name).first().id
|
||||
|
||||
self.create_user()
|
||||
current_app.logger.info('Created user "{0}" in the DB'.format(
|
||||
self.username))
|
||||
|
||||
# user already exists in database, set their role based on group membership (if enabled)
|
||||
if LDAP_GROUP_SECURITY_ENABLED:
|
||||
self.set_role(role_name)
|
||||
|
||||
return True
|
||||
else:
|
||||
current_app.logger.error('Unsupported authentication method')
|
||||
return False
|
||||
|
||||
# def get_apikeys(self, domain_name=None):
|
||||
# info = []
|
||||
# apikey_query = db.session.query(ApiKey) \
|
||||
# .join(Domain.apikeys) \
|
||||
# .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 == User.id,
|
||||
# AccountUser.user_id == User.id
|
||||
# )
|
||||
# ) \
|
||||
# .filter(User.id == self.id)
|
||||
|
||||
# if domain_name:
|
||||
# info = apikey_query.filter(Domain.name == domain_name).all()
|
||||
# else:
|
||||
# info = apikey_query.all()
|
||||
|
||||
# return info
|
||||
|
||||
def create_user(self):
|
||||
"""
|
||||
If user logged in successfully via LDAP in the first time
|
||||
We will create a local user (in DB) in order to manage user
|
||||
profile such as name, roles,...
|
||||
"""
|
||||
|
||||
# Set an invalid password hash for non local users
|
||||
self.password = '*'
|
||||
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
def create_local_user(self):
|
||||
"""
|
||||
Create local user witch stores username / password in the DB
|
||||
"""
|
||||
# check if username existed
|
||||
user = User.query.filter(User.username == self.username).first()
|
||||
if user:
|
||||
return {'status': False, 'msg': 'Username is already in use'}
|
||||
|
||||
# check if email existed
|
||||
user = User.query.filter(User.email == self.email).first()
|
||||
if user:
|
||||
return {'status': False, 'msg': 'Email address is already in use'}
|
||||
|
||||
# first register user will be in Administrator role
|
||||
self.role_id = Role.query.filter_by(name='User').first().id
|
||||
if User.query.count() == 0:
|
||||
self.role_id = Role.query.filter_by(
|
||||
name='Administrator').first().id
|
||||
|
||||
self.password = self.get_hashed_password(
|
||||
self.plain_text_password) if self.plain_text_password else '*'
|
||||
|
||||
if self.password and self.password != '*':
|
||||
self.password = self.password.decode("utf-8")
|
||||
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
return {'status': True, 'msg': 'Created user successfully'}
|
||||
|
||||
def update_local_user(self):
|
||||
"""
|
||||
Update local user
|
||||
"""
|
||||
# Sanity check - account name
|
||||
if self.username == "":
|
||||
return {'status': False, 'msg': 'No user name specified'}
|
||||
|
||||
# read user and check that it exists
|
||||
user = User.query.filter(User.username == self.username).first()
|
||||
if not user:
|
||||
return {'status': False, 'msg': 'User does not exist'}
|
||||
|
||||
# check if new email exists (only if changed)
|
||||
if user.email != self.email:
|
||||
checkuser = User.query.filter(User.email == self.email).first()
|
||||
if checkuser:
|
||||
return {
|
||||
'status': False,
|
||||
'msg': 'New email address is already in use'
|
||||
}
|
||||
|
||||
user.firstname = self.firstname
|
||||
user.lastname = self.lastname
|
||||
user.email = self.email
|
||||
|
||||
# store new password hash (only if changed)
|
||||
if self.plain_text_password != "":
|
||||
user.password = self.get_hashed_password(
|
||||
self.plain_text_password).decode("utf-8")
|
||||
|
||||
db.session.commit()
|
||||
return {'status': True, 'msg': 'User updated successfully'}
|
||||
|
||||
def update_profile(self, enable_otp=None):
|
||||
"""
|
||||
Update user profile
|
||||
"""
|
||||
|
||||
user = User.query.filter(User.username == self.username).first()
|
||||
if not user:
|
||||
return False
|
||||
|
||||
user.firstname = self.firstname if self.firstname else user.firstname
|
||||
user.lastname = self.lastname if self.lastname else user.lastname
|
||||
user.email = self.email if self.email else user.email
|
||||
user.password = self.get_hashed_password(
|
||||
self.plain_text_password).decode(
|
||||
"utf-8") if self.plain_text_password else user.password
|
||||
|
||||
if enable_otp is not None:
|
||||
user.otp_secret = ""
|
||||
|
||||
if enable_otp == True:
|
||||
# generate the opt secret key
|
||||
user.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8')
|
||||
|
||||
try:
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
return False
|
||||
|
||||
def get_domains(self):
|
||||
"""
|
||||
Get list of domains which the user is granted to have
|
||||
access.
|
||||
|
||||
Note: This doesn't include the permission granting from Account
|
||||
which user belong to
|
||||
"""
|
||||
|
||||
return self.get_domain_query().all()
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Delete a user
|
||||
"""
|
||||
# revoke all user privileges first
|
||||
self.revoke_privilege()
|
||||
|
||||
try:
|
||||
User.query.filter(User.username == self.username).delete()
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error('Cannot delete user {0} from DB. DETAIL: {1}'.format(
|
||||
self.username, e))
|
||||
return False
|
||||
|
||||
def revoke_privilege(self):
|
||||
"""
|
||||
Revoke all privileges from a user
|
||||
"""
|
||||
user = User.query.filter(User.username == self.username).first()
|
||||
|
||||
if user:
|
||||
user_id = user.id
|
||||
try:
|
||||
DomainUser.query.filter(DomainUser.user_id == user_id).delete()
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(
|
||||
'Cannot revoke user {0} privileges. DETAIL: {1}'.format(
|
||||
self.username, e))
|
||||
return False
|
||||
return False
|
||||
|
||||
def set_role(self, role_name):
|
||||
role = Role.query.filter(Role.name == role_name).first()
|
||||
if role:
|
||||
user = User.query.filter(User.username == self.username).first()
|
||||
user.role_id = role.id
|
||||
db.session.commit()
|
||||
return {'status': True, 'msg': 'Set user role successfully'}
|
||||
else:
|
||||
return {'status': False, 'msg': 'Role does not exist'}
|
25
powerdnsadmin/routes/__init__.py
Normal file
25
powerdnsadmin/routes/__init__.py
Normal file
@ -0,0 +1,25 @@
|
||||
from .base import login_manager, handle_bad_request, handle_unauthorized_access, handle_access_forbidden, handle_page_not_found, handle_internal_server_error
|
||||
|
||||
from .index import index_bp
|
||||
from .user import user_bp
|
||||
from .dashboard import dashboard_bp
|
||||
from .domain import domain_bp
|
||||
from .admin import admin_bp
|
||||
from .api import api_bp
|
||||
|
||||
|
||||
def init_app(app):
|
||||
login_manager.init_app(app)
|
||||
|
||||
app.register_blueprint(index_bp)
|
||||
app.register_blueprint(user_bp)
|
||||
app.register_blueprint(dashboard_bp)
|
||||
app.register_blueprint(domain_bp)
|
||||
app.register_blueprint(admin_bp)
|
||||
app.register_blueprint(api_bp)
|
||||
|
||||
app.register_error_handler(400, handle_bad_request)
|
||||
app.register_error_handler(401, handle_unauthorized_access)
|
||||
app.register_error_handler(403, handle_access_forbidden)
|
||||
app.register_error_handler(404, handle_page_not_found)
|
||||
app.register_error_handler(500, handle_internal_server_error)
|
994
powerdnsadmin/routes/admin.py
Normal file
994
powerdnsadmin/routes/admin.py
Normal file
@ -0,0 +1,994 @@
|
||||
import re
|
||||
import json
|
||||
import traceback
|
||||
import datetime
|
||||
from ast import literal_eval
|
||||
from distutils.version import StrictVersion
|
||||
from flask import Blueprint, render_template, make_response, url_for, current_app, g, session, request, redirect, jsonify, abort
|
||||
from flask_login import login_user, login_required, current_user
|
||||
|
||||
from .base import login_manager
|
||||
from ..decorators import operator_role_required, admin_role_required
|
||||
from ..models.user import User, Anonymous
|
||||
from ..models.account import Account
|
||||
from ..models.account_user import AccountUser
|
||||
from ..models.role import Role
|
||||
from ..models.server import Server
|
||||
from ..models.setting import Setting
|
||||
from ..models.history import History
|
||||
from ..models.domain import Domain
|
||||
from ..models.record import Record
|
||||
from ..models.domain_template import DomainTemplate
|
||||
from ..models.domain_template_record import DomainTemplateRecord
|
||||
|
||||
admin_bp = Blueprint('admin',
|
||||
__name__,
|
||||
template_folder='templates',
|
||||
url_prefix='/admin')
|
||||
|
||||
|
||||
@admin_bp.route('/pdns', methods=['GET'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def pdns_stats():
|
||||
if not Setting().get('pdns_api_url') or not Setting().get(
|
||||
'pdns_api_key') or not Setting().get('pdns_version'):
|
||||
return redirect(url_for('admin.setting_pdns'))
|
||||
|
||||
domains = Domain.query.all()
|
||||
users = User.query.all()
|
||||
|
||||
server = Server(server_id='localhost')
|
||||
configs = server.get_config()
|
||||
statistics = server.get_statistic()
|
||||
history_number = History.query.count()
|
||||
|
||||
if statistics:
|
||||
uptime = list([
|
||||
uptime for uptime in statistics if uptime['name'] == 'uptime'
|
||||
])[0]['value']
|
||||
else:
|
||||
uptime = 0
|
||||
|
||||
return render_template('admin_pdns_stats.html',
|
||||
domains=domains,
|
||||
users=users,
|
||||
configs=configs,
|
||||
statistics=statistics,
|
||||
uptime=uptime,
|
||||
history_number=history_number)
|
||||
|
||||
|
||||
@admin_bp.route('/user/edit/<user_username>', methods=['GET', 'POST'])
|
||||
@admin_bp.route('/user/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def edit_user(user_username=None):
|
||||
if user_username:
|
||||
user = User.query.filter(User.username == user_username).first()
|
||||
create = False
|
||||
|
||||
if not user:
|
||||
return render_template('errors/404.html'), 404
|
||||
|
||||
if user.role.name == 'Administrator' and current_user.role.name != 'Administrator':
|
||||
return render_template('errors/401.html'), 401
|
||||
else:
|
||||
user = None
|
||||
create = True
|
||||
|
||||
if request.method == 'GET':
|
||||
return render_template('admin_edit_user.html',
|
||||
user=user,
|
||||
create=create)
|
||||
|
||||
elif request.method == 'POST':
|
||||
fdata = request.form
|
||||
|
||||
if create:
|
||||
user_username = fdata['username']
|
||||
|
||||
user = User(username=user_username,
|
||||
plain_text_password=fdata['password'],
|
||||
firstname=fdata['firstname'],
|
||||
lastname=fdata['lastname'],
|
||||
email=fdata['email'],
|
||||
reload_info=False)
|
||||
|
||||
if create:
|
||||
if fdata['password'] == "":
|
||||
return render_template('admin_edit_user.html',
|
||||
user=user,
|
||||
create=create,
|
||||
blank_password=True)
|
||||
|
||||
result = user.create_local_user()
|
||||
history = History(msg='Created user {0}'.format(user.username),
|
||||
created_by=current_user.username)
|
||||
|
||||
else:
|
||||
result = user.update_local_user()
|
||||
history = History(msg='Updated user {0}'.format(user.username),
|
||||
created_by=current_user.username)
|
||||
|
||||
if result['status']:
|
||||
history.add()
|
||||
return redirect(url_for('admin.manage_user'))
|
||||
|
||||
return render_template('admin_edit_user.html',
|
||||
user=user,
|
||||
create=create,
|
||||
error=result['msg'])
|
||||
|
||||
|
||||
@admin_bp.route('/manage-user', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def manage_user():
|
||||
if request.method == 'GET':
|
||||
roles = Role.query.all()
|
||||
users = User.query.order_by(User.username).all()
|
||||
return render_template('admin_manage_user.html',
|
||||
users=users,
|
||||
roles=roles)
|
||||
|
||||
if request.method == 'POST':
|
||||
#
|
||||
# post data should in format
|
||||
# {'action': 'delete_user', 'data': 'username'}
|
||||
#
|
||||
try:
|
||||
jdata = request.json
|
||||
data = jdata['data']
|
||||
|
||||
if jdata['action'] == 'user_otp_disable':
|
||||
user = User(username=data)
|
||||
result = user.update_profile(enable_otp=False)
|
||||
if result:
|
||||
history = History(
|
||||
msg='Two factor authentication disabled for user {0}'.
|
||||
format(data),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status':
|
||||
'ok',
|
||||
'msg':
|
||||
'Two factor authentication has been disabled for user.'
|
||||
}), 200)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'Cannot disable two factor authentication for user.'
|
||||
}), 500)
|
||||
|
||||
elif jdata['action'] == 'delete_user':
|
||||
user = User(username=data)
|
||||
if user.username == current_user.username:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'You cannot delete yourself.'
|
||||
}), 400)
|
||||
|
||||
# Remove account associations first
|
||||
user_accounts = Account.query.join(AccountUser).join(
|
||||
User).filter(AccountUser.user_id == user.id,
|
||||
AccountUser.account_id == Account.id).all()
|
||||
for uc in user_accounts:
|
||||
uc.revoke_privileges_by_id(user.id)
|
||||
|
||||
# Then delete the user
|
||||
result = user.delete()
|
||||
if result:
|
||||
history = History(msg='Delete username {0}'.format(data),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': 'User has been removed.'
|
||||
}), 200)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Cannot remove user.'
|
||||
}), 500)
|
||||
|
||||
elif jdata['action'] == 'revoke_user_privileges':
|
||||
user = User(username=data)
|
||||
result = user.revoke_privilege()
|
||||
if result:
|
||||
history = History(
|
||||
msg='Revoke {0} user privileges'.format(data),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': 'Revoked user privileges.'
|
||||
}), 200)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Cannot revoke user privilege.'
|
||||
}), 500)
|
||||
|
||||
elif jdata['action'] == 'update_user_role':
|
||||
username = data['username']
|
||||
role_name = data['role_name']
|
||||
|
||||
if username == current_user.username:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'You cannot change you own roles.'
|
||||
}), 400)
|
||||
|
||||
user = User.query.filter(User.username == username).first()
|
||||
if not user:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'User does not exist.'
|
||||
}), 404)
|
||||
|
||||
if user.role.name == 'Administrator' and current_user.role.name != 'Administrator':
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'You do not have permission to change Administrator users role.'
|
||||
}), 400)
|
||||
|
||||
if role_name == 'Administrator' and current_user.role.name != 'Administrator':
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'You do not have permission to promote a user to Administrator role.'
|
||||
}), 400)
|
||||
|
||||
user = User(username=username)
|
||||
result = user.set_role(role_name)
|
||||
if result['status']:
|
||||
history = History(
|
||||
msg='Change user role of {0} to {1}'.format(
|
||||
username, role_name),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': 'Changed user role successfully.'
|
||||
}), 200)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'Cannot change user role. {0}'.format(
|
||||
result['msg'])
|
||||
}), 500)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Action not supported.'
|
||||
}), 400)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot update user. Error: {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'There is something wrong, please contact Administrator.'
|
||||
}), 400)
|
||||
|
||||
|
||||
@admin_bp.route('/account/edit/<account_name>', methods=['GET', 'POST'])
|
||||
@admin_bp.route('/account/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def edit_account(account_name=None):
|
||||
users = User.query.all()
|
||||
|
||||
if request.method == 'GET':
|
||||
if account_name is None:
|
||||
return render_template('admin_edit_account.html',
|
||||
users=users,
|
||||
create=1)
|
||||
|
||||
else:
|
||||
account = Account.query.filter(
|
||||
Account.name == account_name).first()
|
||||
account_user_ids = account.get_user()
|
||||
return render_template('admin_edit_account.html',
|
||||
account=account,
|
||||
account_user_ids=account_user_ids,
|
||||
users=users,
|
||||
create=0)
|
||||
|
||||
if request.method == 'POST':
|
||||
fdata = request.form
|
||||
new_user_list = request.form.getlist('account_multi_user')
|
||||
|
||||
# on POST, synthesize account and account_user_ids from form data
|
||||
if not account_name:
|
||||
account_name = fdata['accountname']
|
||||
|
||||
account = Account(name=account_name,
|
||||
description=fdata['accountdescription'],
|
||||
contact=fdata['accountcontact'],
|
||||
mail=fdata['accountmail'])
|
||||
account_user_ids = []
|
||||
for username in new_user_list:
|
||||
userid = User(username=username).get_user_info_by_username().id
|
||||
account_user_ids.append(userid)
|
||||
|
||||
create = int(fdata['create'])
|
||||
if create:
|
||||
# account __init__ sanitizes and lowercases the name, so to manage expectations
|
||||
# we let the user reenter the name until it's not empty and it's valid (ignoring the case)
|
||||
if account.name == "" or account.name != account_name.lower():
|
||||
return render_template('admin_edit_account.html',
|
||||
account=account,
|
||||
account_user_ids=account_user_ids,
|
||||
users=users,
|
||||
create=create,
|
||||
invalid_accountname=True)
|
||||
|
||||
if Account.query.filter(Account.name == account.name).first():
|
||||
return render_template('admin_edit_account.html',
|
||||
account=account,
|
||||
account_user_ids=account_user_ids,
|
||||
users=users,
|
||||
create=create,
|
||||
duplicate_accountname=True)
|
||||
|
||||
result = account.create_account()
|
||||
history = History(msg='Create account {0}'.format(account.name),
|
||||
created_by=current_user.username)
|
||||
|
||||
else:
|
||||
result = account.update_account()
|
||||
history = History(msg='Update account {0}'.format(account.name),
|
||||
created_by=current_user.username)
|
||||
|
||||
if result['status']:
|
||||
account.grant_privileges(new_user_list)
|
||||
history.add()
|
||||
return redirect(url_for('admin.manage_account'))
|
||||
|
||||
return render_template('admin_edit_account.html',
|
||||
account=account,
|
||||
account_user_ids=account_user_ids,
|
||||
users=users,
|
||||
create=create,
|
||||
error=result['msg'])
|
||||
|
||||
|
||||
@admin_bp.route('/manage-account', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def manage_account():
|
||||
if request.method == 'GET':
|
||||
accounts = Account.query.order_by(Account.name).all()
|
||||
for account in accounts:
|
||||
account.user_num = AccountUser.query.filter(
|
||||
AccountUser.account_id == account.id).count()
|
||||
return render_template('admin_manage_account.html', accounts=accounts)
|
||||
|
||||
if request.method == 'POST':
|
||||
#
|
||||
# post data should in format
|
||||
# {'action': 'delete_account', 'data': 'accountname'}
|
||||
#
|
||||
try:
|
||||
jdata = request.json
|
||||
data = jdata['data']
|
||||
|
||||
if jdata['action'] == 'delete_account':
|
||||
account = Account.query.filter(Account.name == data).first()
|
||||
if not account:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Account not found.'
|
||||
}), 404)
|
||||
# Remove account association from domains first
|
||||
for domain in account.domains:
|
||||
Domain(name=domain.name).assoc_account(None)
|
||||
# Then delete the account
|
||||
result = account.delete_account()
|
||||
if result:
|
||||
history = History(msg='Delete account {0}'.format(data),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': 'Account has been removed.'
|
||||
}), 200)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Cannot remove account.'
|
||||
}), 500)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Action not supported.'
|
||||
}), 400)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot update account. Error: {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'There is something wrong, please contact Administrator.'
|
||||
}), 400)
|
||||
|
||||
|
||||
@admin_bp.route('/history', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def history():
|
||||
if request.method == 'POST':
|
||||
if current_user.role.name != 'Administrator':
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'You do not have permission to remove history.'
|
||||
}), 401)
|
||||
|
||||
h = History()
|
||||
result = h.remove_all()
|
||||
if result:
|
||||
history = History(msg='Remove all histories',
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': 'Changed user role successfully.'
|
||||
}), 200)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Can not remove histories.'
|
||||
}), 500)
|
||||
|
||||
if request.method == 'GET':
|
||||
histories = History.query.all()
|
||||
return render_template('admin_history.html', histories=histories)
|
||||
|
||||
|
||||
@admin_bp.route('/setting/basic', methods=['GET'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def setting_basic():
|
||||
if request.method == 'GET':
|
||||
settings = [
|
||||
'maintenance', 'fullscreen_layout', 'record_helper',
|
||||
'login_ldap_first', 'default_record_table_size',
|
||||
'default_domain_table_size', 'auto_ptr', 'record_quick_edit',
|
||||
'pretty_ipv6_ptr', 'dnssec_admins_only',
|
||||
'allow_user_create_domain', 'bg_domain_updates', 'site_name',
|
||||
'session_timeout', 'ttl_options', 'pdns_api_timeout'
|
||||
]
|
||||
|
||||
return render_template('admin_setting_basic.html', settings=settings)
|
||||
|
||||
|
||||
@admin_bp.route('/setting/basic/<path:setting>/edit', methods=['POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def setting_basic_edit(setting):
|
||||
jdata = request.json
|
||||
new_value = jdata['value']
|
||||
result = Setting().set(setting, new_value)
|
||||
|
||||
if (result):
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': 'Toggled setting successfully.'
|
||||
}), 200)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Unable to toggle setting.'
|
||||
}), 500)
|
||||
|
||||
|
||||
@admin_bp.route('/setting/basic/<path:setting>/toggle', methods=['POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def setting_basic_toggle(setting):
|
||||
result = Setting().toggle(setting)
|
||||
if (result):
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': 'Toggled setting successfully.'
|
||||
}), 200)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Unable to toggle setting.'
|
||||
}), 500)
|
||||
|
||||
|
||||
@admin_bp.route('/setting/pdns', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@admin_role_required
|
||||
def setting_pdns():
|
||||
if request.method == 'GET':
|
||||
pdns_api_url = Setting().get('pdns_api_url')
|
||||
pdns_api_key = Setting().get('pdns_api_key')
|
||||
pdns_version = Setting().get('pdns_version')
|
||||
return render_template('admin_setting_pdns.html',
|
||||
pdns_api_url=pdns_api_url,
|
||||
pdns_api_key=pdns_api_key,
|
||||
pdns_version=pdns_version)
|
||||
elif request.method == 'POST':
|
||||
pdns_api_url = request.form.get('pdns_api_url')
|
||||
pdns_api_key = request.form.get('pdns_api_key')
|
||||
pdns_version = request.form.get('pdns_version')
|
||||
|
||||
Setting().set('pdns_api_url', pdns_api_url)
|
||||
Setting().set('pdns_api_key', pdns_api_key)
|
||||
Setting().set('pdns_version', pdns_version)
|
||||
|
||||
return render_template('admin_setting_pdns.html',
|
||||
pdns_api_url=pdns_api_url,
|
||||
pdns_api_key=pdns_api_key,
|
||||
pdns_version=pdns_version)
|
||||
|
||||
|
||||
@admin_bp.route('/setting/dns-records', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def setting_records():
|
||||
if request.method == 'GET':
|
||||
_fr = Setting().get('forward_records_allow_edit')
|
||||
_rr = Setting().get('reverse_records_allow_edit')
|
||||
f_records = literal_eval(_fr) if isinstance(_fr, str) else _fr
|
||||
r_records = literal_eval(_rr) if isinstance(_rr, str) else _rr
|
||||
|
||||
return render_template('admin_setting_records.html',
|
||||
f_records=f_records,
|
||||
r_records=r_records)
|
||||
elif request.method == 'POST':
|
||||
fr = {}
|
||||
rr = {}
|
||||
records = Setting().defaults['forward_records_allow_edit']
|
||||
for r in records:
|
||||
fr[r] = True if request.form.get('fr_{0}'.format(
|
||||
r.lower())) else False
|
||||
rr[r] = True if request.form.get('rr_{0}'.format(
|
||||
r.lower())) else False
|
||||
|
||||
Setting().set('forward_records_allow_edit', str(fr))
|
||||
Setting().set('reverse_records_allow_edit', str(rr))
|
||||
return redirect(url_for('admin.setting_records'))
|
||||
|
||||
|
||||
@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
|
||||
|
||||
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 local_db_enabled and not Setting().get('ldap_enabled'):
|
||||
result = {
|
||||
'status':
|
||||
False,
|
||||
'msg':
|
||||
'Local DB and LDAP Authentication can not be disabled at the same time.'
|
||||
}
|
||||
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
|
||||
|
||||
if not ldap_enabled and not Setting().get('local_db_enabled'):
|
||||
result = {
|
||||
'status':
|
||||
False,
|
||||
'msg':
|
||||
'Local DB and LDAP Authentication can not be disabled at the same time.'
|
||||
}
|
||||
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_username',
|
||||
request.form.get('ldap_filter_username'))
|
||||
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'))
|
||||
result = {'status': True, 'msg': 'Saved successfully'}
|
||||
elif conf_type == 'google':
|
||||
Setting().set(
|
||||
'google_oauth_enabled',
|
||||
True if request.form.get('google_oauth_enabled') else False)
|
||||
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':
|
||||
Setting().set(
|
||||
'github_oauth_enabled',
|
||||
True if request.form.get('github_oauth_enabled') else False)
|
||||
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 == 'oidc':
|
||||
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'))
|
||||
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)
|
||||
|
||||
|
||||
@admin_bp.route('/templates', methods=['GET', 'POST'])
|
||||
@admin_bp.route('/templates/list', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def templates():
|
||||
templates = DomainTemplate.query.all()
|
||||
return render_template('template.html', templates=templates)
|
||||
|
||||
|
||||
@admin_bp.route('/template/create', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def create_template():
|
||||
if request.method == 'GET':
|
||||
return render_template('template_add.html')
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
name = request.form.getlist('name')[0]
|
||||
description = request.form.getlist('description')[0]
|
||||
|
||||
if ' ' in name or not name or not type:
|
||||
flash("Please correct your input", 'error')
|
||||
return redirect(url_for('admin.create_template'))
|
||||
|
||||
if DomainTemplate.query.filter(
|
||||
DomainTemplate.name == name).first():
|
||||
flash(
|
||||
"A template with the name {0} already exists!".format(
|
||||
name), 'error')
|
||||
return redirect(url_for('admin.create_template'))
|
||||
|
||||
t = DomainTemplate(name=name, description=description)
|
||||
result = t.create()
|
||||
if result['status'] == 'ok':
|
||||
history = History(msg='Add domain template {0}'.format(name),
|
||||
detail=str({
|
||||
'name': name,
|
||||
'description': description
|
||||
}),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return redirect(url_for('admin.templates'))
|
||||
else:
|
||||
flash(result['msg'], 'error')
|
||||
return redirect(url_for('admin.create_template'))
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot create domain template. Error: {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
abort(500)
|
||||
|
||||
|
||||
@admin_bp.route('/template/create-from-zone', methods=['POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def create_template_from_zone():
|
||||
try:
|
||||
jdata = request.json
|
||||
name = jdata['name']
|
||||
description = jdata['description']
|
||||
domain_name = jdata['domain']
|
||||
|
||||
if ' ' in name or not name or not type:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Please correct template name'
|
||||
}), 400)
|
||||
|
||||
if DomainTemplate.query.filter(DomainTemplate.name == name).first():
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'A template with the name {0} already exists!'.format(name)
|
||||
}), 409)
|
||||
|
||||
t = DomainTemplate(name=name, description=description)
|
||||
result = t.create()
|
||||
if result['status'] == 'ok':
|
||||
history = History(msg='Add domain template {0}'.format(name),
|
||||
detail=str({
|
||||
'name': name,
|
||||
'description': description
|
||||
}),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
|
||||
records = []
|
||||
r = Record()
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if domain:
|
||||
# query domain info from PowerDNS API
|
||||
zone_info = r.get_record_data(domain.name)
|
||||
if zone_info:
|
||||
jrecords = zone_info['records']
|
||||
|
||||
if StrictVersion(Setting().get(
|
||||
'pdns_version')) >= StrictVersion('4.0.0'):
|
||||
for jr in jrecords:
|
||||
if jr['type'] in Setting().get_records_allow_to_edit():
|
||||
name = '@' if jr['name'] == domain_name else re.sub(
|
||||
'\.{}$'.format(domain_name), '', jr['name'])
|
||||
for subrecord in jr['records']:
|
||||
record = DomainTemplateRecord(
|
||||
name=name,
|
||||
type=jr['type'],
|
||||
status=True
|
||||
if subrecord['disabled'] else False,
|
||||
ttl=jr['ttl'],
|
||||
data=subrecord['content'])
|
||||
records.append(record)
|
||||
else:
|
||||
for jr in jrecords:
|
||||
if jr['type'] in Setting().get_records_allow_to_edit():
|
||||
name = '@' if jr['name'] == domain_name else re.sub(
|
||||
'\.{}$'.format(domain_name), '', jr['name'])
|
||||
record = DomainTemplateRecord(
|
||||
name=name,
|
||||
type=jr['type'],
|
||||
status=True if jr['disabled'] else False,
|
||||
ttl=jr['ttl'],
|
||||
data=jr['content'])
|
||||
records.append(record)
|
||||
|
||||
result_records = t.replace_records(records)
|
||||
|
||||
if result_records['status'] == 'ok':
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': result['msg']
|
||||
}), 200)
|
||||
else:
|
||||
t.delete_template()
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': result_records['msg']
|
||||
}), 500)
|
||||
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': result['msg']
|
||||
}), 500)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot create template from zone. Error: {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Error when applying new changes'
|
||||
}), 500)
|
||||
|
||||
|
||||
@admin_bp.route('/template/<path:template>/edit', methods=['GET'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def edit_template(template):
|
||||
try:
|
||||
t = DomainTemplate.query.filter(
|
||||
DomainTemplate.name == template).first()
|
||||
records_allow_to_edit = Setting().get_records_allow_to_edit()
|
||||
quick_edit = Setting().get('record_quick_edit')
|
||||
ttl_options = Setting().get_ttl_options()
|
||||
if t is not None:
|
||||
records = []
|
||||
for jr in t.records:
|
||||
if jr.type in records_allow_to_edit:
|
||||
record = DomainTemplateRecord(
|
||||
name=jr.name,
|
||||
type=jr.type,
|
||||
status='Disabled' if jr.status else 'Active',
|
||||
ttl=jr.ttl,
|
||||
data=jr.data)
|
||||
records.append(record)
|
||||
|
||||
return render_template('template_edit.html',
|
||||
template=t.name,
|
||||
records=records,
|
||||
editable_records=records_allow_to_edit,
|
||||
quick_edit=quick_edit,
|
||||
ttl_options=ttl_options)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot open domain template page. DETAIL: {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
abort(500)
|
||||
return redirect(url_for('admin.templates'))
|
||||
|
||||
|
||||
@admin_bp.route('/template/<path:template>/apply',
|
||||
methods=['POST'],
|
||||
strict_slashes=False)
|
||||
@login_required
|
||||
def apply_records(template):
|
||||
try:
|
||||
jdata = request.json
|
||||
records = []
|
||||
|
||||
for j in jdata['records']:
|
||||
name = '@' if j['record_name'] in ['@', ''] else j['record_name']
|
||||
type = j['record_type']
|
||||
data = j['record_data']
|
||||
disabled = True if j['record_status'] == 'Disabled' else False
|
||||
ttl = int(j['record_ttl']) if j['record_ttl'] else 3600
|
||||
|
||||
dtr = DomainTemplateRecord(name=name,
|
||||
type=type,
|
||||
data=data,
|
||||
status=disabled,
|
||||
ttl=ttl)
|
||||
records.append(dtr)
|
||||
|
||||
t = DomainTemplate.query.filter(
|
||||
DomainTemplate.name == template).first()
|
||||
result = t.replace_records(records)
|
||||
if result['status'] == 'ok':
|
||||
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}'
|
||||
.format(template),
|
||||
detail=str(json.dumps(jdata)),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return make_response(jsonify(result), 200)
|
||||
else:
|
||||
return make_response(jsonify(result), 400)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot apply record changes to the template. Error: {0}'.format(
|
||||
e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Error when applying new changes'
|
||||
}), 500)
|
||||
|
||||
|
||||
@admin_bp.route('/template/<path:template>/delete', methods=['POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def delete_template(template):
|
||||
try:
|
||||
t = DomainTemplate.query.filter(
|
||||
DomainTemplate.name == template).first()
|
||||
if t is not None:
|
||||
result = t.delete_template()
|
||||
if result['status'] == 'ok':
|
||||
history = History(
|
||||
msg='Deleted domain template {0}'.format(template),
|
||||
detail=str({'name': template}),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return redirect(url_for('admin.templates'))
|
||||
else:
|
||||
flash(result['msg'], 'error')
|
||||
return redirect(url_for('admin.templates'))
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot delete template. Error: {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
abort(500)
|
||||
return redirect(url_for('admin.templates'))
|
@ -1,77 +1,97 @@
|
||||
import json
|
||||
from flask import Blueprint, g, request, abort
|
||||
from app.models import Domain, History, Setting, ApiKey
|
||||
from app.lib import utils, helper
|
||||
from app.decorators import api_basic_auth, api_can_create_domain, is_json
|
||||
from app.decorators import apikey_auth, apikey_is_admin
|
||||
from app.decorators import apikey_can_access_domain
|
||||
from app import csrf
|
||||
from app.errors import DomainNotExists, DomainAccessForbidden, RequestIsNotJSON
|
||||
from app.errors import ApiKeyCreateFail, ApiKeyNotUsable, NotEnoughPrivileges
|
||||
from app.schema import ApiKeySchema, DomainSchema, ApiPlainKeySchema
|
||||
from urllib.parse import urljoin
|
||||
from app.lib.log import logging
|
||||
from flask import Blueprint, g, request, abort, current_app
|
||||
from flask_login import current_user
|
||||
|
||||
api_blueprint = Blueprint('api_blueprint', __name__)
|
||||
from ..models.base import db
|
||||
from ..models import Domain, DomainUser, Account, AccountUser, History, Setting, ApiKey
|
||||
from ..lib import utils, helper
|
||||
from ..lib.schema import ApiKeySchema, DomainSchema, ApiPlainKeySchema
|
||||
from ..lib.errors import DomainNotExists, DomainAlreadyExists, DomainAccessForbidden, RequestIsNotJSON, ApiKeyCreateFail, ApiKeyNotUsable, NotEnoughPrivileges
|
||||
from ..decorators import api_basic_auth, api_can_create_domain, is_json, apikey_auth, apikey_is_admin, apikey_can_access_domain
|
||||
|
||||
api_bp = Blueprint('api', __name__, url_prefix='/api/v1')
|
||||
|
||||
apikey_schema = ApiKeySchema(many=True)
|
||||
domain_schema = DomainSchema(many=True)
|
||||
apikey_plain_schema = ApiPlainKeySchema(many=True)
|
||||
|
||||
|
||||
@api_blueprint.errorhandler(400)
|
||||
def get_user_domains():
|
||||
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
|
||||
)).all()
|
||||
return domains
|
||||
|
||||
|
||||
@api_bp.errorhandler(400)
|
||||
def handle_400(err):
|
||||
return json.dumps({"msg": "Bad Request"}), 400
|
||||
|
||||
|
||||
@api_blueprint.errorhandler(401)
|
||||
@api_bp.errorhandler(401)
|
||||
def handle_401(err):
|
||||
return json.dumps({"msg": "Unauthorized"}), 401
|
||||
|
||||
|
||||
@api_blueprint.errorhandler(500)
|
||||
@api_bp.errorhandler(409)
|
||||
def handle_409(err):
|
||||
return json.dumps({"msg": "Conflict"}), 409
|
||||
|
||||
|
||||
@api_bp.errorhandler(500)
|
||||
def handle_500(err):
|
||||
return json.dumps({"msg": "Internal Server Error"}), 500
|
||||
|
||||
|
||||
@api_blueprint.errorhandler(DomainNotExists)
|
||||
@api_bp.errorhandler(DomainNotExists)
|
||||
def handle_domain_not_exists(err):
|
||||
return json.dumps(err.to_dict()), err.status_code
|
||||
|
||||
|
||||
@api_blueprint.errorhandler(DomainAccessForbidden)
|
||||
@api_bp.errorhandler(DomainAlreadyExists)
|
||||
def handle_domain_already_exists(err):
|
||||
return json.dumps(err.to_dict()), err.status_code
|
||||
|
||||
|
||||
@api_bp.errorhandler(DomainAccessForbidden)
|
||||
def handle_domain_access_forbidden(err):
|
||||
return json.dumps(err.to_dict()), err.status_code
|
||||
|
||||
|
||||
@api_blueprint.errorhandler(ApiKeyCreateFail)
|
||||
@api_bp.errorhandler(ApiKeyCreateFail)
|
||||
def handle_apikey_create_fail(err):
|
||||
return json.dumps(err.to_dict()), err.status_code
|
||||
|
||||
|
||||
@api_blueprint.errorhandler(ApiKeyNotUsable)
|
||||
@api_bp.errorhandler(ApiKeyNotUsable)
|
||||
def handle_apikey_not_usable(err):
|
||||
return json.dumps(err.to_dict()), err.status_code
|
||||
|
||||
|
||||
@api_blueprint.errorhandler(NotEnoughPrivileges)
|
||||
@api_bp.errorhandler(NotEnoughPrivileges)
|
||||
def handle_not_enough_privileges(err):
|
||||
return json.dumps(err.to_dict()), err.status_code
|
||||
|
||||
|
||||
@api_blueprint.errorhandler(RequestIsNotJSON)
|
||||
@api_bp.errorhandler(RequestIsNotJSON)
|
||||
def handle_request_is_not_json(err):
|
||||
return json.dumps(err.to_dict()), err.status_code
|
||||
|
||||
|
||||
@api_blueprint.before_request
|
||||
@api_bp.before_request
|
||||
@is_json
|
||||
def before_request():
|
||||
pass
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@api_blueprint.route('/pdnsadmin/zones', methods=['POST'])
|
||||
@api_bp.route('/pdnsadmin/zones', methods=['POST'])
|
||||
@api_basic_auth
|
||||
@api_can_create_domain
|
||||
def api_login_create_zone():
|
||||
@ -85,45 +105,49 @@ def api_login_create_zone():
|
||||
|
||||
msg_str = "Sending request to powerdns API {0}"
|
||||
msg = msg_str.format(request.get_json(force=True))
|
||||
logging.debug(msg)
|
||||
current_app.logger.debug(msg)
|
||||
|
||||
resp = utils.fetch_remote(
|
||||
urljoin(pdns_api_url, api_full_uri),
|
||||
method='POST',
|
||||
data=request.get_json(force=True),
|
||||
headers=headers,
|
||||
accept='application/json; q=1'
|
||||
)
|
||||
try:
|
||||
resp = utils.fetch_remote(urljoin(pdns_api_url, api_full_uri),
|
||||
method='POST',
|
||||
data=request.get_json(force=True),
|
||||
headers=headers,
|
||||
accept='application/json; q=1')
|
||||
except Exception as e:
|
||||
current_app.logger.error("Cannot create domain. Error: {}".format(e))
|
||||
abort(500)
|
||||
|
||||
if resp.status_code == 201:
|
||||
logging.debug("Request to powerdns API successful")
|
||||
current_app.logger.debug("Request to powerdns API successful")
|
||||
data = request.get_json(force=True)
|
||||
|
||||
history = History(
|
||||
msg='Add domain {0}'.format(data['name'].rstrip('.')),
|
||||
detail=json.dumps(data),
|
||||
created_by=g.user.username
|
||||
)
|
||||
history = History(msg='Add domain {0}'.format(
|
||||
data['name'].rstrip('.')),
|
||||
detail=json.dumps(data),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
|
||||
if g.user.role.name not in ['Administrator', 'Operator']:
|
||||
logging.debug("User is ordinary user, assigning created domain")
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
current_app.logger.debug(
|
||||
"User is ordinary user, assigning created domain")
|
||||
domain = Domain(name=data['name'].rstrip('.'))
|
||||
domain.update()
|
||||
domain.grant_privileges([g.user.username])
|
||||
domain.grant_privileges([current_user.id])
|
||||
|
||||
domain = Domain()
|
||||
domain.update()
|
||||
|
||||
if resp.status_code == 409:
|
||||
raise(DomainAlreadyExists)
|
||||
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@api_blueprint.route('/pdnsadmin/zones', methods=['GET'])
|
||||
@api_bp.route('/pdnsadmin/zones', methods=['GET'])
|
||||
@api_basic_auth
|
||||
def api_login_list_zones():
|
||||
if g.user.role.name not in ['Administrator', 'Operator']:
|
||||
domain_obj_list = g.user.get_domains()
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
domain_obj_list = get_user_domains()
|
||||
else:
|
||||
domain_obj_list = Domain.query.all()
|
||||
|
||||
@ -131,11 +155,7 @@ def api_login_list_zones():
|
||||
return json.dumps(domain_schema.dump(domain_obj_list)), 200
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@api_blueprint.route(
|
||||
'/pdnsadmin/zones/<string:domain_name>',
|
||||
methods=['DELETE']
|
||||
)
|
||||
@api_bp.route('/pdnsadmin/zones/<string:domain_name>', methods=['DELETE'])
|
||||
@api_basic_auth
|
||||
@api_can_create_domain
|
||||
def api_login_delete_zone(domain_name):
|
||||
@ -153,45 +173,40 @@ def api_login_delete_zone(domain_name):
|
||||
if not domain:
|
||||
abort(404)
|
||||
|
||||
if g.user.role.name not in ['Administrator', 'Operator']:
|
||||
user_domains_obj_list = g.user.get_domains()
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
user_domains_obj_list = get_user_domains()
|
||||
user_domains_list = [item.name for item in user_domains_obj_list]
|
||||
|
||||
if domain_name not in user_domains_list:
|
||||
raise DomainAccessForbidden()
|
||||
|
||||
msg_str = "Sending request to powerdns API {0}"
|
||||
logging.debug(msg_str.format(domain_name))
|
||||
current_app.logger.debug(msg_str.format(domain_name))
|
||||
|
||||
try:
|
||||
resp = utils.fetch_remote(
|
||||
urljoin(pdns_api_url, api_full_uri),
|
||||
method='DELETE',
|
||||
headers=headers,
|
||||
accept='application/json; q=1'
|
||||
)
|
||||
resp = utils.fetch_remote(urljoin(pdns_api_url, api_full_uri),
|
||||
method='DELETE',
|
||||
headers=headers,
|
||||
accept='application/json; q=1')
|
||||
|
||||
if resp.status_code == 204:
|
||||
logging.debug("Request to powerdns API successful")
|
||||
current_app.logger.debug("Request to powerdns API successful")
|
||||
|
||||
history = History(
|
||||
msg='Delete domain {0}'.format(domain_name),
|
||||
detail='',
|
||||
created_by=g.user.username
|
||||
)
|
||||
history = History(msg='Delete domain {0}'.format(domain_name),
|
||||
detail='',
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
|
||||
domain = Domain()
|
||||
domain.update()
|
||||
except Exception as e:
|
||||
logging.error('Error: {0}'.format(e))
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(500)
|
||||
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@api_blueprint.route('/pdnsadmin/apikeys', methods=['POST'])
|
||||
@api_bp.route('/pdnsadmin/apikeys', methods=['POST'])
|
||||
@api_basic_auth
|
||||
def api_generate_apikey():
|
||||
data = request.get_json()
|
||||
@ -201,7 +216,7 @@ def api_generate_apikey():
|
||||
domain_obj_list = []
|
||||
|
||||
abort(400) if 'domains' not in data else None
|
||||
abort(400) if not isinstance(data['domains'], (list,)) else None
|
||||
abort(400) if not isinstance(data['domains'], (list, )) else None
|
||||
abort(400) if 'role' not in data else None
|
||||
|
||||
description = data['description'] if 'description' in data else None
|
||||
@ -209,98 +224,95 @@ def api_generate_apikey():
|
||||
domains = data['domains']
|
||||
|
||||
if role_name == 'User' and len(domains) == 0:
|
||||
logging.error("Apikey with User role must have domains")
|
||||
current_app.logger.error("Apikey with User role must have domains")
|
||||
raise ApiKeyNotUsable()
|
||||
elif role_name == 'User':
|
||||
domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all()
|
||||
if len(domain_obj_list) == 0:
|
||||
msg = "One of supplied domains does not exists"
|
||||
logging.error(msg)
|
||||
current_app.logger.error(msg)
|
||||
raise DomainNotExists(message=msg)
|
||||
|
||||
if g.user.role.name not in ['Administrator', 'Operator']:
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
# domain list of domain api key should be valid for
|
||||
# if not any domain error
|
||||
# role of api key, user cannot assign role above for api key
|
||||
if role_name != 'User':
|
||||
msg = "User cannot assign other role than User"
|
||||
logging.error(msg)
|
||||
current_app.logger.error(msg)
|
||||
raise NotEnoughPrivileges(message=msg)
|
||||
|
||||
user_domain_obj_list = g.user.get_domains()
|
||||
user_domain_obj_list = get_user_domains()
|
||||
|
||||
domain_list = [item.name for item in domain_obj_list]
|
||||
user_domain_list = [item.name for item in user_domain_obj_list]
|
||||
|
||||
logging.debug("Input domain list: {0}".format(domain_list))
|
||||
logging.debug("User domain list: {0}".format(user_domain_list))
|
||||
current_app.logger.debug("Input domain list: {0}".format(domain_list))
|
||||
current_app.logger.debug(
|
||||
"User domain 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"
|
||||
logging.error(msg)
|
||||
current_app.logger.error(msg)
|
||||
raise DomainAccessForbidden(message=msg)
|
||||
|
||||
apikey = ApiKey(
|
||||
desc=description,
|
||||
role_name=role_name,
|
||||
domains=domain_obj_list
|
||||
)
|
||||
apikey = ApiKey(desc=description,
|
||||
role_name=role_name,
|
||||
domains=domain_obj_list)
|
||||
|
||||
try:
|
||||
apikey.create()
|
||||
except Exception as e:
|
||||
logging.error('Error: {0}'.format(e))
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
raise ApiKeyCreateFail(message='Api key create failed')
|
||||
|
||||
return json.dumps(apikey_plain_schema.dump([apikey])), 201
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@api_blueprint.route('/pdnsadmin/apikeys', defaults={'domain_name': None})
|
||||
@api_blueprint.route('/pdnsadmin/apikeys/<string:domain_name>')
|
||||
@api_bp.route('/pdnsadmin/apikeys', defaults={'domain_name': None})
|
||||
@api_bp.route('/pdnsadmin/apikeys/<string:domain_name>')
|
||||
@api_basic_auth
|
||||
def api_get_apikeys(domain_name):
|
||||
apikeys = []
|
||||
logging.debug("Getting apikeys")
|
||||
current_app.logger.debug("Getting apikeys")
|
||||
|
||||
if g.user.role.name not in ['Administrator', 'Operator']:
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
if domain_name:
|
||||
msg = "Check if domain {0} exists and \
|
||||
is allowed for user." . format(domain_name)
|
||||
logging.debug(msg)
|
||||
apikeys = g.user.get_apikeys(domain_name)
|
||||
is allowed for user." .format(domain_name)
|
||||
current_app.logger.debug(msg)
|
||||
apikeys = current_user.get_apikeys(domain_name)
|
||||
|
||||
if not apikeys:
|
||||
raise DomainAccessForbidden(name=domain_name)
|
||||
|
||||
logging.debug(apikey_schema.dump(apikeys))
|
||||
current_app.logger.debug(apikey_schema.dump(apikeys))
|
||||
else:
|
||||
msg_str = "Getting all allowed domains for user {0}"
|
||||
msg = msg_str . format(g.user.username)
|
||||
logging.debug(msg)
|
||||
msg = msg_str.format(current_user.username)
|
||||
current_app.logger.debug(msg)
|
||||
|
||||
try:
|
||||
apikeys = g.user.get_apikeys()
|
||||
logging.debug(apikey_schema.dump(apikeys))
|
||||
apikeys = current_user.get_apikeys()
|
||||
current_app.logger.debug(apikey_schema.dump(apikeys))
|
||||
except Exception as e:
|
||||
logging.error('Error: {0}'.format(e))
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(500)
|
||||
else:
|
||||
logging.debug("Getting all domains for administrative user")
|
||||
current_app.logger.debug("Getting all domains for administrative user")
|
||||
try:
|
||||
apikeys = ApiKey.query.all()
|
||||
logging.debug(apikey_schema.dump(apikeys))
|
||||
current_app.logger.debug(apikey_schema.dump(apikeys))
|
||||
except Exception as e:
|
||||
logging.error('Error: {0}'.format(e))
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(500)
|
||||
|
||||
return json.dumps(apikey_schema.dump(apikeys)), 200
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@api_blueprint.route('/pdnsadmin/apikeys/<int:apikey_id>', methods=['DELETE'])
|
||||
@api_bp.route('/pdnsadmin/apikeys/<int:apikey_id>', methods=['DELETE'])
|
||||
@api_basic_auth
|
||||
def api_delete_apikey(apikey_id):
|
||||
apikey = ApiKey.query.get(apikey_id)
|
||||
@ -308,11 +320,11 @@ def api_delete_apikey(apikey_id):
|
||||
if not apikey:
|
||||
abort(404)
|
||||
|
||||
logging.debug(g.user.role.name)
|
||||
current_app.logger.debug(current_user.role.name)
|
||||
|
||||
if g.user.role.name not in ['Administrator', 'Operator']:
|
||||
apikeys = g.user.get_apikeys()
|
||||
user_domains_obj_list = g.user.get_domain().all()
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
apikeys = current_user.get_apikeys()
|
||||
user_domains_obj_list = current_user.get_domain().all()
|
||||
apikey_domains_obj_list = apikey.domains
|
||||
user_domains_list = [item.name for item in user_domains_obj_list]
|
||||
apikey_domains_list = [item.name for item in apikey_domains_obj_list]
|
||||
@ -322,7 +334,7 @@ def api_delete_apikey(apikey_id):
|
||||
|
||||
if not (len(inter) == len(apikey_domains_list)):
|
||||
msg = "You don't have access to some domains apikey belongs to"
|
||||
logging.error(msg)
|
||||
current_app.logger.error(msg)
|
||||
raise DomainAccessForbidden(message=msg)
|
||||
|
||||
if apikey_id not in apikeys_ids:
|
||||
@ -331,14 +343,13 @@ def api_delete_apikey(apikey_id):
|
||||
try:
|
||||
apikey.delete()
|
||||
except Exception as e:
|
||||
logging.error('Error: {0}'.format(e))
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(500)
|
||||
|
||||
return '', 204
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@api_blueprint.route('/pdnsadmin/apikeys/<int:apikey_id>', methods=['PUT'])
|
||||
@api_bp.route('/pdnsadmin/apikeys/<int:apikey_id>', methods=['PUT'])
|
||||
@api_basic_auth
|
||||
def api_update_apikey(apikey_id):
|
||||
# if role different and user is allowed to change it, update
|
||||
@ -355,80 +366,78 @@ def api_update_apikey(apikey_id):
|
||||
if not apikey:
|
||||
abort(404)
|
||||
|
||||
logging.debug('Updating apikey with id {0}'.format(apikey_id))
|
||||
current_app.logger.debug('Updating apikey with id {0}'.format(apikey_id))
|
||||
|
||||
if role_name == 'User' and len(domains) == 0:
|
||||
logging.error("Apikey with User role must have domains")
|
||||
current_app.logger.error("Apikey with User role must have domains")
|
||||
raise ApiKeyNotUsable()
|
||||
elif role_name == 'User':
|
||||
domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all()
|
||||
if len(domain_obj_list) == 0:
|
||||
msg = "One of supplied domains does not exists"
|
||||
logging.error(msg)
|
||||
current_app.logger.error(msg)
|
||||
raise DomainNotExists(message=msg)
|
||||
|
||||
if g.user.role.name not in ['Administrator', 'Operator']:
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
if role_name != 'User':
|
||||
msg = "User cannot assign other role than User"
|
||||
logging.error(msg)
|
||||
current_app.logger.error(msg)
|
||||
raise NotEnoughPrivileges(message=msg)
|
||||
|
||||
apikeys = g.user.get_apikeys()
|
||||
apikeys = current_user.get_apikeys()
|
||||
apikey_domains = [item.name for item in apikey.domains]
|
||||
apikeys_ids = [apikey_item.id for apikey_item in apikeys]
|
||||
|
||||
user_domain_obj_list = g.user.get_domain().all()
|
||||
user_domain_obj_list = current_user.get_domain().all()
|
||||
|
||||
domain_list = [item.name for item in domain_obj_list]
|
||||
user_domain_list = [item.name for item in user_domain_obj_list]
|
||||
|
||||
logging.debug("Input domain list: {0}".format(domain_list))
|
||||
logging.debug("User domain list: {0}".format(user_domain_list))
|
||||
current_app.logger.debug("Input domain list: {0}".format(domain_list))
|
||||
current_app.logger.debug(
|
||||
"User domain 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"
|
||||
logging.error(msg)
|
||||
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'
|
||||
logging.error(msg)
|
||||
current_app.logger.error(msg)
|
||||
raise DomainAccessForbidden()
|
||||
|
||||
if set(domains) == set(apikey_domains):
|
||||
logging.debug("Domains are same, apikey domains won't be updated")
|
||||
current_app.logger.debug(
|
||||
"Domains are same, apikey domains won't be updated")
|
||||
domains = None
|
||||
|
||||
if role_name == apikey.role:
|
||||
logging.debug("Role is same, apikey role won't be updated")
|
||||
current_app.logger.debug("Role is same, apikey role won't be updated")
|
||||
role_name = None
|
||||
|
||||
if description == apikey.description:
|
||||
msg = "Description is same, apikey description won't be updated"
|
||||
logging.debug(msg)
|
||||
current_app.logger.debug(msg)
|
||||
description = None
|
||||
|
||||
try:
|
||||
apikey = ApiKey.query.get(apikey_id)
|
||||
apikey.update(
|
||||
role_name=role_name,
|
||||
domains=domains,
|
||||
description=description
|
||||
)
|
||||
apikey.update(role_name=role_name,
|
||||
domains=domains,
|
||||
description=description)
|
||||
except Exception as e:
|
||||
logging.error('Error: {0}'.format(e))
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(500)
|
||||
|
||||
return '', 204
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@api_blueprint.route(
|
||||
@api_bp.route(
|
||||
'/servers/<string:server_id>/zones/<string:zone_id>/<path:subpath>',
|
||||
methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
)
|
||||
methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
|
||||
@apikey_auth
|
||||
@apikey_can_access_domain
|
||||
def api_zone_subpath_forward(server_id, zone_id, subpath):
|
||||
@ -436,11 +445,8 @@ def api_zone_subpath_forward(server_id, zone_id, subpath):
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@api_blueprint.route(
|
||||
'/servers/<string:server_id>/zones/<string:zone_id>',
|
||||
methods=['GET', 'PUT', 'PATCH', 'DELETE']
|
||||
)
|
||||
@api_bp.route('/servers/<string:server_id>/zones/<string:zone_id>',
|
||||
methods=['GET', 'PUT', 'PATCH', 'DELETE'])
|
||||
@apikey_auth
|
||||
@apikey_can_access_domain
|
||||
def api_zone_forward(server_id, zone_id):
|
||||
@ -450,10 +456,7 @@ def api_zone_forward(server_id, zone_id):
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@api_blueprint.route(
|
||||
'/servers',
|
||||
methods=['GET']
|
||||
)
|
||||
@api_bp.route('/servers', methods=['GET'])
|
||||
@apikey_auth
|
||||
@apikey_is_admin
|
||||
def api_server_forward():
|
||||
@ -461,10 +464,7 @@ def api_server_forward():
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@api_blueprint.route(
|
||||
'/servers/<path:subpath>',
|
||||
methods=['GET', 'PUT']
|
||||
)
|
||||
@api_bp.route('/servers/<path:subpath>', methods=['GET', 'PUT'])
|
||||
@apikey_auth
|
||||
@apikey_is_admin
|
||||
def api_server_sub_forward(subpath):
|
||||
@ -472,25 +472,24 @@ def api_server_sub_forward(subpath):
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@api_blueprint.route('/servers/<string:server_id>/zones', methods=['POST'])
|
||||
@api_bp.route('/servers/<string:server_id>/zones', methods=['POST'])
|
||||
@apikey_auth
|
||||
def api_create_zone(server_id):
|
||||
resp = helper.forward_request()
|
||||
|
||||
if resp.status_code == 201:
|
||||
logging.debug("Request to powerdns API successful")
|
||||
current_app.logger.debug("Request to powerdns API successful")
|
||||
data = request.get_json(force=True)
|
||||
|
||||
history = History(
|
||||
msg='Add domain {0}'.format(data['name'].rstrip('.')),
|
||||
detail=json.dumps(data),
|
||||
created_by=g.apikey.description
|
||||
)
|
||||
history = History(msg='Add domain {0}'.format(
|
||||
data['name'].rstrip('.')),
|
||||
detail=json.dumps(data),
|
||||
created_by=g.apikey.description)
|
||||
history.add()
|
||||
|
||||
if g.apikey.role.name not in ['Administrator', 'Operator']:
|
||||
logging.debug("Apikey is user key, assigning created domain")
|
||||
current_app.logger.debug(
|
||||
"Apikey is user key, assigning created domain")
|
||||
domain = Domain(name=data['name'].rstrip('.'))
|
||||
g.apikey.domains.append(domain)
|
||||
|
||||
@ -500,19 +499,22 @@ def api_create_zone(server_id):
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@api_blueprint.route('/servers/<string:server_id>/zones', methods=['GET'])
|
||||
@api_bp.route('/servers/<string:server_id>/zones', methods=['GET'])
|
||||
@apikey_auth
|
||||
def api_get_zones(server_id):
|
||||
if g.apikey.role.name not in ['Administrator', 'Operator']:
|
||||
domain_obj_list = g.apikey.domains
|
||||
if server_id == 'pdnsadmin':
|
||||
if g.apikey.role.name not in ['Administrator', 'Operator']:
|
||||
domain_obj_list = g.apikey.domains
|
||||
else:
|
||||
domain_obj_list = Domain.query.all()
|
||||
return json.dumps(domain_schema.dump(domain_obj_list)), 200
|
||||
else:
|
||||
domain_obj_list = Domain.query.all()
|
||||
return json.dumps(domain_schema.dump(domain_obj_list)), 200
|
||||
resp = helper.forward_request()
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
#endpoint to snychronize Domains in background
|
||||
@csrf.exempt
|
||||
@api_blueprint.route('/sync_domains', methods=['GET'])
|
||||
|
||||
# The endpoint to snychronize Domains in background
|
||||
@api_bp.route('/sync_domains', methods=['GET'])
|
||||
@apikey_auth
|
||||
def sync_domains():
|
||||
domain = Domain()
|
65
powerdnsadmin/routes/base.py
Normal file
65
powerdnsadmin/routes/base.py
Normal file
@ -0,0 +1,65 @@
|
||||
import base64
|
||||
from flask import render_template, url_for, redirect, session, request, current_app
|
||||
from flask_login import LoginManager, login_user
|
||||
|
||||
from ..models.user import User
|
||||
|
||||
login_manager = LoginManager()
|
||||
|
||||
|
||||
def handle_bad_request(e):
|
||||
return render_template('errors/400.html', code=400, message=e), 400
|
||||
|
||||
|
||||
def handle_unauthorized_access(e):
|
||||
session['next'] = request.script_root + request.path
|
||||
return redirect(url_for('index.login'))
|
||||
|
||||
|
||||
def handle_access_forbidden(e):
|
||||
return render_template('errors/403.html', code=403, message=e), 403
|
||||
|
||||
|
||||
def handle_page_not_found(e):
|
||||
return render_template('errors/404.html', code=404, message=e), 404
|
||||
|
||||
|
||||
def handle_internal_server_error(e):
|
||||
return render_template('errors/500.html', code=500, message=e), 500
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(id):
|
||||
"""
|
||||
This will be current_user
|
||||
"""
|
||||
return User.query.get(int(id))
|
||||
|
||||
|
||||
@login_manager.request_loader
|
||||
def login_via_authorization_header(request):
|
||||
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 TypeError as e:
|
||||
return None
|
||||
user = User(username=username,
|
||||
password=password,
|
||||
plain_text_password=password)
|
||||
try:
|
||||
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 auth == False:
|
||||
return None
|
||||
else:
|
||||
# login_user(user, remember=False)
|
||||
return User.query.filter(User.id==user.id).first()
|
||||
except Exception as e:
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
return None
|
||||
return None
|
166
powerdnsadmin/routes/dashboard.py
Normal file
166
powerdnsadmin/routes/dashboard.py
Normal file
@ -0,0 +1,166 @@
|
||||
from flask import Blueprint, render_template, make_response, url_for, current_app, request, jsonify, redirect
|
||||
from flask_login import login_required, current_user
|
||||
from sqlalchemy import not_, or_
|
||||
|
||||
from ..lib.utils import customBoxes
|
||||
from ..models.user import User
|
||||
from ..models.account import Account
|
||||
from ..models.account_user import AccountUser
|
||||
from ..models.domain import Domain
|
||||
from ..models.domain_user import DomainUser
|
||||
from ..models.setting import Setting
|
||||
from ..models.history import History
|
||||
from ..models.server import Server
|
||||
from ..models.base import db
|
||||
|
||||
dashboard_bp = Blueprint('dashboard',
|
||||
__name__,
|
||||
template_folder='templates',
|
||||
url_prefix='/dashboard')
|
||||
|
||||
|
||||
@dashboard_bp.route('/domains-custom/<path:boxId>', methods=['GET'])
|
||||
@login_required
|
||||
def domains_custom(boxId):
|
||||
if current_user.role.name in ['Administrator', 'Operator']:
|
||||
domains = Domain.query
|
||||
else:
|
||||
# Get query for domain to which the user has access permission.
|
||||
# This includes direct domain permission AND permission through
|
||||
# account membership
|
||||
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
|
||||
))
|
||||
|
||||
template = current_app.jinja_env.get_template("dashboard_domain.html")
|
||||
render = template.make_module(vars={"current_user": current_user})
|
||||
|
||||
columns = [
|
||||
Domain.name, Domain.dnssec, Domain.type, Domain.serial, Domain.master,
|
||||
Domain.account
|
||||
]
|
||||
# History.created_on.desc()
|
||||
order_by = []
|
||||
for i in range(len(columns)):
|
||||
column_index = request.args.get("order[{0}][column]".format(i))
|
||||
sort_direction = request.args.get("order[{0}][dir]".format(i))
|
||||
if column_index is None:
|
||||
break
|
||||
if sort_direction != "asc" and sort_direction != "desc":
|
||||
sort_direction = "asc"
|
||||
|
||||
column = columns[int(column_index)]
|
||||
order_by.append(getattr(column, sort_direction)())
|
||||
|
||||
if order_by:
|
||||
domains = domains.order_by(*order_by)
|
||||
|
||||
if boxId == "reverse":
|
||||
for boxId in customBoxes.order:
|
||||
if boxId == "reverse": continue
|
||||
domains = domains.filter(
|
||||
not_(Domain.name.ilike(customBoxes.boxes[boxId][1])))
|
||||
else:
|
||||
domains = domains.filter(Domain.name.ilike(
|
||||
customBoxes.boxes[boxId][1]))
|
||||
|
||||
total_count = domains.count()
|
||||
|
||||
search = request.args.get("search[value]")
|
||||
if search:
|
||||
start = "" if search.startswith("^") else "%"
|
||||
end = "" if search.endswith("$") else "%"
|
||||
|
||||
if current_user.role.name in ['Administrator', 'Operator']:
|
||||
domains = domains.outerjoin(Account).filter(
|
||||
Domain.name.ilike(start + search.strip("^$") + end)
|
||||
| Account.name.ilike(start + search.strip("^$") + end)
|
||||
| Account.description.ilike(start + search.strip("^$") + end))
|
||||
else:
|
||||
domains = domains.filter(
|
||||
Domain.name.ilike(start + search.strip("^$") + end))
|
||||
|
||||
filtered_count = domains.count()
|
||||
|
||||
start = int(request.args.get("start", 0))
|
||||
length = min(int(request.args.get("length", 0)), 100)
|
||||
|
||||
if length != -1:
|
||||
domains = domains[start:start + length]
|
||||
|
||||
data = []
|
||||
for domain in domains:
|
||||
data.append([
|
||||
render.name(domain),
|
||||
render.dnssec(domain),
|
||||
render.type(domain),
|
||||
render.serial(domain),
|
||||
render.master(domain),
|
||||
render.account(domain),
|
||||
render.actions(domain),
|
||||
])
|
||||
|
||||
response_data = {
|
||||
"draw": int(request.args.get("draw", 0)),
|
||||
"recordsTotal": total_count,
|
||||
"recordsFiltered": filtered_count,
|
||||
"data": data,
|
||||
}
|
||||
return jsonify(response_data)
|
||||
|
||||
|
||||
@dashboard_bp.route('/', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def dashboard():
|
||||
if not Setting().get('pdns_api_url') or not Setting().get(
|
||||
'pdns_api_key') or not Setting().get('pdns_version'):
|
||||
return redirect(url_for('admin.setting_pdns'))
|
||||
|
||||
BG_DOMAIN_UPDATE = Setting().get('bg_domain_updates')
|
||||
if not BG_DOMAIN_UPDATE:
|
||||
current_app.logger.info('Updating domains in foreground...')
|
||||
Domain().update()
|
||||
else:
|
||||
current_app.logger.info('Updating domains in background...')
|
||||
|
||||
# Stats for dashboard
|
||||
domain_count = Domain.query.count()
|
||||
user_num = User.query.count()
|
||||
history_number = History.query.count()
|
||||
history = History.query.order_by(History.created_on.desc()).limit(4)
|
||||
server = Server(server_id='localhost')
|
||||
statistics = server.get_statistic()
|
||||
if statistics:
|
||||
uptime = list([
|
||||
uptime for uptime in statistics if uptime['name'] == 'uptime'
|
||||
])[0]['value']
|
||||
else:
|
||||
uptime = 0
|
||||
|
||||
# Add custom boxes to render_template
|
||||
return render_template('dashboard.html',
|
||||
custom_boxes=customBoxes,
|
||||
domain_count=domain_count,
|
||||
user_num=user_num,
|
||||
history_number=history_number,
|
||||
uptime=uptime,
|
||||
histories=history,
|
||||
show_bg_domain_button=BG_DOMAIN_UPDATE)
|
||||
|
||||
|
||||
@dashboard_bp.route('/domains-updater', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def domains_updater():
|
||||
current_app.logger.debug('Update domains in background')
|
||||
d = Domain().update()
|
||||
|
||||
response_data = {
|
||||
"result": d,
|
||||
}
|
||||
return jsonify(response_data)
|
525
powerdnsadmin/routes/domain.py
Normal file
525
powerdnsadmin/routes/domain.py
Normal file
@ -0,0 +1,525 @@
|
||||
import re
|
||||
import json
|
||||
import traceback
|
||||
import datetime
|
||||
from distutils.version import StrictVersion
|
||||
from flask import Blueprint, render_template, make_response, url_for, current_app, g, session, request, redirect, abort, jsonify
|
||||
from flask_login import login_user, login_required, current_user
|
||||
|
||||
from .base import login_manager
|
||||
from ..decorators import can_create_domain, operator_role_required, can_access_domain, can_configure_dnssec
|
||||
from ..models.user import User, Anonymous
|
||||
from ..models.account import Account
|
||||
from ..models.setting import Setting
|
||||
from ..models.history import History
|
||||
from ..models.domain import Domain
|
||||
from ..models.record import Record
|
||||
from ..models.record_entry import RecordEntry
|
||||
from ..models.domain_template import DomainTemplate
|
||||
from ..models.domain_template_record import DomainTemplateRecord
|
||||
from ..models.domain_setting import DomainSetting
|
||||
|
||||
domain_bp = Blueprint('domain',
|
||||
__name__,
|
||||
template_folder='templates',
|
||||
url_prefix='/domain')
|
||||
|
||||
|
||||
@domain_bp.route('/<path:domain_name>', methods=['GET'])
|
||||
@login_required
|
||||
@can_access_domain
|
||||
def domain(domain_name):
|
||||
r = Record()
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if not domain:
|
||||
abort(404)
|
||||
|
||||
# query domain info from PowerDNS API
|
||||
zone_info = r.get_record_data(domain.name)
|
||||
if zone_info:
|
||||
jrecords = zone_info['records']
|
||||
else:
|
||||
# can not get any record, API server might be down
|
||||
abort(500)
|
||||
|
||||
quick_edit = Setting().get('record_quick_edit')
|
||||
records_allow_to_edit = Setting().get_records_allow_to_edit()
|
||||
forward_records_allow_to_edit = Setting(
|
||||
).get_forward_records_allow_to_edit()
|
||||
reverse_records_allow_to_edit = Setting(
|
||||
).get_reverse_records_allow_to_edit()
|
||||
ttl_options = Setting().get_ttl_options()
|
||||
records = []
|
||||
|
||||
if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'):
|
||||
for jr in jrecords:
|
||||
if jr['type'] in records_allow_to_edit:
|
||||
for subrecord in jr['records']:
|
||||
record = RecordEntry(name=jr['name'],
|
||||
type=jr['type'],
|
||||
status='Disabled' if
|
||||
subrecord['disabled'] else 'Active',
|
||||
ttl=jr['ttl'],
|
||||
data=subrecord['content'],
|
||||
is_allowed_edit=True)
|
||||
records.append(record)
|
||||
if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name):
|
||||
editable_records = forward_records_allow_to_edit
|
||||
else:
|
||||
editable_records = reverse_records_allow_to_edit
|
||||
return render_template('domain.html',
|
||||
domain=domain,
|
||||
records=records,
|
||||
editable_records=editable_records,
|
||||
quick_edit=quick_edit,
|
||||
ttl_options=ttl_options)
|
||||
else:
|
||||
for jr in jrecords:
|
||||
if jr['type'] in records_allow_to_edit:
|
||||
record = RecordEntry(
|
||||
name=jr['name'],
|
||||
type=jr['type'],
|
||||
status='Disabled' if jr['disabled'] else 'Active',
|
||||
ttl=jr['ttl'],
|
||||
data=jr['content'],
|
||||
is_allowed_edit=True)
|
||||
records.append(record)
|
||||
if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name):
|
||||
editable_records = forward_records_allow_to_edit
|
||||
else:
|
||||
editable_records = reverse_records_allow_to_edit
|
||||
return render_template('domain.html',
|
||||
domain=domain,
|
||||
records=records,
|
||||
editable_records=editable_records,
|
||||
quick_edit=quick_edit,
|
||||
ttl_options=ttl_options)
|
||||
|
||||
|
||||
@domain_bp.route('/add', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@can_create_domain
|
||||
def add():
|
||||
templates = DomainTemplate.query.all()
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
domain_name = request.form.getlist('domain_name')[0]
|
||||
domain_type = request.form.getlist('radio_type')[0]
|
||||
domain_template = request.form.getlist('domain_template')[0]
|
||||
soa_edit_api = request.form.getlist('radio_type_soa_edit_api')[0]
|
||||
account_id = request.form.getlist('accountid')[0]
|
||||
|
||||
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
|
||||
|
||||
if domain_type == 'slave':
|
||||
if request.form.getlist('domain_master_address'):
|
||||
domain_master_string = request.form.getlist(
|
||||
'domain_master_address')[0]
|
||||
domain_master_string = domain_master_string.replace(
|
||||
' ', '')
|
||||
domain_master_ips = domain_master_string.split(',')
|
||||
else:
|
||||
domain_master_ips = []
|
||||
|
||||
account_name = Account().get_name_by_id(account_id)
|
||||
|
||||
d = Domain()
|
||||
result = d.add(domain_name=domain_name,
|
||||
domain_type=domain_type,
|
||||
soa_edit_api=soa_edit_api,
|
||||
domain_master_ips=domain_master_ips,
|
||||
account_name=account_name)
|
||||
if result['status'] == 'ok':
|
||||
history = History(msg='Add domain {0}'.format(domain_name),
|
||||
detail=str({
|
||||
'domain_type': domain_type,
|
||||
'domain_master_ips': domain_master_ips,
|
||||
'account_id': account_id
|
||||
}),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
|
||||
# grant user access to the domain
|
||||
Domain(name=domain_name).grant_privileges(
|
||||
[current_user.id])
|
||||
|
||||
# apply template if needed
|
||||
if domain_template != '0':
|
||||
template = DomainTemplate.query.filter(
|
||||
DomainTemplate.id == domain_template).first()
|
||||
template_records = DomainTemplateRecord.query.filter(
|
||||
DomainTemplateRecord.template_id ==
|
||||
domain_template).all()
|
||||
record_data = []
|
||||
for template_record in template_records:
|
||||
record_row = {
|
||||
'record_data': template_record.data,
|
||||
'record_name': template_record.name,
|
||||
'record_status': template_record.status,
|
||||
'record_ttl': template_record.ttl,
|
||||
'record_type': template_record.type
|
||||
}
|
||||
record_data.append(record_row)
|
||||
r = Record()
|
||||
result = r.apply(domain_name, record_data)
|
||||
if result['status'] == 'ok':
|
||||
history = History(
|
||||
msg=
|
||||
'Applying template {0} to {1}, created records successfully.'
|
||||
.format(template.name, domain_name),
|
||||
detail=str(result),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
else:
|
||||
history = History(
|
||||
msg=
|
||||
'Applying template {0} to {1}, FAILED to created records.'
|
||||
.format(template.name, domain_name),
|
||||
detail=str(result),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return redirect(url_for('dashboard.dashboard'))
|
||||
else:
|
||||
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.debug(traceback.format_exc())
|
||||
abort(500)
|
||||
|
||||
else:
|
||||
accounts = Account.query.all()
|
||||
return render_template('domain_add.html',
|
||||
templates=templates,
|
||||
accounts=accounts)
|
||||
|
||||
|
||||
@domain_bp.route('/setting/<path:domain_name>/delete', methods=['POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def delete(domain_name):
|
||||
d = Domain()
|
||||
result = d.delete(domain_name)
|
||||
|
||||
if result['status'] == 'error':
|
||||
abort(500)
|
||||
|
||||
history = History(msg='Delete domain {0}'.format(domain_name),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
|
||||
return redirect(url_for('dashboard.dashboard'))
|
||||
|
||||
|
||||
@domain_bp.route('/setting/<path:domain_name>/manage', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def setting(domain_name):
|
||||
if request.method == 'GET':
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if not domain:
|
||||
abort(404)
|
||||
users = User.query.all()
|
||||
accounts = Account.query.all()
|
||||
|
||||
# get list of user ids to initialize selection data
|
||||
d = Domain(name=domain_name)
|
||||
domain_user_ids = d.get_user()
|
||||
account = d.get_account()
|
||||
|
||||
return render_template('domain_setting.html',
|
||||
domain=domain,
|
||||
users=users,
|
||||
domain_user_ids=domain_user_ids,
|
||||
accounts=accounts,
|
||||
domain_account=account)
|
||||
|
||||
if request.method == 'POST':
|
||||
# username in right column
|
||||
new_user_list = request.form.getlist('domain_multi_user[]')
|
||||
new_user_ids = [
|
||||
user.id for user in User.query.filter(
|
||||
User.username.in_(new_user_list)).all() if user
|
||||
]
|
||||
|
||||
# grant/revoke user privileges
|
||||
d = Domain(name=domain_name)
|
||||
d.grant_privileges(new_user_ids)
|
||||
|
||||
history = History(
|
||||
msg='Change domain {0} access control'.format(domain_name),
|
||||
detail=str({'user_has_access': new_user_list}),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
|
||||
return redirect(url_for('domain.setting', domain_name=domain_name))
|
||||
|
||||
|
||||
@domain_bp.route('/setting/<path:domain_name>/change_soa_setting',
|
||||
methods=['POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def change_soa_edit_api(domain_name):
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if not domain:
|
||||
abort(404)
|
||||
new_setting = request.form.get('soa_edit_api')
|
||||
if new_setting is None:
|
||||
abort(500)
|
||||
if new_setting == '0':
|
||||
return redirect(url_for('domain.setting', domain_name=domain_name))
|
||||
|
||||
d = Domain()
|
||||
status = d.update_soa_setting(domain_name=domain_name,
|
||||
soa_edit_api=new_setting)
|
||||
if status['status'] != None:
|
||||
users = User.query.all()
|
||||
accounts = Account.query.all()
|
||||
d = Domain(name=domain_name)
|
||||
domain_user_ids = d.get_user()
|
||||
account = d.get_account()
|
||||
return render_template('domain_setting.html',
|
||||
domain=domain,
|
||||
users=users,
|
||||
domain_user_ids=domain_user_ids,
|
||||
accounts=accounts,
|
||||
domain_account=account)
|
||||
else:
|
||||
abort(500)
|
||||
|
||||
|
||||
@domain_bp.route('/setting/<path:domain_name>/change_account',
|
||||
methods=['POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def change_account(domain_name):
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if not domain:
|
||||
abort(404)
|
||||
|
||||
account_id = request.form.get('accountid')
|
||||
status = Domain(name=domain.name).assoc_account(account_id)
|
||||
if status['status']:
|
||||
return redirect(url_for('domain.setting', domain_name=domain.name))
|
||||
else:
|
||||
abort(500)
|
||||
|
||||
|
||||
@domain_bp.route('/<path:domain_name>/apply',
|
||||
methods=['POST'],
|
||||
strict_slashes=False)
|
||||
@login_required
|
||||
@can_access_domain
|
||||
def record_apply(domain_name):
|
||||
#TODO: filter removed records / name modified records.
|
||||
|
||||
try:
|
||||
jdata = request.json
|
||||
submitted_serial = jdata['serial']
|
||||
submitted_record = jdata['record']
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
current_app.logger.debug(
|
||||
'Your submitted serial: {0}'.format(submitted_serial))
|
||||
current_app.logger.debug('Current domain serial: {0}'.format(
|
||||
domain.serial))
|
||||
|
||||
if domain:
|
||||
if int(submitted_serial) != domain.serial:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'The zone has been changed by another session or user. Please refresh this web page to load updated records.'
|
||||
}), 500)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'Domain name {0} does not exist'.format(domain_name)
|
||||
}), 404)
|
||||
|
||||
r = Record()
|
||||
result = r.apply(domain_name, submitted_record)
|
||||
if result['status'] == 'ok':
|
||||
jdata.pop('_csrf_token',
|
||||
None) # don't store csrf token in the history.
|
||||
history = History(
|
||||
msg='Apply record changes to domain {0}'.format(domain_name),
|
||||
detail=str(json.dumps(jdata)),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return make_response(jsonify(result), 200)
|
||||
else:
|
||||
return make_response(jsonify(result), 400)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot apply record changes. Error: {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Error when applying new changes'
|
||||
}), 500)
|
||||
|
||||
|
||||
@domain_bp.route('/<path:domain_name>/update',
|
||||
methods=['POST'],
|
||||
strict_slashes=False)
|
||||
@login_required
|
||||
@can_access_domain
|
||||
def record_update(domain_name):
|
||||
"""
|
||||
This route is used for domain work as Slave Zone only
|
||||
Pulling the records update from its Master
|
||||
"""
|
||||
try:
|
||||
jdata = request.json
|
||||
|
||||
domain_name = jdata['domain']
|
||||
d = Domain()
|
||||
result = d.update_from_master(domain_name)
|
||||
if result['status'] == 'ok':
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': result['msg']
|
||||
}), 200)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': result['msg']
|
||||
}), 500)
|
||||
except Exception as e:
|
||||
current_app.logger.error('Cannot update record. Error: {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Error when applying new changes'
|
||||
}), 500)
|
||||
|
||||
|
||||
@domain_bp.route('/<path:domain_name>/info', methods=['GET'])
|
||||
@login_required
|
||||
@can_access_domain
|
||||
def info(domain_name):
|
||||
domain = Domain()
|
||||
domain_info = domain.get_domain_info(domain_name)
|
||||
return make_response(jsonify(domain_info), 200)
|
||||
|
||||
|
||||
@domain_bp.route('/<path:domain_name>/dnssec', methods=['GET'])
|
||||
@login_required
|
||||
@can_access_domain
|
||||
def dnssec(domain_name):
|
||||
domain = Domain()
|
||||
dnssec = domain.get_domain_dnssec(domain_name)
|
||||
return make_response(jsonify(dnssec), 200)
|
||||
|
||||
|
||||
@domain_bp.route('/<path:domain_name>/dnssec/enable', methods=['POST'])
|
||||
@login_required
|
||||
@can_access_domain
|
||||
@can_configure_dnssec
|
||||
def dnssec_enable(domain_name):
|
||||
domain = Domain()
|
||||
dnssec = domain.enable_domain_dnssec(domain_name)
|
||||
return make_response(jsonify(dnssec), 200)
|
||||
|
||||
|
||||
@domain_bp.route('/<path:domain_name>/dnssec/disable', methods=['POST'])
|
||||
@login_required
|
||||
@can_access_domain
|
||||
@can_configure_dnssec
|
||||
def dnssec_disable(domain_name):
|
||||
domain = Domain()
|
||||
dnssec = domain.get_domain_dnssec(domain_name)
|
||||
|
||||
for key in dnssec['dnssec']:
|
||||
domain.delete_dnssec_key(domain_name, key['id'])
|
||||
|
||||
return make_response(jsonify({'status': 'ok', 'msg': 'DNSSEC removed.'}))
|
||||
|
||||
|
||||
@domain_bp.route('/<path:domain_name>/manage-setting', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def admin_setdomainsetting(domain_name):
|
||||
if request.method == 'POST':
|
||||
#
|
||||
# post data should in format
|
||||
# {'action': 'set_setting', 'setting': 'default_action, 'value': 'True'}
|
||||
#
|
||||
try:
|
||||
jdata = request.json
|
||||
data = jdata['data']
|
||||
|
||||
if jdata['action'] == 'set_setting':
|
||||
new_setting = data['setting']
|
||||
new_value = str(data['value'])
|
||||
domain = Domain.query.filter(
|
||||
Domain.name == domain_name).first()
|
||||
setting = DomainSetting.query.filter(
|
||||
DomainSetting.domain == domain).filter(
|
||||
DomainSetting.setting == new_setting).first()
|
||||
|
||||
if setting:
|
||||
if setting.set(new_value):
|
||||
history = History(
|
||||
msg='Setting {0} changed value to {1} for {2}'.
|
||||
format(new_setting, new_value, domain.name),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': 'Setting updated.'
|
||||
}))
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Unable to set value of setting.'
|
||||
}))
|
||||
else:
|
||||
if domain.add_setting(new_setting, new_value):
|
||||
history = History(
|
||||
msg=
|
||||
'New setting {0} with value {1} for {2} has been created'
|
||||
.format(new_setting, new_value, domain.name),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': 'New setting created and updated.'
|
||||
}))
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Unable to create new setting.'
|
||||
}))
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Action not supported.'
|
||||
}), 400)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot change domain setting. Error: {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'There is something wrong, please contact Administrator.'
|
||||
}), 400)
|
686
powerdnsadmin/routes/index.py
Normal file
686
powerdnsadmin/routes/index.py
Normal file
@ -0,0 +1,686 @@
|
||||
import os
|
||||
import json
|
||||
import traceback
|
||||
import datetime
|
||||
import ipaddress
|
||||
from distutils.util import strtobool
|
||||
from yaml import Loader, load
|
||||
from onelogin.saml2.utils import OneLogin_Saml2_Utils
|
||||
from flask import Blueprint, render_template, make_response, url_for, current_app, g, session, request, redirect, abort
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
|
||||
from .base import login_manager
|
||||
from ..lib import utils
|
||||
from ..decorators import dyndns_login_required
|
||||
from ..models.base import db
|
||||
from ..models.user import User, Anonymous
|
||||
from ..models.role import Role
|
||||
from ..models.account import Account
|
||||
from ..models.account_user import AccountUser
|
||||
from ..models.domain import Domain
|
||||
from ..models.domain_user import DomainUser
|
||||
from ..models.domain_setting import DomainSetting
|
||||
from ..models.record import Record
|
||||
from ..models.setting import Setting
|
||||
from ..models.history import History
|
||||
from ..services.google import google_oauth
|
||||
from ..services.github import github_oauth
|
||||
from ..services.oidc import oidc_oauth
|
||||
|
||||
google = None
|
||||
github = None
|
||||
oidc = None
|
||||
|
||||
index_bp = Blueprint('index',
|
||||
__name__,
|
||||
template_folder='templates',
|
||||
url_prefix='/')
|
||||
|
||||
|
||||
@index_bp.before_app_first_request
|
||||
def register_modules():
|
||||
global google
|
||||
global github
|
||||
global oidc
|
||||
google = google_oauth()
|
||||
github = github_oauth()
|
||||
oidc = oidc_oauth()
|
||||
|
||||
|
||||
@index_bp.before_request
|
||||
def before_request():
|
||||
# Check if user is anonymous
|
||||
g.user = current_user
|
||||
login_manager.anonymous_user = Anonymous
|
||||
|
||||
# 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'
|
||||
]:
|
||||
return render_template('maintenance.html')
|
||||
|
||||
# Manage session timeout
|
||||
session.permanent = True
|
||||
current_app.permanent_session_lifetime = datetime.timedelta(
|
||||
minutes=int(Setting().get('session_timeout')))
|
||||
session.modified = True
|
||||
|
||||
|
||||
@index_bp.route('/', methods=['GET'])
|
||||
@login_required
|
||||
def index():
|
||||
return redirect(url_for('dashboard.dashboard'))
|
||||
|
||||
|
||||
@index_bp.route('/google/login')
|
||||
def google_login():
|
||||
if not Setting().get('google_oauth_enabled') or google is None:
|
||||
current_app.logger.error(
|
||||
'Google OAuth is disabled or you have not yet reloaded the pda application after enabling.'
|
||||
)
|
||||
abort(400)
|
||||
else:
|
||||
redirect_uri = url_for('google_authorized', _external=True)
|
||||
return google.authorize_redirect(redirect_uri)
|
||||
|
||||
|
||||
@index_bp.route('/github/login')
|
||||
def github_login():
|
||||
if not Setting().get('github_oauth_enabled') or github is None:
|
||||
current_app.logger.error(
|
||||
'Github OAuth is disabled or you have not yet reloaded the pda application after enabling.'
|
||||
)
|
||||
abort(400)
|
||||
else:
|
||||
redirect_uri = url_for('github_authorized', _external=True)
|
||||
return github.authorize_redirect(redirect_uri)
|
||||
|
||||
|
||||
@index_bp.route('/oidc/login')
|
||||
def oidc_login():
|
||||
if not Setting().get('oidc_oauth_enabled') or oidc is None:
|
||||
current_app.logger.error(
|
||||
'OIDC OAuth is disabled or you have not yet reloaded the pda application after enabling.'
|
||||
)
|
||||
abort(400)
|
||||
else:
|
||||
redirect_uri = url_for('oidc_authorized', _external=True)
|
||||
return oidc.authorize_redirect(redirect_uri)
|
||||
|
||||
|
||||
@index_bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
SAML_ENABLED = current_app.config.get('SAML_ENABLED')
|
||||
|
||||
if g.user is not None and current_user.is_authenticated:
|
||||
return redirect(url_for('dashboard.dashboard'))
|
||||
|
||||
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()
|
||||
if user is None:
|
||||
user = User.query.filter_by(email=email).first()
|
||||
if not user:
|
||||
user = User(username=email,
|
||||
firstname=first_name,
|
||||
lastname=surname,
|
||||
plain_text_password=None,
|
||||
email=email)
|
||||
|
||||
result = user.create_local_user()
|
||||
if not result['status']:
|
||||
session.pop('google_token', None)
|
||||
return redirect(url_for('index.login'))
|
||||
|
||||
session['user_id'] = user.id
|
||||
login_user(user, remember=False)
|
||||
session['authentication_type'] = 'OAuth'
|
||||
return redirect(url_for('index.index'))
|
||||
|
||||
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 = User.query.filter_by(username=github_username).first()
|
||||
if user is None:
|
||||
user = User.query.filter_by(email=github_email).first()
|
||||
if not user:
|
||||
user = User(username=github_username,
|
||||
plain_text_password=None,
|
||||
firstname=github_name,
|
||||
lastname='',
|
||||
email=github_email)
|
||||
|
||||
result = user.create_local_user()
|
||||
if not result['status']:
|
||||
session.pop('github_token', None)
|
||||
return redirect(url_for('index.login'))
|
||||
|
||||
session['user_id'] = user.id
|
||||
session['authentication_type'] = 'OAuth'
|
||||
login_user(user, remember=False)
|
||||
return redirect(url_for('index.index'))
|
||||
|
||||
if 'oidc_token' in session:
|
||||
me = json.loads(oidc.get('userinfo').text)
|
||||
oidc_username = me["preferred_username"]
|
||||
oidc_givenname = me["name"]
|
||||
oidc_familyname = ""
|
||||
oidc_email = me["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,
|
||||
email=oidc_email)
|
||||
|
||||
result = user.create_local_user()
|
||||
if not result['status']:
|
||||
session.pop('oidc_token', None)
|
||||
return redirect(url_for('index.login'))
|
||||
|
||||
session['user_id'] = user.id
|
||||
session['authentication_type'] = 'OAuth'
|
||||
login_user(user, remember=False)
|
||||
return redirect(url_for('index.index'))
|
||||
|
||||
if request.method == 'GET':
|
||||
return render_template('login.html', saml_enabled=SAML_ENABLED)
|
||||
elif request.method == 'POST':
|
||||
# process Local-DB authentication
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
otp_token = request.form.get('otptoken')
|
||||
auth_method = request.form.get('auth_method', 'LOCAL')
|
||||
session[
|
||||
'authentication_type'] = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL'
|
||||
remember_me = True if 'remember' in request.form else False
|
||||
|
||||
user = User(username=username,
|
||||
password=password,
|
||||
plain_text_password=password)
|
||||
|
||||
try:
|
||||
auth = user.is_validate(method=auth_method,
|
||||
src_ip=request.remote_addr)
|
||||
if auth == False:
|
||||
return render_template('login.html',
|
||||
saml_enabled=SAML_ENABLED,
|
||||
error='Invalid credentials')
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
"Cannot authenticate user. Error: {}".format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return render_template('login.html',
|
||||
saml_enabled=SAML_ENABLED,
|
||||
error=e)
|
||||
|
||||
# check if user enabled OPT authentication
|
||||
if user.otp_secret:
|
||||
if otp_token and otp_token.isdigit():
|
||||
good_token = user.verify_totp(otp_token)
|
||||
if not good_token:
|
||||
return render_template('login.html',
|
||||
saml_enabled=SAML_ENABLED,
|
||||
error='Invalid credentials')
|
||||
else:
|
||||
return render_template('login.html',
|
||||
saml_enabled=SAML_ENABLED,
|
||||
error='Token required')
|
||||
|
||||
login_user(user, remember=remember_me)
|
||||
return redirect(session.get('next', url_for('index.index')))
|
||||
|
||||
|
||||
def clear_session():
|
||||
session.pop('user_id', None)
|
||||
session.pop('github_token', None)
|
||||
session.pop('google_token', None)
|
||||
session.pop('authentication_type', None)
|
||||
session.clear()
|
||||
logout_user()
|
||||
|
||||
|
||||
@index_bp.route('/logout')
|
||||
def logout():
|
||||
if current_app.config.get(
|
||||
'SAML_ENABLED'
|
||||
) and 'samlSessionIndex' in session and current_app.config.get(
|
||||
'SAML_LOGOUT'):
|
||||
req = utils.prepare_flask_request(request)
|
||||
auth = utils.init_saml_auth(req)
|
||||
if current_app.config.get('SAML_LOGOUT_URL'):
|
||||
return redirect(
|
||||
auth.logout(
|
||||
name_id_format=
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
return_to=current_app.config.get('SAML_LOGOUT_URL'),
|
||||
session_index=session['samlSessionIndex'],
|
||||
name_id=session['samlNameId']))
|
||||
return redirect(
|
||||
auth.logout(
|
||||
name_id_format=
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
session_index=session['samlSessionIndex'],
|
||||
name_id=session['samlNameId']))
|
||||
clear_session()
|
||||
return redirect(url_for('index.login'))
|
||||
|
||||
|
||||
@index_bp.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if Setting().get('signup_enabled'):
|
||||
if request.method == 'GET':
|
||||
return render_template('register.html')
|
||||
elif request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
firstname = request.form.get('firstname')
|
||||
lastname = request.form.get('lastname')
|
||||
email = request.form.get('email')
|
||||
rpassword = request.form.get('rpassword')
|
||||
|
||||
if not username or not password or not email:
|
||||
return render_template(
|
||||
'register.html', error='Please input required information')
|
||||
|
||||
if password != rpassword:
|
||||
return render_template(
|
||||
'register.html',
|
||||
error="Password confirmation does not match")
|
||||
|
||||
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']:
|
||||
return redirect(url_for('index.login'))
|
||||
else:
|
||||
return render_template('register.html',
|
||||
error=result['msg'])
|
||||
except Exception as e:
|
||||
return render_template('register.html', error=e)
|
||||
else:
|
||||
return render_template('errors/404.html'), 404
|
||||
|
||||
|
||||
@index_bp.route('/nic/checkip.html', methods=['GET', 'POST'])
|
||||
def dyndns_checkip():
|
||||
# This route covers the default ddclient 'web' setting for the checkip service
|
||||
return render_template('dyndns.html',
|
||||
response=request.environ.get(
|
||||
'HTTP_X_REAL_IP', request.remote_addr))
|
||||
|
||||
|
||||
@index_bp.route('/nic/update', methods=['GET', 'POST'])
|
||||
@dyndns_login_required
|
||||
def dyndns_update():
|
||||
# dyndns protocol response codes in use are:
|
||||
# good: update successful
|
||||
# nochg: IP address already set to update address
|
||||
# nohost: hostname does not exist for this user account
|
||||
# 911: server error
|
||||
# have to use 200 HTTP return codes because ddclient does not read the return string if the code is other than 200
|
||||
# reference: https://help.dyn.com/remote-access-api/perform-update/
|
||||
# reference: https://help.dyn.com/remote-access-api/return-codes/
|
||||
hostname = request.args.get('hostname')
|
||||
myip = request.args.get('myip')
|
||||
|
||||
if not hostname:
|
||||
history = History(msg="DynDNS update: missing hostname parameter",
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return render_template('dyndns.html', response='nohost'), 200
|
||||
|
||||
try:
|
||||
if current_user.role.name in ['Administrator', 'Operator']:
|
||||
domains = Domain.query.all()
|
||||
else:
|
||||
# Get query for domain to which the user has access permission.
|
||||
# This includes direct domain permission AND permission through
|
||||
# account membership
|
||||
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
|
||||
)).all()
|
||||
except Exception as e:
|
||||
current_app.logger.error('DynDNS Error: {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
return render_template('dyndns.html', response='911'), 200
|
||||
|
||||
domain = None
|
||||
domain_segments = hostname.split('.')
|
||||
for index in range(len(domain_segments)):
|
||||
full_domain = '.'.join(domain_segments)
|
||||
potential_domain = Domain.query.filter(
|
||||
Domain.name == full_domain).first()
|
||||
if potential_domain in domains:
|
||||
domain = potential_domain
|
||||
break
|
||||
domain_segments.pop(0)
|
||||
|
||||
if not domain:
|
||||
history = History(
|
||||
msg=
|
||||
"DynDNS update: attempted update of {0} but it does not exist for this user"
|
||||
.format(hostname),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return render_template('dyndns.html', response='nohost'), 200
|
||||
|
||||
myip_addr = []
|
||||
if myip:
|
||||
for address in myip.split(','):
|
||||
myip_addr += utils.validate_ipaddress(address)
|
||||
|
||||
remote_addr = utils.validate_ipaddress(
|
||||
request.headers.get('X-Forwarded-For',
|
||||
request.remote_addr).split(', ')[:1])
|
||||
|
||||
response = 'nochg'
|
||||
for ip in myip_addr or remote_addr:
|
||||
if isinstance(ip, ipaddress.IPv4Address):
|
||||
rtype = 'A'
|
||||
else:
|
||||
rtype = 'AAAA'
|
||||
|
||||
r = Record(name=hostname, type=rtype)
|
||||
# Check if the user requested record exists within this domain
|
||||
if r.exists(domain.name) and r.is_allowed_edit():
|
||||
if r.data == str(ip):
|
||||
# Record content did not change, return 'nochg'
|
||||
history = History(
|
||||
msg=
|
||||
"DynDNS update: attempted update of {0} but record did not change"
|
||||
.format(hostname),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
else:
|
||||
oldip = r.data
|
||||
result = r.update(domain.name, str(ip))
|
||||
if result['status'] == 'ok':
|
||||
history = History(
|
||||
msg=
|
||||
'DynDNS update: updated {0} record {1} in zone {2}, it changed from {3} to {4}'
|
||||
.format(rtype, hostname, domain.name, oldip, str(ip)),
|
||||
detail=str(result),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
response = 'good'
|
||||
else:
|
||||
response = '911'
|
||||
break
|
||||
elif r.is_allowed_edit():
|
||||
ondemand_creation = DomainSetting.query.filter(
|
||||
DomainSetting.domain == domain).filter(
|
||||
DomainSetting.setting == 'create_via_dyndns').first()
|
||||
if (ondemand_creation is not None) and (strtobool(
|
||||
ondemand_creation.value) == True):
|
||||
record = Record(name=hostname,
|
||||
type=rtype,
|
||||
data=str(ip),
|
||||
status=False,
|
||||
ttl=3600)
|
||||
result = record.add(domain.name)
|
||||
if result['status'] == 'ok':
|
||||
history = History(
|
||||
msg=
|
||||
'DynDNS update: created record {0} in zone {1}, it now represents {2}'
|
||||
.format(hostname, domain.name, str(ip)),
|
||||
detail=str(result),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
response = 'good'
|
||||
else:
|
||||
history = History(
|
||||
msg=
|
||||
'DynDNS update: attempted update of {0} but it does not exist for this user'
|
||||
.format(hostname),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
|
||||
return render_template('dyndns.html', response=response), 200
|
||||
|
||||
|
||||
### START SAML AUTHENTICATION ###
|
||||
@index_bp.route('/saml/login')
|
||||
def saml_login():
|
||||
if not current_app.config.get('SAML_ENABLED'):
|
||||
abort(400)
|
||||
req = utils.prepare_flask_request(request)
|
||||
auth = utils.init_saml_auth(req)
|
||||
redirect_url = OneLogin_Saml2_Utils.get_self_url(req) + url_for(
|
||||
'saml_authorized')
|
||||
return redirect(auth.login(return_to=redirect_url))
|
||||
|
||||
|
||||
@index_bp.route('/saml/metadata')
|
||||
def saml_metadata():
|
||||
if not current_app.config.get('SAML_ENABLED'):
|
||||
current_app.logger.error("SAML authentication is disabled.")
|
||||
abort(400)
|
||||
|
||||
req = utils.prepare_flask_request(request)
|
||||
auth = utils.init_saml_auth(req)
|
||||
settings = auth.get_settings()
|
||||
metadata = settings.get_sp_metadata()
|
||||
errors = settings.validate_metadata(metadata)
|
||||
|
||||
if len(errors) == 0:
|
||||
resp = make_response(metadata, 200)
|
||||
resp.headers['Content-Type'] = 'text/xml'
|
||||
else:
|
||||
resp = make_response(errors.join(', '), 500)
|
||||
return resp
|
||||
|
||||
|
||||
@index_bp.route('/saml/authorized', methods=['GET', 'POST'])
|
||||
def saml_authorized():
|
||||
errors = []
|
||||
if not current_app.config.get('SAML_ENABLED'):
|
||||
current_app.logger.error("SAML authentication is disabled.")
|
||||
abort(400)
|
||||
req = utils.prepare_flask_request(request)
|
||||
auth = utils.init_saml_auth(req)
|
||||
auth.process_response()
|
||||
errors = auth.get_errors()
|
||||
if len(errors) == 0:
|
||||
session['samlUserdata'] = auth.get_attributes()
|
||||
session['samlNameId'] = auth.get_nameid()
|
||||
session['samlSessionIndex'] = auth.get_session_index()
|
||||
self_url = OneLogin_Saml2_Utils.get_self_url(req)
|
||||
self_url = self_url + req['script_name']
|
||||
if 'RelayState' in request.form and self_url != request.form[
|
||||
'RelayState']:
|
||||
return redirect(auth.redirect_to(request.form['RelayState']))
|
||||
if current_app.config.get('SAML_ATTRIBUTE_USERNAME', False):
|
||||
username = session['samlUserdata'][
|
||||
current_app.config['SAML_ATTRIBUTE_USERNAME']][0].lower()
|
||||
else:
|
||||
username = session['samlNameId'].lower()
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if not user:
|
||||
# create user
|
||||
user = User(username=username,
|
||||
plain_text_password=None,
|
||||
email=session['samlNameId'])
|
||||
user.create_local_user()
|
||||
session['user_id'] = user.id
|
||||
email_attribute_name = current_app.config.get('SAML_ATTRIBUTE_EMAIL',
|
||||
'email')
|
||||
givenname_attribute_name = current_app.config.get(
|
||||
'SAML_ATTRIBUTE_GIVENNAME', 'givenname')
|
||||
surname_attribute_name = current_app.config.get(
|
||||
'SAML_ATTRIBUTE_SURNAME', 'surname')
|
||||
name_attribute_name = current_app.config.get('SAML_ATTRIBUTE_NAME',
|
||||
None)
|
||||
account_attribute_name = current_app.config.get(
|
||||
'SAML_ATTRIBUTE_ACCOUNT', None)
|
||||
admin_attribute_name = current_app.config.get('SAML_ATTRIBUTE_ADMIN',
|
||||
None)
|
||||
group_attribute_name = current_app.config.get('SAML_ATTRIBUTE_GROUP',
|
||||
None)
|
||||
admin_group_name = current_app.config.get('SAML_GROUP_ADMIN_NAME',
|
||||
None)
|
||||
group_to_account_mapping = create_group_to_account_mapping()
|
||||
|
||||
if email_attribute_name in session['samlUserdata']:
|
||||
user.email = session['samlUserdata'][email_attribute_name][
|
||||
0].lower()
|
||||
if givenname_attribute_name in session['samlUserdata']:
|
||||
user.firstname = session['samlUserdata'][givenname_attribute_name][
|
||||
0]
|
||||
if surname_attribute_name in session['samlUserdata']:
|
||||
user.lastname = session['samlUserdata'][surname_attribute_name][0]
|
||||
if name_attribute_name in session['samlUserdata']:
|
||||
name = session['samlUserdata'][name_attribute_name][0].split(' ')
|
||||
user.firstname = name[0]
|
||||
user.lastname = ' '.join(name[1:])
|
||||
|
||||
if group_attribute_name:
|
||||
user_groups = session['samlUserdata'].get(group_attribute_name, [])
|
||||
else:
|
||||
user_groups = []
|
||||
if admin_attribute_name or group_attribute_name:
|
||||
user_accounts = set(user.get_account())
|
||||
saml_accounts = []
|
||||
for group_mapping in group_to_account_mapping:
|
||||
mapping = group_mapping.split('=')
|
||||
group = mapping[0]
|
||||
account_name = mapping[1]
|
||||
|
||||
if group in user_groups:
|
||||
account = handle_account(account_name)
|
||||
saml_accounts.append(account)
|
||||
|
||||
for account_name in session['samlUserdata'].get(
|
||||
account_attribute_name, []):
|
||||
account = handle_account(account_name)
|
||||
saml_accounts.append(account)
|
||||
saml_accounts = set(saml_accounts)
|
||||
for account in saml_accounts - user_accounts:
|
||||
account.add_user(user)
|
||||
history = History(msg='Adding {0} to account {1}'.format(
|
||||
user.username, account.name),
|
||||
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')
|
||||
history.add()
|
||||
if admin_attribute_name and 'true' in session['samlUserdata'].get(
|
||||
admin_attribute_name, []):
|
||||
uplift_to_admin(user)
|
||||
elif admin_group_name in user_groups:
|
||||
uplift_to_admin(user)
|
||||
elif admin_attribute_name or group_attribute_name:
|
||||
if user.role.name != 'User':
|
||||
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')
|
||||
history.add()
|
||||
user.plain_text_password = None
|
||||
user.update_profile()
|
||||
session['authentication_type'] = 'SAML'
|
||||
login_user(user, remember=False)
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
return render_template('errors/SAML.html', errors=errors)
|
||||
|
||||
|
||||
def create_group_to_account_mapping():
|
||||
group_to_account_mapping_string = current_app.config.get(
|
||||
'SAML_GROUP_TO_ACCOUNT_MAPPING', None)
|
||||
if group_to_account_mapping_string and len(
|
||||
group_to_account_mapping_string.strip()) > 0:
|
||||
group_to_account_mapping = group_to_account_mapping_string.split(',')
|
||||
else:
|
||||
group_to_account_mapping = []
|
||||
return group_to_account_mapping
|
||||
|
||||
|
||||
def handle_account(account_name):
|
||||
clean_name = ''.join(c for c in account_name.lower()
|
||||
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
|
||||
if len(clean_name) > Account.name.type.length:
|
||||
logging.error(
|
||||
"Account name {0} too long. Truncated.".format(clean_name))
|
||||
account = Account.query.filter_by(name=clean_name).first()
|
||||
if not account:
|
||||
account = Account(name=clean_name.lower(),
|
||||
description='',
|
||||
contact='',
|
||||
mail='')
|
||||
account.create_account()
|
||||
history = History(msg='Account {0} created'.format(account.name),
|
||||
created_by='SAML Assertion')
|
||||
history.add()
|
||||
return account
|
||||
|
||||
|
||||
def uplift_to_admin(user):
|
||||
if user.role.name != 'Administrator':
|
||||
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')
|
||||
history.add()
|
||||
|
||||
|
||||
@index_bp.route('/saml/sls')
|
||||
def saml_logout():
|
||||
req = utils.prepare_flask_request(request)
|
||||
auth = utils.init_saml_auth(req)
|
||||
url = auth.process_slo()
|
||||
errors = auth.get_errors()
|
||||
if len(errors) == 0:
|
||||
clear_session()
|
||||
if url is not None:
|
||||
return redirect(url)
|
||||
elif current_app.config.get('SAML_LOGOUT_URL') is not None:
|
||||
return redirect(current_app.config.get('SAML_LOGOUT_URL'))
|
||||
else:
|
||||
return redirect(url_for('login'))
|
||||
else:
|
||||
return render_template('errors/SAML.html', errors=errors)
|
||||
|
||||
|
||||
### END SAML AUTHENTICATION ###
|
||||
|
||||
|
||||
@index_bp.route('/swagger', methods=['GET'])
|
||||
def swagger_spec():
|
||||
try:
|
||||
spec_path = os.path.join(current_app.root_path, "swagger-spec.yaml")
|
||||
spec = open(spec_path, 'r')
|
||||
loaded_spec = load(spec.read(), Loader)
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
'Cannot view swagger spec. Error: {0}'.format(e))
|
||||
current_app.logger.debug(traceback.format_exc())
|
||||
abort(500)
|
||||
|
||||
resp = make_response(json.dumps(loaded_spec), 200)
|
||||
resp.headers['Content-Type'] = 'application/json'
|
||||
|
||||
return resp
|
89
powerdnsadmin/routes/user.py
Normal file
89
powerdnsadmin/routes/user.py
Normal file
@ -0,0 +1,89 @@
|
||||
import qrcode as qrc
|
||||
import qrcode.image.svg as qrc_svg
|
||||
from io import BytesIO
|
||||
from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, current_app, session, g
|
||||
from flask_login import current_user, login_user, logout_user, login_required
|
||||
|
||||
from .base import login_manager
|
||||
from ..models.user import User
|
||||
from ..models.role import Role
|
||||
|
||||
user_bp = Blueprint('user',
|
||||
__name__,
|
||||
template_folder='templates',
|
||||
url_prefix='/user')
|
||||
|
||||
|
||||
@user_bp.route('/profile', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def profile():
|
||||
if request.method == 'GET':
|
||||
return render_template('user_profile.html')
|
||||
if request.method == 'POST':
|
||||
if session['authentication_type'] == 'LOCAL':
|
||||
firstname = request.form[
|
||||
'firstname'] if 'firstname' in request.form else ''
|
||||
lastname = request.form[
|
||||
'lastname'] if 'lastname' in request.form else ''
|
||||
email = request.form['email'] if 'email' in request.form else ''
|
||||
new_password = request.form[
|
||||
'password'] if 'password' in request.form else ''
|
||||
else:
|
||||
firstname = lastname = email = new_password = ''
|
||||
logging.warning(
|
||||
'Authenticated externally. User {0} information will not allowed to update the profile'
|
||||
.format(current_user.username))
|
||||
|
||||
if request.data:
|
||||
jdata = request.json
|
||||
data = jdata['data']
|
||||
if jdata['action'] == 'enable_otp':
|
||||
if session['authentication_type'] in ['LOCAL', 'LDAP']:
|
||||
enable_otp = data['enable_otp']
|
||||
user = User(username=current_user.username)
|
||||
user.update_profile(enable_otp=enable_otp)
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status':
|
||||
'ok',
|
||||
'msg':
|
||||
'Change OTP Authentication successfully. Status: {0}'
|
||||
.format(enable_otp)
|
||||
}), 200)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status':
|
||||
'error',
|
||||
'msg':
|
||||
'User {0} is externally. You are not allowed to update the OTP'
|
||||
.format(current_user.username)
|
||||
}), 400)
|
||||
|
||||
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')
|
||||
|
||||
|
||||
@user_bp.route('/qrcode')
|
||||
@login_required
|
||||
def qrcode():
|
||||
if not current_user:
|
||||
return redirect(url_for('index'))
|
||||
|
||||
img = qrc.make(current_user.get_totp_uri(),
|
||||
image_factory=qrc_svg.SvgPathImage)
|
||||
stream = BytesIO()
|
||||
img.save(stream)
|
||||
return stream.getvalue(), 200, {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
4
powerdnsadmin/services/__init__.py
Normal file
4
powerdnsadmin/services/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .base import authlib_oauth_client
|
||||
|
||||
def init_app(app):
|
||||
authlib_oauth_client.init_app(app)
|
3
powerdnsadmin/services/base.py
Normal file
3
powerdnsadmin/services/base.py
Normal file
@ -0,0 +1,3 @@
|
||||
from authlib.flask.client import OAuth
|
||||
|
||||
authlib_oauth_client = OAuth()
|
42
powerdnsadmin/services/github.py
Normal file
42
powerdnsadmin/services/github.py
Normal file
@ -0,0 +1,42 @@
|
||||
from flask import request, session, redirect, url_for, current_app
|
||||
|
||||
from .base import authlib_oauth_client
|
||||
from ..models.setting import Setting
|
||||
|
||||
|
||||
def github_oauth():
|
||||
if not Setting().get('github_oauth_enabled'):
|
||||
return None
|
||||
|
||||
def fetch_github_token():
|
||||
return session.get('github_token')
|
||||
|
||||
def update_token(token):
|
||||
session['google_token'] = token
|
||||
return token
|
||||
|
||||
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)
|
||||
|
||||
@current_app.route('/github/authorized')
|
||||
def github_authorized():
|
||||
session['github_oauthredir'] = url_for('.github_authorized',
|
||||
_external=True)
|
||||
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'))
|
||||
|
||||
return github
|
44
powerdnsadmin/services/google.py
Normal file
44
powerdnsadmin/services/google.py
Normal file
@ -0,0 +1,44 @@
|
||||
from flask import request, session, redirect, url_for, current_app
|
||||
|
||||
from .base import authlib_oauth_client
|
||||
from ..models.setting import Setting
|
||||
|
||||
|
||||
def google_oauth():
|
||||
if not Setting().get('google_oauth_enabled'):
|
||||
return None
|
||||
|
||||
def fetch_google_token():
|
||||
return session.get('google_token')
|
||||
|
||||
def update_token(token):
|
||||
session['google_token'] = token
|
||||
return token
|
||||
|
||||
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)
|
||||
|
||||
@current_app.route('/google/authorized')
|
||||
def google_authorized():
|
||||
session['google_oauthredir'] = url_for(
|
||||
'.google_authorized', _external=True)
|
||||
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'))
|
||||
|
||||
return google
|
||||
|
41
powerdnsadmin/services/oidc.py
Normal file
41
powerdnsadmin/services/oidc.py
Normal file
@ -0,0 +1,41 @@
|
||||
from flask import request, session, redirect, url_for, current_app
|
||||
|
||||
from .base import authlib_oauth_client
|
||||
from ..models.setting import Setting
|
||||
|
||||
|
||||
def oidc_oauth():
|
||||
if not Setting().get('oidc_oauth_enabled'):
|
||||
return None
|
||||
|
||||
def fetch_oidc_token():
|
||||
return session.get('oidc_token')
|
||||
|
||||
def update_token(token):
|
||||
session['google_token'] = token
|
||||
return token
|
||||
|
||||
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'),
|
||||
client_kwargs={'scope': Setting().get('oidc_oauth_scope')},
|
||||
fetch_token=fetch_oidc_token,
|
||||
update_token=update_token)
|
||||
|
||||
@current_app.route('/oidc/authorized')
|
||||
def oidc_authorized():
|
||||
session['oidc_oauthredir'] = url_for('.oidc_authorized',
|
||||
_external=True)
|
||||
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'))
|
||||
|
||||
return oidc
|
BIN
powerdnsadmin/static/favicon.ico
Normal file
BIN
powerdnsadmin/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 894 B |
1
powerdnsadmin/static/generated/login.css
Normal file
1
powerdnsadmin/static/generated/login.css
Normal file
File diff suppressed because one or more lines are too long
1603
powerdnsadmin/static/generated/login.js
Normal file
1603
powerdnsadmin/static/generated/login.js
Normal file
File diff suppressed because it is too large
Load Diff
1
powerdnsadmin/static/generated/main.css
Normal file
1
powerdnsadmin/static/generated/main.css
Normal file
File diff suppressed because one or more lines are too long
5227
powerdnsadmin/static/generated/main.js
Normal file
5227
powerdnsadmin/static/generated/main.js
Normal file
File diff suppressed because one or more lines are too long
394
powerdnsadmin/static/generated/validation.js
Normal file
394
powerdnsadmin/static/generated/validation.js
Normal file
@ -0,0 +1,394 @@
|
||||
/*!
|
||||
* Validator v0.11.9 for Bootstrap 3, by @1000hz
|
||||
* Copyright 2017 Cina Saffary
|
||||
* Licensed under http://opensource.org/licenses/MIT
|
||||
*
|
||||
* https://github.com/1000hz/bootstrap-validator
|
||||
*/
|
||||
|
||||
+function ($) {
|
||||
'use strict';
|
||||
|
||||
// VALIDATOR CLASS DEFINITION
|
||||
// ==========================
|
||||
|
||||
function getValue($el) {
|
||||
return $el.is('[type="checkbox"]') ? $el.prop('checked') :
|
||||
$el.is('[type="radio"]') ? !!$('[name="' + $el.attr('name') + '"]:checked').length :
|
||||
$el.is('select[multiple]') ? ($el.val() || []).length :
|
||||
$el.val()
|
||||
}
|
||||
|
||||
var Validator = function (element, options) {
|
||||
this.options = options
|
||||
this.validators = $.extend({}, Validator.VALIDATORS, options.custom)
|
||||
this.$element = $(element)
|
||||
this.$btn = $('button[type="submit"], input[type="submit"]')
|
||||
.filter('[form="' + this.$element.attr('id') + '"]')
|
||||
.add(this.$element.find('input[type="submit"], button[type="submit"]'))
|
||||
|
||||
this.update()
|
||||
|
||||
this.$element.on('input.bs.validator change.bs.validator focusout.bs.validator', $.proxy(this.onInput, this))
|
||||
this.$element.on('submit.bs.validator', $.proxy(this.onSubmit, this))
|
||||
this.$element.on('reset.bs.validator', $.proxy(this.reset, this))
|
||||
|
||||
this.$element.find('[data-match]').each(function () {
|
||||
var $this = $(this)
|
||||
var target = $this.attr('data-match')
|
||||
|
||||
$(target).on('input.bs.validator', function (e) {
|
||||
getValue($this) && $this.trigger('input.bs.validator')
|
||||
})
|
||||
})
|
||||
|
||||
// run validators for fields with values, but don't clobber server-side errors
|
||||
this.$inputs.filter(function () {
|
||||
return getValue($(this)) && !$(this).closest('.has-error').length
|
||||
}).trigger('focusout')
|
||||
|
||||
this.$element.attr('novalidate', true) // disable automatic native validation
|
||||
}
|
||||
|
||||
Validator.VERSION = '0.11.9'
|
||||
|
||||
Validator.INPUT_SELECTOR = ':input:not([type="hidden"], [type="submit"], [type="reset"], button)'
|
||||
|
||||
Validator.FOCUS_OFFSET = 20
|
||||
|
||||
Validator.DEFAULTS = {
|
||||
delay: 500,
|
||||
html: false,
|
||||
disable: true,
|
||||
focus: true,
|
||||
custom: {},
|
||||
errors: {
|
||||
match: 'Does not match',
|
||||
minlength: 'Not long enough'
|
||||
},
|
||||
feedback: {
|
||||
success: 'glyphicon-ok',
|
||||
error: 'glyphicon-remove'
|
||||
}
|
||||
}
|
||||
|
||||
Validator.VALIDATORS = {
|
||||
'native': function ($el) {
|
||||
var el = $el[0]
|
||||
if (el.checkValidity) {
|
||||
return !el.checkValidity() && !el.validity.valid && (el.validationMessage || "error!")
|
||||
}
|
||||
},
|
||||
'match': function ($el) {
|
||||
var target = $el.attr('data-match')
|
||||
return $el.val() !== $(target).val() && Validator.DEFAULTS.errors.match
|
||||
},
|
||||
'minlength': function ($el) {
|
||||
var minlength = $el.attr('data-minlength')
|
||||
return $el.val().length < minlength && Validator.DEFAULTS.errors.minlength
|
||||
}
|
||||
}
|
||||
|
||||
Validator.prototype.update = function () {
|
||||
var self = this
|
||||
|
||||
this.$inputs = this.$element.find(Validator.INPUT_SELECTOR)
|
||||
.add(this.$element.find('[data-validate="true"]'))
|
||||
.not(this.$element.find('[data-validate="false"]')
|
||||
.each(function () { self.clearErrors($(this)) })
|
||||
)
|
||||
|
||||
this.toggleSubmit()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
Validator.prototype.onInput = function (e) {
|
||||
var self = this
|
||||
var $el = $(e.target)
|
||||
var deferErrors = e.type !== 'focusout'
|
||||
|
||||
if (!this.$inputs.is($el)) return
|
||||
|
||||
this.validateInput($el, deferErrors).done(function () {
|
||||
self.toggleSubmit()
|
||||
})
|
||||
}
|
||||
|
||||
Validator.prototype.validateInput = function ($el, deferErrors) {
|
||||
var value = getValue($el)
|
||||
var prevErrors = $el.data('bs.validator.errors')
|
||||
|
||||
if ($el.is('[type="radio"]')) $el = this.$element.find('input[name="' + $el.attr('name') + '"]')
|
||||
|
||||
var e = $.Event('validate.bs.validator', {relatedTarget: $el[0]})
|
||||
this.$element.trigger(e)
|
||||
if (e.isDefaultPrevented()) return
|
||||
|
||||
var self = this
|
||||
|
||||
return this.runValidators($el).done(function (errors) {
|
||||
$el.data('bs.validator.errors', errors)
|
||||
|
||||
errors.length
|
||||
? deferErrors ? self.defer($el, self.showErrors) : self.showErrors($el)
|
||||
: self.clearErrors($el)
|
||||
|
||||
if (!prevErrors || errors.toString() !== prevErrors.toString()) {
|
||||
e = errors.length
|
||||
? $.Event('invalid.bs.validator', {relatedTarget: $el[0], detail: errors})
|
||||
: $.Event('valid.bs.validator', {relatedTarget: $el[0], detail: prevErrors})
|
||||
|
||||
self.$element.trigger(e)
|
||||
}
|
||||
|
||||
self.toggleSubmit()
|
||||
|
||||
self.$element.trigger($.Event('validated.bs.validator', {relatedTarget: $el[0]}))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Validator.prototype.runValidators = function ($el) {
|
||||
var errors = []
|
||||
var deferred = $.Deferred()
|
||||
|
||||
$el.data('bs.validator.deferred') && $el.data('bs.validator.deferred').reject()
|
||||
$el.data('bs.validator.deferred', deferred)
|
||||
|
||||
function getValidatorSpecificError(key) {
|
||||
return $el.attr('data-' + key + '-error')
|
||||
}
|
||||
|
||||
function getValidityStateError() {
|
||||
var validity = $el[0].validity
|
||||
return validity.typeMismatch ? $el.attr('data-type-error')
|
||||
: validity.patternMismatch ? $el.attr('data-pattern-error')
|
||||
: validity.stepMismatch ? $el.attr('data-step-error')
|
||||
: validity.rangeOverflow ? $el.attr('data-max-error')
|
||||
: validity.rangeUnderflow ? $el.attr('data-min-error')
|
||||
: validity.valueMissing ? $el.attr('data-required-error')
|
||||
: null
|
||||
}
|
||||
|
||||
function getGenericError() {
|
||||
return $el.attr('data-error')
|
||||
}
|
||||
|
||||
function getErrorMessage(key) {
|
||||
return getValidatorSpecificError(key)
|
||||
|| getValidityStateError()
|
||||
|| getGenericError()
|
||||
}
|
||||
|
||||
$.each(this.validators, $.proxy(function (key, validator) {
|
||||
var error = null
|
||||
if ((getValue($el) || $el.attr('required')) &&
|
||||
($el.attr('data-' + key) !== undefined || key == 'native') &&
|
||||
(error = validator.call(this, $el))) {
|
||||
error = getErrorMessage(key) || error
|
||||
!~errors.indexOf(error) && errors.push(error)
|
||||
}
|
||||
}, this))
|
||||
|
||||
if (!errors.length && getValue($el) && $el.attr('data-remote')) {
|
||||
this.defer($el, function () {
|
||||
var data = {}
|
||||
data[$el.attr('name')] = getValue($el)
|
||||
$.get($el.attr('data-remote'), data)
|
||||
.fail(function (jqXHR, textStatus, error) { errors.push(getErrorMessage('remote') || error) })
|
||||
.always(function () { deferred.resolve(errors)})
|
||||
})
|
||||
} else deferred.resolve(errors)
|
||||
|
||||
return deferred.promise()
|
||||
}
|
||||
|
||||
Validator.prototype.validate = function () {
|
||||
var self = this
|
||||
|
||||
$.when(this.$inputs.map(function (el) {
|
||||
return self.validateInput($(this), false)
|
||||
})).then(function () {
|
||||
self.toggleSubmit()
|
||||
self.focusError()
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
Validator.prototype.focusError = function () {
|
||||
if (!this.options.focus) return
|
||||
|
||||
var $input = this.$element.find(".has-error:first :input")
|
||||
if ($input.length === 0) return
|
||||
|
||||
$('html, body').animate({scrollTop: $input.offset().top - Validator.FOCUS_OFFSET}, 250)
|
||||
$input.focus()
|
||||
}
|
||||
|
||||
Validator.prototype.showErrors = function ($el) {
|
||||
var method = this.options.html ? 'html' : 'text'
|
||||
var errors = $el.data('bs.validator.errors')
|
||||
var $group = $el.closest('.form-group')
|
||||
var $block = $group.find('.help-block.with-errors')
|
||||
var $feedback = $group.find('.form-control-feedback')
|
||||
|
||||
if (!errors.length) return
|
||||
|
||||
errors = $('<ul/>')
|
||||
.addClass('list-unstyled')
|
||||
.append($.map(errors, function (error) { return $('<li/>')[method](error) }))
|
||||
|
||||
$block.data('bs.validator.originalContent') === undefined && $block.data('bs.validator.originalContent', $block.html())
|
||||
$block.empty().append(errors)
|
||||
$group.addClass('has-error has-danger')
|
||||
|
||||
$group.hasClass('has-feedback')
|
||||
&& $feedback.removeClass(this.options.feedback.success)
|
||||
&& $feedback.addClass(this.options.feedback.error)
|
||||
&& $group.removeClass('has-success')
|
||||
}
|
||||
|
||||
Validator.prototype.clearErrors = function ($el) {
|
||||
var $group = $el.closest('.form-group')
|
||||
var $block = $group.find('.help-block.with-errors')
|
||||
var $feedback = $group.find('.form-control-feedback')
|
||||
|
||||
$block.html($block.data('bs.validator.originalContent'))
|
||||
$group.removeClass('has-error has-danger has-success')
|
||||
|
||||
$group.hasClass('has-feedback')
|
||||
&& $feedback.removeClass(this.options.feedback.error)
|
||||
&& $feedback.removeClass(this.options.feedback.success)
|
||||
&& getValue($el)
|
||||
&& $feedback.addClass(this.options.feedback.success)
|
||||
&& $group.addClass('has-success')
|
||||
}
|
||||
|
||||
Validator.prototype.hasErrors = function () {
|
||||
function fieldErrors() {
|
||||
return !!($(this).data('bs.validator.errors') || []).length
|
||||
}
|
||||
|
||||
return !!this.$inputs.filter(fieldErrors).length
|
||||
}
|
||||
|
||||
Validator.prototype.isIncomplete = function () {
|
||||
function fieldIncomplete() {
|
||||
var value = getValue($(this))
|
||||
return !(typeof value == "string" ? $.trim(value) : value)
|
||||
}
|
||||
|
||||
return !!this.$inputs.filter('[required]').filter(fieldIncomplete).length
|
||||
}
|
||||
|
||||
Validator.prototype.onSubmit = function (e) {
|
||||
this.validate()
|
||||
if (this.isIncomplete() || this.hasErrors()) e.preventDefault()
|
||||
}
|
||||
|
||||
Validator.prototype.toggleSubmit = function () {
|
||||
if (!this.options.disable) return
|
||||
this.$btn.toggleClass('disabled', this.isIncomplete() || this.hasErrors())
|
||||
}
|
||||
|
||||
Validator.prototype.defer = function ($el, callback) {
|
||||
callback = $.proxy(callback, this, $el)
|
||||
if (!this.options.delay) return callback()
|
||||
window.clearTimeout($el.data('bs.validator.timeout'))
|
||||
$el.data('bs.validator.timeout', window.setTimeout(callback, this.options.delay))
|
||||
}
|
||||
|
||||
Validator.prototype.reset = function () {
|
||||
this.$element.find('.form-control-feedback')
|
||||
.removeClass(this.options.feedback.error)
|
||||
.removeClass(this.options.feedback.success)
|
||||
|
||||
this.$inputs
|
||||
.removeData(['bs.validator.errors', 'bs.validator.deferred'])
|
||||
.each(function () {
|
||||
var $this = $(this)
|
||||
var timeout = $this.data('bs.validator.timeout')
|
||||
window.clearTimeout(timeout) && $this.removeData('bs.validator.timeout')
|
||||
})
|
||||
|
||||
this.$element.find('.help-block.with-errors')
|
||||
.each(function () {
|
||||
var $this = $(this)
|
||||
var originalContent = $this.data('bs.validator.originalContent')
|
||||
|
||||
$this
|
||||
.removeData('bs.validator.originalContent')
|
||||
.html(originalContent)
|
||||
})
|
||||
|
||||
this.$btn.removeClass('disabled')
|
||||
|
||||
this.$element.find('.has-error, .has-danger, .has-success').removeClass('has-error has-danger has-success')
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
Validator.prototype.destroy = function () {
|
||||
this.reset()
|
||||
|
||||
this.$element
|
||||
.removeAttr('novalidate')
|
||||
.removeData('bs.validator')
|
||||
.off('.bs.validator')
|
||||
|
||||
this.$inputs
|
||||
.off('.bs.validator')
|
||||
|
||||
this.options = null
|
||||
this.validators = null
|
||||
this.$element = null
|
||||
this.$btn = null
|
||||
this.$inputs = null
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
// VALIDATOR PLUGIN DEFINITION
|
||||
// ===========================
|
||||
|
||||
|
||||
function Plugin(option) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
var options = $.extend({}, Validator.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
||||
var data = $this.data('bs.validator')
|
||||
|
||||
if (!data && option == 'destroy') return
|
||||
if (!data) $this.data('bs.validator', (data = new Validator(this, options)))
|
||||
if (typeof option == 'string') data[option]()
|
||||
})
|
||||
}
|
||||
|
||||
var old = $.fn.validator
|
||||
|
||||
$.fn.validator = Plugin
|
||||
$.fn.validator.Constructor = Validator
|
||||
|
||||
|
||||
// VALIDATOR NO CONFLICT
|
||||
// =====================
|
||||
|
||||
$.fn.validator.noConflict = function () {
|
||||
$.fn.validator = old
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
// VALIDATOR DATA-API
|
||||
// ==================
|
||||
|
||||
$(window).on('load', function () {
|
||||
$('form[data-toggle="validator"]').each(function () {
|
||||
var $form = $(this)
|
||||
Plugin.call($form, $form.data())
|
||||
})
|
||||
})
|
||||
|
||||
}(jQuery);
|
131
powerdnsadmin/templates/admin_edit_account.html
Normal file
131
powerdnsadmin/templates/admin_edit_account.html
Normal file
@ -0,0 +1,131 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "admin_accounts" %}
|
||||
{% block title %}<title>Edit Account - {{ SITE_NAME }}</title>{% endblock %}
|
||||
|
||||
{% block dashboard_stat %}
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Account
|
||||
<small>{% if create %}New account{% else %}{{ account.name }}{% endif %}</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
|
||||
<li><a href="{{ url_for('admin.manage_account') }}">Accounts</a></li>
|
||||
<li class="active">{% if create %}Add{% else %}Edit{% endif %} account</li>
|
||||
</ol>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">{% if create %}Add{% else %}Edit{% endif %} account</h3>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<!-- form start -->
|
||||
<form role="form" method="post"
|
||||
action="{% if create %}{{ url_for('admin.edit_account') }}{% else %}{{ url_for('admin.edit_account', account_name=account.name) }}{% endif %}">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="create" value="{{ create }}">
|
||||
<div class="box-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<h4><i class="icon fa fa-ban"></i> Error!</h4>
|
||||
{{ error }}
|
||||
</div>
|
||||
<span class="help-block">{{ error }}</span>
|
||||
{% endif %}
|
||||
<div
|
||||
class="form-group has-feedback {% if invalid_accountname or duplicate_accountname %}has-error{% endif %}">
|
||||
<label class="control-label" for="accountname">Name</label>
|
||||
<input type="text" class="form-control" placeholder="Account Name (required)"
|
||||
name="accountname" {% if account %}value="{{ account.name }}" {% endif %}
|
||||
{% if not create %}disabled{% endif %}>
|
||||
<span class="fa fa-cog form-control-feedback"></span>
|
||||
{% if invalid_accountname %}
|
||||
<span class="help-block">Cannot be blank and must only contain alphanumeric
|
||||
characters.</span>
|
||||
{% elif duplicate_accountname %}
|
||||
<span class="help-block">Account name already in use.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="accountdescription">Description</label>
|
||||
<input type="text" class="form-control" placeholder="Account Description (optional)"
|
||||
name="accountdescription" {% if account %}value="{{ account.description }}" {% endif %}>
|
||||
<span class="fa fa-industry form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="accountcontact">Contact Person</label>
|
||||
<input type="text" class="form-control" placeholder="Contact Person (optional)"
|
||||
name="accountcontact" {% if account %}value="{{ account.contact }}" {% endif %}>
|
||||
<span class="fa fa-user form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="accountmail">Mail Address</label>
|
||||
<input type="email" class="form-control" placeholder="Mail Address (optional)"
|
||||
name="accountmail" {% if account %}value="{{ account.mail }}" {% endif %}>
|
||||
<span class="fa fa-envelope form-control-feedback"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Access Control</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>Users on the right have access to manage records in all domains
|
||||
associated with the account.</p>
|
||||
<p>Click on users to move between columns.</p>
|
||||
<div class="form-group col-xs-2">
|
||||
<select multiple="multiple" class="form-control" id="account_multi_user"
|
||||
name="account_multi_user">
|
||||
{% for user in users %}
|
||||
<option {% if user.id in account_user_ids %}selected{% endif %}
|
||||
value="{{ user.username }}">{{ user.username }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit"
|
||||
class="btn btn-flat btn-primary">{% if create %}Create{% else %}Update{% endif %}
|
||||
Account</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Help with creating a new account</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>
|
||||
An account allows grouping of domains belonging to a particular entity, such as a customer or
|
||||
department.<br />
|
||||
A domain can be assigned to an account upon domain creation or through the domain administration
|
||||
page.
|
||||
</p>
|
||||
<p>Fill in all the fields to the in the form to the left.</p>
|
||||
<p>
|
||||
<strong>Name</strong> is an account identifier. It will be stored as all lowercase letters (no
|
||||
spaces, special characters etc).<br />
|
||||
<strong>Description</strong> is a user friendly name for this account.<br />
|
||||
<strong>Contact person</strong> is the name of a contact person at the account.<br />
|
||||
<strong>Mail Address</strong> is an e-mail address for the contact person.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
$("#account_multi_user").multiSelect();
|
||||
</script>
|
||||
{% endblock %}
|
166
powerdnsadmin/templates/admin_edit_user.html
Normal file
166
powerdnsadmin/templates/admin_edit_user.html
Normal file
@ -0,0 +1,166 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "admin_users" %}
|
||||
{% block title %}<title>Edit User - {{ SITE_NAME }}</title>{% endblock %}
|
||||
|
||||
{% block dashboard_stat %}
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
User
|
||||
<small>{% if create %}New user{% else %}{{ user.username }}{% endif %}</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
|
||||
<li><a href="{{ url_for('admin.manage_user') }}">User</a></li>
|
||||
<li class="active">{% if create %}Add{% else %}Edit{% endif %} user</li>
|
||||
</ol>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">{% if create %}Add{% else %}Edit{% endif %} user</h3>
|
||||
</div>
|
||||
<!-- /.box-header -->
|
||||
<!-- form start -->
|
||||
<form role="form" method="post"
|
||||
action="{% if create %}{{ url_for('admin.edit_user') }}{% else %}{{ url_for('admin.edit_user', user_username=user.username) }}{% endif %}">
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="create" value="{{ create }}">
|
||||
<div class="box-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<h4><i class="icon fa fa-ban"></i> Error!</h4>
|
||||
{{ error }}
|
||||
</div>
|
||||
<span class="help-block">{{ error }}</span>
|
||||
{% endif %}
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="firstname">First Name</label>
|
||||
<input type="text" class="form-control" placeholder="First Name" name="firstname"
|
||||
{% if user %}value="{{ user.firstname }}" {% endif %}> <span
|
||||
class="glyphicon glyphicon-user form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="lastname">Last Name</label>
|
||||
<input type="text" class="form-control" placeholder="Last name" name="lastname"
|
||||
{% if user %}value="{{ user.lastname }}" {% endif %}> <span
|
||||
class="glyphicon glyphicon-user form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="email">E-mail address</label>
|
||||
<input type="email" class="form-control" placeholder="Email" name="email" id="email"
|
||||
{% if user %}value="{{ user.email }}" {% endif %}> <span
|
||||
class="glyphicon glyphicon-envelope form-control-feedback"></span>
|
||||
</div>
|
||||
<p class="login-box-msg">Enter the account details below</p>
|
||||
<div class="form-group has-feedback">
|
||||
<label class="control-label" for="username">Username</label>
|
||||
<input type="text" class="form-control" placeholder="Username" name="username"
|
||||
{% if user %}value="{{ user.username }}" {% endif %}
|
||||
{% if not create %}disabled{% endif %}> <span
|
||||
class="glyphicon glyphicon-user form-control-feedback"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback {% if blank_password %}has-error{% endif %}">
|
||||
<label class="control-label" for="username">Password</label>
|
||||
<input type="password" class="form-control"
|
||||
placeholder="Password {% if create %}(Required){% else %}(Leave blank to keep unchanged){% endif %}"
|
||||
name="password"> <span class="glyphicon glyphicon-lock form-control-feedback"></span>
|
||||
{% if blank_password %}
|
||||
<span class="help-block">The password cannot be blank.</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit"
|
||||
class="btn btn-flat btn-primary">{% if create %}Create{% else %}Update{% endif %}
|
||||
User</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% if not create %}
|
||||
<div class="box box-secondary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Two Factor Authentication</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<p>If two factor authentication was configured and is causing problems due to a lost device or
|
||||
technical issue, it can be disabled here.</p>
|
||||
<p>The user will need to reconfigure two factor authentication, to re-enable it.</p>
|
||||
<p><strong>Beware: This could compromise security!</strong></p>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="button" class="btn btn-flat btn-warning button_otp_disable" id="{{ user.username }}"
|
||||
{% if not user.otp_secret %}disabled{% endif %}>Disable Two Factor Authentication</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Help with {% if create %}creating a new{% else%}updating a{% endif %} user
|
||||
</h3>
|
||||
</div>
|
||||
<div class="box-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 to grant
|
||||
access to the user once it is created via the domain management buttons on the dashboard.</p>
|
||||
{% else %}
|
||||
<p><strong>Password</strong> can be left empty to keep the current password.</p>
|
||||
<p><strong>Username</strong> cannot be changed.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
// handle disabling two factor authentication
|
||||
$(document.body).on('click', '.button_otp_disable', function () {
|
||||
var modal = $("#modal_otp_disable");
|
||||
var username = $(this).prop('id');
|
||||
var info = "Are you sure you want to disable two factor authentication for user " + username + "?";
|
||||
modal.find('.modal-body p').text(info);
|
||||
modal.find('#button_otp_disable_confirm').click(function () {
|
||||
var postdata = {
|
||||
'action': 'user_otp_disable',
|
||||
'data': username,
|
||||
'_csrf_token': '{{ csrf_token() }}'
|
||||
}
|
||||
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manage-user', false, true);
|
||||
})
|
||||
modal.modal('show');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block modals %}
|
||||
<div class="modal fade modal-warning" id="modal_otp_disable">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Confirmation</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-flat btn-default pull-left" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-flat btn-danger" id="button_otp_disable_confirm">Disable Two Factor
|
||||
Authentication</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -6,11 +6,10 @@
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
History <small>Recent PowerDNS-Admin events</small>
|
||||
History <small>Recent events</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard') }}"><i
|
||||
class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li class="active">History</li>
|
||||
</ol>
|
||||
</section>
|
||||
@ -23,7 +22,9 @@
|
||||
<h3 class="box-title">History Management</h3>
|
||||
</div>
|
||||
<div class="box-body clearfix">
|
||||
<button type="button" class="btn btn-flat btn-danger pull-right" data-toggle="modal" data-target="#modal_clear_history" {% if current_user.role.name != 'Administrator' %}disabled{% endif %}>
|
||||
<button type="button" class="btn btn-flat btn-danger pull-right" data-toggle="modal"
|
||||
data-target="#modal_clear_history"
|
||||
{% if current_user.role.name != 'Administrator' %}disabled{% endif %}>
|
||||
Clear History <i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
@ -44,7 +45,8 @@
|
||||
<td>{{ history.msg }}</td>
|
||||
<td>{{ history.created_on }}</td>
|
||||
<td width="6%">
|
||||
<button type="button" class="btn btn-flat btn-primary history-info-button" value='{{ history.detail }}'>Info <i class="fa fa-info"></i>
|
||||
<button type="button" class="btn btn-flat btn-primary history-info-button"
|
||||
value='{{ history.detail }}'>Info <i class="fa fa-info"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@ -65,24 +67,24 @@
|
||||
<script>
|
||||
// set up history data table
|
||||
$("#tbl_history").DataTable({
|
||||
"paging" : true,
|
||||
"lengthChange" : false,
|
||||
"searching" : true,
|
||||
"ordering" : true,
|
||||
"info" : true,
|
||||
"autoWidth" : false,
|
||||
"order": [[ 2, "desc" ]],
|
||||
"columnDefs": [
|
||||
{
|
||||
"type": "time",
|
||||
"render": function ( data, type, row ) {
|
||||
return moment.utc(data).local().format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
"targets": 2
|
||||
}
|
||||
]
|
||||
"paging": true,
|
||||
"lengthChange": false,
|
||||
"searching": true,
|
||||
"ordering": true,
|
||||
"info": true,
|
||||
"autoWidth": false,
|
||||
"order": [
|
||||
[2, "desc"]
|
||||
],
|
||||
"columnDefs": [{
|
||||
"type": "time",
|
||||
"render": function (data, type, row) {
|
||||
return moment.utc(data).local().format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
"targets": 2
|
||||
}]
|
||||
});
|
||||
$(document.body).on('click', '.history-info-button', function() {
|
||||
$(document.body).on('click', '.history-info-button', function () {
|
||||
var modal = $("#modal_history_info");
|
||||
var info = $(this).val();
|
||||
$('#modal-code-content').html(json_library.prettyPrint(info));
|
||||
@ -96,8 +98,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-label="Close">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Confirmation</h4>
|
||||
@ -106,9 +107,10 @@
|
||||
<p>Are you sure you want to remove all history?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-flat btn-default pull-left"
|
||||
data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-flat btn-danger" onclick="applyChanges({'_csrf_token': '{{ csrf_token() }}'}, $SCRIPT_ROOT + '/admin/history', false, true);">Clear History</button>
|
||||
<button type="button" class="btn btn-flat btn-default pull-left" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-flat btn-danger"
|
||||
onclick="applyChanges({'_csrf_token': '{{ csrf_token() }}'}, $SCRIPT_ROOT + '/admin/history', false, true);">Clear
|
||||
History</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.modal-content -->
|
||||
@ -119,8 +121,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-label="Close">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">History Details</h4>
|
||||
@ -129,8 +130,7 @@
|
||||
<pre><code id="modal-code-content"></code></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-flat btn-default pull-right"
|
||||
data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-flat btn-default pull-right" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.modal-content -->
|
@ -8,8 +8,7 @@
|
||||
Accounts <small>Manage accounts</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard') }}"><i
|
||||
class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li class="active">Accounts</li>
|
||||
</ol>
|
||||
</section>
|
||||
@ -22,7 +21,7 @@
|
||||
<h3 class="box-title">Account Management</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<a href="{{ url_for('admin_editaccount') }}">
|
||||
<a href="{{ url_for('admin.edit_account') }}">
|
||||
<button type="button" class="btn btn-flat btn-primary pull-left button_add_account">
|
||||
Add Account <i class="fa fa-plus"></i>
|
||||
</button>
|
||||
@ -51,10 +50,12 @@
|
||||
<td>{{ account.user_num }}</td>
|
||||
<td>{{ account.domains|length }}</td>
|
||||
<td width="15%">
|
||||
<button type="button" class="btn btn-flat btn-success" onclick="window.location.href='{{ url_for('admin_editaccount', account_name=account.name) }}'">
|
||||
<button type="button" class="btn btn-flat btn-success"
|
||||
onclick="window.location.href='{{ url_for('admin.edit_account', account_name=account.name) }}'">
|
||||
Edit <i class="fa fa-cog"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-flat btn-danger button_delete" id="{{ account.name }}">
|
||||
<button type="button" class="btn btn-flat btn-danger button_delete"
|
||||
id="{{ account.name }}">
|
||||
Delete <i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
@ -76,34 +77,40 @@
|
||||
<script>
|
||||
// set up accounts data table
|
||||
$("#tbl_accounts").DataTable({
|
||||
"paging" : true,
|
||||
"lengthChange" : true,
|
||||
"searching" : true,
|
||||
"ordering" : true,
|
||||
"columnDefs": [
|
||||
{ "orderable": false, "targets": [-1] }
|
||||
"paging": true,
|
||||
"lengthChange": true,
|
||||
"searching": true,
|
||||
"ordering": true,
|
||||
"columnDefs": [{
|
||||
"orderable": false,
|
||||
"targets": [-1]
|
||||
}],
|
||||
"info": false,
|
||||
"autoWidth": false,
|
||||
"lengthMenu": [
|
||||
[10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, "All"]
|
||||
],
|
||||
"info" : false,
|
||||
"autoWidth" : false,
|
||||
"lengthMenu": [ [10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, "All"]],
|
||||
"pageLength": 10
|
||||
});
|
||||
|
||||
// handle deletion of account
|
||||
$(document.body).on('click', '.button_delete', function() {
|
||||
$(document.body).on('click', '.button_delete', function () {
|
||||
var modal = $("#modal_delete");
|
||||
var accountname = $(this).prop('id');
|
||||
var info = "Are you sure you want to delete " + accountname + "?";
|
||||
modal.find('.modal-body p').text(info);
|
||||
modal.find('#button_delete_confirm').click(function() {
|
||||
var postdata = {'action': 'delete_account', 'data': accountname, '_csrf_token': '{{ csrf_token() }}'}
|
||||
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manageaccount', false, true);
|
||||
modal.find('#button_delete_confirm').click(function () {
|
||||
var postdata = {
|
||||
'action': 'delete_account',
|
||||
'data': accountname,
|
||||
'_csrf_token': '{{ csrf_token() }}'
|
||||
}
|
||||
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manage-account', false, true);
|
||||
modal.modal('hide');
|
||||
})
|
||||
modal.modal('show');
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block modals %}
|
||||
@ -111,8 +118,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-label="Close">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Confirmation</h4>
|
||||
@ -121,8 +127,7 @@
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-flat btn-default pull-left"
|
||||
data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-flat btn-default pull-left" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-flat btn-danger" id="button_delete_confirm">Delete</button>
|
||||
</div>
|
||||
</div>
|
@ -8,8 +8,7 @@
|
||||
User <small>Manage user privileges</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard') }}"><i
|
||||
class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li class="active">User</li>
|
||||
</ol>
|
||||
</section>
|
||||
@ -22,7 +21,7 @@
|
||||
<h3 class="box-title">User Management</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<a href="{{ url_for('admin_edituser') }}">
|
||||
<a href="{{ url_for('admin.edit_user') }}">
|
||||
<button type="button" class="btn btn-flat btn-primary pull-left button_add_user">
|
||||
Add User <i class="fa fa-plus"></i>
|
||||
</button>
|
||||
@ -49,22 +48,30 @@
|
||||
<td>{{ user.lastname }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
<select id="{{ user.username }}" class="user_role" {% if user.username==current_user.username or (current_user.role.name=='Operator' and user.role.name=='Administrator') %}disabled{% endif %}>
|
||||
<select id="{{ user.username }}" class="user_role"
|
||||
{% if user.username==current_user.username or (current_user.role.name=='Operator' and user.role.name=='Administrator') %}disabled{% endif %}>
|
||||
{% for role in roles %}
|
||||
<option value="{{ role.name }}" {% if role.id==user.role.id %}selected{% endif %}>{{ role.name }}</option>
|
||||
<option value="{{ role.name }}"
|
||||
{% if role.id==user.role.id %}selected{% endif %}>{{ role.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td width="6%">
|
||||
<button type="button" class="btn btn-flat btn-warning button_revoke" id="{{ user.username }}" {% if current_user.role.name=='Operator' and user.role.name=='Administrator' %}disabled{% endif %}>
|
||||
<button type="button" class="btn btn-flat btn-warning button_revoke"
|
||||
id="{{ user.username }}"
|
||||
{% if current_user.role.name=='Operator' and user.role.name=='Administrator' %}disabled{% endif %}>
|
||||
Revoke <i class="fa fa-lock"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td width="15%">
|
||||
<button type="button" class="btn btn-flat btn-success button_edit" onclick="window.location.href='{{ url_for('admin_edituser', user_username=user.username) }}'" {% if current_user.role.name=='Operator' and user.role.name=='Administrator' %}disabled{% endif %}>
|
||||
<button type="button" class="btn btn-flat btn-success button_edit"
|
||||
onclick="window.location.href='{{ url_for('admin.edit_user', user_username=user.username) }}'"
|
||||
{% if current_user.role.name=='Operator' and user.role.name=='Administrator' %}disabled{% endif %}>
|
||||
Edit <i class="fa fa-lock"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-flat btn-danger button_delete" id="{{ user.username }}" {% if user.username==current_user.username or (current_user.role.name=='Operator' and user.role.name=='Administrator') %}disabled{% endif %}>
|
||||
<button type="button" class="btn btn-flat btn-danger button_delete"
|
||||
id="{{ user.username }}"
|
||||
{% if user.username==current_user.username or (current_user.role.name=='Operator' and user.role.name=='Administrator') %}disabled{% endif %}>
|
||||
Delete <i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
@ -86,39 +93,50 @@
|
||||
<script>
|
||||
// set up user data table
|
||||
$("#tbl_users").DataTable({
|
||||
"paging" : true,
|
||||
"lengthChange" : true,
|
||||
"searching" : true,
|
||||
"ordering" : true,
|
||||
"info" : false,
|
||||
"autoWidth" : false,
|
||||
"lengthMenu": [ [10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, "All"]],
|
||||
"paging": true,
|
||||
"lengthChange": true,
|
||||
"searching": true,
|
||||
"ordering": true,
|
||||
"info": false,
|
||||
"autoWidth": false,
|
||||
"lengthMenu": [
|
||||
[10, 25, 50, 100, -1],
|
||||
[10, 25, 50, 100, "All"]
|
||||
],
|
||||
"pageLength": 10
|
||||
});
|
||||
|
||||
// handle revocation of privileges
|
||||
$(document.body).on('click', '.button_revoke', function() {
|
||||
$(document.body).on('click', '.button_revoke', function () {
|
||||
var modal = $("#modal_revoke");
|
||||
var username = $(this).prop('id');
|
||||
var info = "Are you sure you want to revoke all privileges for " + username + ". They will not able to access any domain.";
|
||||
var info = "Are you sure you want to revoke all privileges for " + username +
|
||||
". They will not able to access any domain.";
|
||||
modal.find('.modal-body p').text(info);
|
||||
modal.find('#button_revoke_confirm').click(function() {
|
||||
var postdata = {'action': 'revoke_user_privileges', 'data': username, '_csrf_token': '{{ csrf_token() }}'}
|
||||
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manageuser');
|
||||
modal.find('#button_revoke_confirm').click(function () {
|
||||
var postdata = {
|
||||
'action': 'revoke_user_privileges',
|
||||
'data': username,
|
||||
'_csrf_token': '{{ csrf_token() }}'
|
||||
}
|
||||
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manage-user', true);
|
||||
modal.modal('hide');
|
||||
})
|
||||
modal.modal('show');
|
||||
});
|
||||
// handle deletion of user
|
||||
$(document.body).on('click', '.button_delete', function() {
|
||||
$(document.body).on('click', '.button_delete', function () {
|
||||
var modal = $("#modal_delete");
|
||||
var username = $(this).prop('id');
|
||||
var info = "Are you sure you want to delete " + username + "?";
|
||||
modal.find('.modal-body p').text(info);
|
||||
modal.find('#button_delete_confirm').click(function() {
|
||||
var postdata = {'action': 'delete_user', 'data': username, '_csrf_token': '{{ csrf_token() }}'}
|
||||
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manageuser', false, true);
|
||||
modal.find('#button_delete_confirm').click(function () {
|
||||
var postdata = {
|
||||
'action': 'delete_user',
|
||||
'data': username,
|
||||
'_csrf_token': '{{ csrf_token() }}'
|
||||
}
|
||||
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manage-user', false, true);
|
||||
modal.modal('hide');
|
||||
})
|
||||
modal.modal('show');
|
||||
@ -126,18 +144,18 @@
|
||||
});
|
||||
|
||||
// handle user role changing
|
||||
$(document.body).on('change', '.user_role', function() {
|
||||
$(document.body).on('change', '.user_role', function () {
|
||||
var role_name = this.value;
|
||||
var username = $(this).prop('id');
|
||||
var postdata = {
|
||||
'action' : 'update_user_role',
|
||||
'data' : {
|
||||
'username' : username,
|
||||
'role_name' : role_name
|
||||
'action': 'update_user_role',
|
||||
'data': {
|
||||
'username': username,
|
||||
'role_name': role_name
|
||||
},
|
||||
'_csrf_token' : '{{ csrf_token() }}'
|
||||
'_csrf_token': '{{ csrf_token() }}'
|
||||
};
|
||||
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manageuser', showResult=true);
|
||||
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manage-user', showResult = true);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -146,8 +164,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-label="Close">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Confirmation</h4>
|
||||
@ -156,8 +173,7 @@
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-flat btn-default pull-left"
|
||||
data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-flat btn-default pull-left" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-flat btn-danger" id="button_revoke_confirm">Revoke</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -169,8 +185,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
aria-label="Close">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Confirmation</h4>
|
||||
@ -179,8 +194,7 @@
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-flat btn-default pull-left"
|
||||
data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-flat btn-default pull-left" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-flat btn-danger" id="button_delete_confirm">Delete</button>
|
||||
</div>
|
||||
</div>
|
@ -6,11 +6,10 @@
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<h1>
|
||||
Admin Console
|
||||
PowerDNS server configuration & statistics
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard') }}"><i
|
||||
class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li class="active">Admin Console</li>
|
||||
</ol>
|
||||
</section>
|
||||
@ -36,10 +35,9 @@
|
||||
<tbody>
|
||||
{% for statistic in statistics %}
|
||||
<tr class="odd gradeX">
|
||||
<td><a
|
||||
href="https://google.com/search?q=site:doc.powerdns.com+{{ statistic['name'] }}"
|
||||
target="_blank" class="btn btn-flat btn-xs blue"><i
|
||||
class="fa fa-search"></i></a></td>
|
||||
<td><a href="https://google.com/search?q=site:doc.powerdns.com+{{ statistic['name'] }}"
|
||||
target="_blank" class="btn btn-flat btn-xs blue"><i
|
||||
class="fa fa-search"></i></a></td>
|
||||
<td>{{ statistic['name'] }}</td>
|
||||
<td>{{ statistic['value'] }}</td>
|
||||
</tr>
|
||||
@ -72,15 +70,14 @@
|
||||
<tbody>
|
||||
{% for config in configs %}
|
||||
<tr class="odd gradeX">
|
||||
<td><a
|
||||
href="https://google.com/search?q=site:doc.powerdns.com+{{ config['name'] }}"
|
||||
target="_blank" class="btn btn-flat btn-xs blue"><i
|
||||
class="fa fa-search"></i></a></td>
|
||||
<td><a href="https://google.com/search?q=site:doc.powerdns.com+{{ config['name'] }}"
|
||||
target="_blank" class="btn btn-flat btn-xs blue"><i
|
||||
class="fa fa-search"></i></a></td>
|
||||
<td>{{ config['name'] }}</td>
|
||||
<td>
|
||||
{{ config['value'] }}
|
||||
</td>
|
||||
</tr>
|
||||
<td>
|
||||
{{ config['value'] }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -96,24 +93,24 @@
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
// set up statistics data table
|
||||
$("#tbl_statistics").DataTable({
|
||||
"paging" : true,
|
||||
"lengthChange" : false,
|
||||
"searching" : true,
|
||||
"ordering" : true,
|
||||
"info" : true,
|
||||
"autoWidth" : false
|
||||
});
|
||||
// set up statistics data table
|
||||
$("#tbl_statistics").DataTable({
|
||||
"paging": true,
|
||||
"lengthChange": false,
|
||||
"searching": true,
|
||||
"ordering": true,
|
||||
"info": true,
|
||||
"autoWidth": false
|
||||
});
|
||||
|
||||
// set up configuration data table
|
||||
$("#tbl_configuration").DataTable({
|
||||
"paging" : true,
|
||||
"lengthChange" : false,
|
||||
"searching" : true,
|
||||
"ordering" : true,
|
||||
"info" : true,
|
||||
"autoWidth" : false
|
||||
});
|
||||
// set up configuration data table
|
||||
$("#tbl_configuration").DataTable({
|
||||
"paging": true,
|
||||
"lengthChange": false,
|
||||
"searching": true,
|
||||
"ordering": true,
|
||||
"info": true,
|
||||
"autoWidth": false
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user