mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2025-06-14 12:06:06 +00:00
Refactoring the code
- Use Flask blueprint - Split model and views into smaller parts - Bug fixes - API adjustment
This commit is contained in:
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'))
|
518
powerdnsadmin/routes/api.py
Normal file
518
powerdnsadmin/routes/api.py
Normal file
@ -0,0 +1,518 @@
|
||||
import json
|
||||
from urllib.parse import urljoin
|
||||
from flask import Blueprint, g, request, abort, current_app
|
||||
from flask_login import current_user
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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_bp.errorhandler(401)
|
||||
def handle_401(err):
|
||||
return json.dumps({"msg": "Unauthorized"}), 401
|
||||
|
||||
|
||||
@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_bp.errorhandler(DomainNotExists)
|
||||
def handle_domain_not_exists(err):
|
||||
return json.dumps(err.to_dict()), err.status_code
|
||||
|
||||
|
||||
@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_bp.errorhandler(ApiKeyCreateFail)
|
||||
def handle_apikey_create_fail(err):
|
||||
return json.dumps(err.to_dict()), err.status_code
|
||||
|
||||
|
||||
@api_bp.errorhandler(ApiKeyNotUsable)
|
||||
def handle_apikey_not_usable(err):
|
||||
return json.dumps(err.to_dict()), err.status_code
|
||||
|
||||
|
||||
@api_bp.errorhandler(NotEnoughPrivileges)
|
||||
def handle_not_enough_privileges(err):
|
||||
return json.dumps(err.to_dict()), err.status_code
|
||||
|
||||
|
||||
@api_bp.errorhandler(RequestIsNotJSON)
|
||||
def handle_request_is_not_json(err):
|
||||
return json.dumps(err.to_dict()), err.status_code
|
||||
|
||||
|
||||
@api_bp.before_request
|
||||
@is_json
|
||||
def before_request():
|
||||
pass
|
||||
|
||||
|
||||
@api_bp.route('/pdnsadmin/zones', methods=['POST'])
|
||||
@api_basic_auth
|
||||
@api_can_create_domain
|
||||
def api_login_create_zone():
|
||||
pdns_api_url = Setting().get('pdns_api_url')
|
||||
pdns_api_key = Setting().get('pdns_api_key')
|
||||
pdns_version = Setting().get('pdns_version')
|
||||
api_uri_with_prefix = utils.pdns_api_extended_uri(pdns_version)
|
||||
api_full_uri = api_uri_with_prefix + '/servers/localhost/zones'
|
||||
headers = {}
|
||||
headers['X-API-Key'] = pdns_api_key
|
||||
|
||||
msg_str = "Sending request to powerdns API {0}"
|
||||
msg = msg_str.format(request.get_json(force=True))
|
||||
current_app.logger.debug(msg)
|
||||
|
||||
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:
|
||||
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=current_user.username)
|
||||
history.add()
|
||||
|
||||
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([current_user.username])
|
||||
|
||||
domain = Domain()
|
||||
domain.update()
|
||||
|
||||
if resp.status_code == 409:
|
||||
raise(DomainAlreadyExists)
|
||||
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@api_bp.route('/pdnsadmin/zones', methods=['GET'])
|
||||
@api_basic_auth
|
||||
def api_login_list_zones():
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
domain_obj_list = get_user_domains()
|
||||
else:
|
||||
domain_obj_list = Domain.query.all()
|
||||
|
||||
domain_obj_list = [] if domain_obj_list is None else domain_obj_list
|
||||
return json.dumps(domain_schema.dump(domain_obj_list)), 200
|
||||
|
||||
|
||||
@api_bp.route('/pdnsadmin/zones/<string:domain_name>', methods=['DELETE'])
|
||||
@api_basic_auth
|
||||
@api_can_create_domain
|
||||
def api_login_delete_zone(domain_name):
|
||||
pdns_api_url = Setting().get('pdns_api_url')
|
||||
pdns_api_key = Setting().get('pdns_api_key')
|
||||
pdns_version = Setting().get('pdns_version')
|
||||
api_uri_with_prefix = utils.pdns_api_extended_uri(pdns_version)
|
||||
api_full_uri = api_uri_with_prefix + '/servers/localhost/zones'
|
||||
api_full_uri += '/' + domain_name
|
||||
headers = {}
|
||||
headers['X-API-Key'] = pdns_api_key
|
||||
|
||||
domain = Domain.query.filter(Domain.name == domain_name)
|
||||
|
||||
if not domain:
|
||||
abort(404)
|
||||
|
||||
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}"
|
||||
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')
|
||||
|
||||
if resp.status_code == 204:
|
||||
current_app.logger.debug("Request to powerdns API successful")
|
||||
|
||||
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:
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(500)
|
||||
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@api_bp.route('/pdnsadmin/apikeys', methods=['POST'])
|
||||
@api_basic_auth
|
||||
def api_generate_apikey():
|
||||
data = request.get_json()
|
||||
description = None
|
||||
role_name = None
|
||||
apikey = None
|
||||
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 'role' not in data else None
|
||||
|
||||
description = data['description'] if 'description' in data else None
|
||||
role_name = data['role']
|
||||
domains = data['domains']
|
||||
|
||||
if role_name == 'User' and len(domains) == 0:
|
||||
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"
|
||||
current_app.logger.error(msg)
|
||||
raise DomainNotExists(message=msg)
|
||||
|
||||
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"
|
||||
current_app.logger.error(msg)
|
||||
raise NotEnoughPrivileges(message=msg)
|
||||
|
||||
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]
|
||||
|
||||
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"
|
||||
current_app.logger.error(msg)
|
||||
raise DomainAccessForbidden(message=msg)
|
||||
|
||||
apikey = ApiKey(desc=description,
|
||||
role_name=role_name,
|
||||
domains=domain_obj_list)
|
||||
|
||||
try:
|
||||
apikey.create()
|
||||
except Exception as 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
|
||||
|
||||
|
||||
@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 = []
|
||||
current_app.logger.debug("Getting apikeys")
|
||||
|
||||
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)
|
||||
current_app.logger.debug(msg)
|
||||
apikeys = current_user.get_apikeys(domain_name)
|
||||
|
||||
if not apikeys:
|
||||
raise DomainAccessForbidden(name=domain_name)
|
||||
|
||||
current_app.logger.debug(apikey_schema.dump(apikeys))
|
||||
else:
|
||||
msg_str = "Getting all allowed domains for user {0}"
|
||||
msg = msg_str.format(current_user.username)
|
||||
current_app.logger.debug(msg)
|
||||
|
||||
try:
|
||||
apikeys = current_user.get_apikeys()
|
||||
current_app.logger.debug(apikey_schema.dump(apikeys))
|
||||
except Exception as e:
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(500)
|
||||
else:
|
||||
current_app.logger.debug("Getting all domains for administrative user")
|
||||
try:
|
||||
apikeys = ApiKey.query.all()
|
||||
current_app.logger.debug(apikey_schema.dump(apikeys))
|
||||
except Exception as e:
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(500)
|
||||
|
||||
return json.dumps(apikey_schema.dump(apikeys)), 200
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
if not apikey:
|
||||
abort(404)
|
||||
|
||||
current_app.logger.debug(current_user.role.name)
|
||||
|
||||
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]
|
||||
apikeys_ids = [apikey_item.id for apikey_item in apikeys]
|
||||
|
||||
inter = set(apikey_domains_list).intersection(set(user_domains_list))
|
||||
|
||||
if not (len(inter) == len(apikey_domains_list)):
|
||||
msg = "You don't have access to some domains apikey belongs to"
|
||||
current_app.logger.error(msg)
|
||||
raise DomainAccessForbidden(message=msg)
|
||||
|
||||
if apikey_id not in apikeys_ids:
|
||||
raise DomainAccessForbidden()
|
||||
|
||||
try:
|
||||
apikey.delete()
|
||||
except Exception as e:
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(500)
|
||||
|
||||
return '', 204
|
||||
|
||||
|
||||
@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
|
||||
# if apikey domains are different and user is allowed to handle
|
||||
# that domains update domains
|
||||
data = request.get_json()
|
||||
description = data['description'] if 'description' in data else None
|
||||
role_name = data['role'] if 'role' in data else None
|
||||
domains = data['domains'] if 'domains' in data else None
|
||||
domain_obj_list = None
|
||||
|
||||
apikey = ApiKey.query.get(apikey_id)
|
||||
|
||||
if not apikey:
|
||||
abort(404)
|
||||
|
||||
current_app.logger.debug('Updating apikey with id {0}'.format(apikey_id))
|
||||
|
||||
if role_name == 'User' and len(domains) == 0:
|
||||
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"
|
||||
current_app.logger.error(msg)
|
||||
raise DomainNotExists(message=msg)
|
||||
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
if role_name != 'User':
|
||||
msg = "User cannot assign other role than User"
|
||||
current_app.logger.error(msg)
|
||||
raise NotEnoughPrivileges(message=msg)
|
||||
|
||||
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 = 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]
|
||||
|
||||
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"
|
||||
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'
|
||||
current_app.logger.error(msg)
|
||||
raise DomainAccessForbidden()
|
||||
|
||||
if set(domains) == set(apikey_domains):
|
||||
current_app.logger.debug(
|
||||
"Domains are same, apikey domains won't be updated")
|
||||
domains = None
|
||||
|
||||
if role_name == apikey.role:
|
||||
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"
|
||||
current_app.logger.debug(msg)
|
||||
description = None
|
||||
|
||||
try:
|
||||
apikey = ApiKey.query.get(apikey_id)
|
||||
apikey.update(role_name=role_name,
|
||||
domains=domains,
|
||||
description=description)
|
||||
except Exception as e:
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
abort(500)
|
||||
|
||||
return '', 204
|
||||
|
||||
|
||||
@api_bp.route(
|
||||
'/servers/<string:server_id>/zones/<string:zone_id>/<path:subpath>',
|
||||
methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
|
||||
@apikey_auth
|
||||
@apikey_can_access_domain
|
||||
def api_zone_subpath_forward(server_id, zone_id, subpath):
|
||||
resp = helper.forward_request()
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@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):
|
||||
resp = helper.forward_request()
|
||||
domain = Domain()
|
||||
domain.update()
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@api_bp.route('/servers', methods=['GET'])
|
||||
@apikey_auth
|
||||
@apikey_is_admin
|
||||
def api_server_forward():
|
||||
resp = helper.forward_request()
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@api_bp.route('/servers/<path:subpath>', methods=['GET', 'PUT'])
|
||||
@apikey_auth
|
||||
@apikey_is_admin
|
||||
def api_server_sub_forward(subpath):
|
||||
resp = helper.forward_request()
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@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:
|
||||
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.add()
|
||||
|
||||
if g.apikey.role.name not in ['Administrator', 'Operator']:
|
||||
current_app.logger.debug(
|
||||
"Apikey is user key, assigning created domain")
|
||||
domain = Domain(name=data['name'].rstrip('.'))
|
||||
g.apikey.domains.append(domain)
|
||||
|
||||
domain = Domain()
|
||||
domain.update()
|
||||
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@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
|
||||
else:
|
||||
domain_obj_list = Domain.query.all()
|
||||
return json.dumps(domain_schema.dump(domain_obj_list)), 200
|
||||
|
||||
|
||||
#endpoint to snychronize Domains in background
|
||||
@api_bp.route('/sync_domains', methods=['GET'])
|
||||
@apikey_auth
|
||||
def sync_domains():
|
||||
domain = Domain()
|
||||
domain.update()
|
||||
return 'Finished synchronization in background', 200
|
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
|
||||
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 correct your input"), 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'
|
||||
}
|
Reference in New Issue
Block a user