Commit 5bbdc90f authored by kaiyou's avatar kaiyou
Browse files
parents 11fc958b b2d72d66
Pipeline #773 failed with stage
in 4 seconds
### 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
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
......@@ -25,20 +25,17 @@ nosetests.xml
flask
config.py
configs/production.py
logfile.log
settings.json
advanced_settings.json
idp.crt
log.txt
pdns.db
idp.crt
*.bak
db_repository/*
upload/avatar/*
tmp/*
.ropeproject
.sonarlint/*
pdns.db
node_modules
powerdnsadmin/static/generated
.webassets-cache
app/static/generated
.venv*
.pytest_cache
language: python
python:
- "3.5.2"
before_install:
- sudo apt-key adv --fetch-keys http://dl.yarnpkg.com/debian/pubkey.gpg
- echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
- travis_retry sudo apt-get update
- travis_retry sudo apt-get install python3-dev libxml2-dev libxmlsec1-dev yarn
- mysql -e 'CREATE DATABASE pda';
- mysql -e "GRANT ALL PRIVILEGES ON pda.* to pda@'%' IDENTIFIED BY 'changeme'";
install:
- pip install -r requirements.txt
before_script:
- mv config_template.py config.py
- export FLASK_APP=app/__init__.py
- flask db upgrade
- yarn install --pure-lockfile
- flask assets build
language: minimal
script:
- sh run_travis.sh
cache:
yarn: true
- docker-compose -f docker-compose-test.yml up --exit-code-from powerdns-admin --abort-on-container-exit
services:
- mysql
\ No newline at end of file
- docker
--*.modules-folder "./app/static/node_modules"
--*.modules-folder "./powerdnsadmin/static/node_modules"
......@@ -12,24 +12,22 @@ A PowerDNS web interface with advanced features.
- User access management based on domain
- User activity logging
- Support Local DB / SAML / LDAP / Active Directory user authentication
- Support Google / Github / OpenID OAuth
- Support Google / Github / Azure / OpenID OAuth
- Support Two-factor authentication (TOTP)
- Dashboard and pdns service statistics
- DynDNS 2 protocol support
- Edit IPv6 PTRs using IPv6 addresses directly (no more editing of literal addresses!)
- 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
Step 1: Update the configuration
The configuration file for developement environment is located at `configs/development.py`, you can override some configs by editing `.env` file.
Edit the `docker-compose.yml` file to update the database connection string in `SQLALCHEMY_DATABASE_URI`. Other environment variables are mentioned in the [legal_envvars](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/configs/docker_config.py#L5-L37).
To use docker secret feature it is possible to append `_FILE` to the environment variables and point to a file with the value stored in it.
Step 2: Build docker images
```$ docker-compose build```
Step 3: Start docker containers
Step 2: Start docker container
```$ docker-compose up```
......@@ -39,4 +37,3 @@ You can now access PowerDNS-Admin at url http://localhost:9191
### Screenshots
![dashboard](https://user-images.githubusercontent.com/6447444/44068603-0d2d81f6-9fa5-11e8-83af-14e2ad79e370.png)
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
# 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 import views
from functools import wraps
from flask import g, redirect, url_for
from app.models import Setting
def admin_role_required(f):
"""
Grant access if user is in Administrator role
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user.role.name != 'Administrator':
return redirect(url_for('error', code=401))
return f(*args, **kwargs)
return decorated_function
def operator_role_required(f):
"""
Grant access if user is in Operator role or higher
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user.role.name not in ['Administrator', 'Operator']:
return redirect(url_for('error', code=401))
return f(*args, **kwargs)
return decorated_function
def can_access_domain(f):
"""
Grant access if:
- user is in Operator role or higher, or
- user is in granted Account, or
- user is in granted Domain
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user.role.name not in ['Administrator', 'Operator']:
domain_name = kwargs.get('domain_name')
user_domain = [d.name for d in g.user.get_domain()]
if domain_name not in user_domain:
return redirect(url_for('error', code=401))
return f(*args, **kwargs)
return decorated_function
def can_configure_dnssec(f):
"""
Grant access if:
- user is in Operator role or higher, or
- dnssec_admins_only is off
"""
@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))
return f(*args, **kwargs)
return decorated_function
def can_create_domain(f):
"""
Grant access if:
- user is in Operator role or higher, or
- allow_user_create_domain is on
"""
@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))
return f(*args, **kwargs)
return decorated_function
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('[%(levelname)s] %(message)s')
stderr_log_handler.setFormatter(console_formatter)
return logging.getLogger(self.name)
\ No newline at end of file
This diff is collapsed.
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
\ No newline at end of file
{% 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">&times;</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.