Merge branch 'master' into AdminLTE-Upgrade

This commit is contained in:
Matt Scott
2023-02-17 18:17:32 -05:00
committed by GitHub
33 changed files with 824 additions and 599 deletions

View File

@ -610,14 +610,21 @@ def manage_user():
@operator_role_required
def edit_account(account_name=None):
users = User.query.all()
account = Account.query.filter(
Account.name == account_name).first()
all_accounts = Account.query.all()
accounts = {acc.id: acc for acc in all_accounts}
domains = Domain.query.all()
if request.method == 'GET':
if account_name is None:
if account_name is None or not account:
return render_template('admin_edit_account.html',
account=None,
account_user_ids=[],
users=users,
domains=domains,
accounts=accounts,
create=1)
else:
account = Account.query.filter(
Account.name == account_name).first()
@ -626,11 +633,14 @@ def edit_account(account_name=None):
account=account,
account_user_ids=account_user_ids,
users=users,
domains=domains,
accounts=accounts,
create=0)
if request.method == 'POST':
fdata = request.form
new_user_list = request.form.getlist('account_multi_user')
new_domain_list = request.form.getlist('account_domains')
# on POST, synthesize account and account_user_ids from form data
if not account_name:
@ -654,6 +664,8 @@ def edit_account(account_name=None):
account=account,
account_user_ids=account_user_ids,
users=users,
domains=domains,
accounts=accounts,
create=create,
invalid_accountname=True)
@ -662,19 +674,33 @@ def edit_account(account_name=None):
account=account,
account_user_ids=account_user_ids,
users=users,
domains=domains,
accounts=accounts,
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 = Account.query.filter(
Account.name == account_name).first()
old_domains = Domain.query.filter(Domain.account_id == account.id).all()
for domain_name in new_domain_list:
domain = Domain.query.filter(
Domain.name == domain_name).first()
if account.id != domain.account_id:
Domain(name=domain_name).assoc_account(account.id)
for domain in old_domains:
if domain.name not in new_domain_list:
Domain(name=domain.name).assoc_account(None)
history = History(msg='{0} account {1}'.format('Create' if create else 'Update', account.name),
created_by=current_user.username)
account.grant_privileges(new_user_list)
history.add()
return redirect(url_for('admin.manage_account'))
@ -891,6 +917,16 @@ class DetailedHistory():
''',
history_status=DetailedHistory.get_key_val(detail_dict, 'status'),
history_msg=DetailedHistory.get_key_val(detail_dict, 'msg'))
elif 'Update domain' in history.msg and 'associate account' in history.msg: # When an account gets associated or dissociate with domains
self.detailed_msg = render_template_string('''
<table class="table table-bordered table-striped">
<tr><td>Associate: </td><td>{{ history_assoc_account }}</td></tr>
<tr><td>Dissociate:</td><td>{{ history_dissoc_account }}</td></tr>
</table>
''',
history_assoc_account=DetailedHistory.get_key_val(detail_dict, 'assoc_account'),
history_dissoc_account=DetailedHistory.get_key_val(detail_dict, 'dissoc_account'))
# check for lower key as well for old databases
@staticmethod
@ -928,6 +964,13 @@ def history():
'msg': 'You do not have permission to remove history.'
}), 401)
if Setting().get('preserve_history'):
return make_response(
jsonify({
'status': 'error',
'msg': 'History removal is not allowed (toggle preserve_history in settings).'
}), 401)
h = History()
result = h.remove_all()
if result:
@ -1283,6 +1326,7 @@ def setting_basic():
'otp_field_enabled',
'otp_force',
'pdns_api_timeout',
'preserve_history',
'pretty_ipv6_ptr',
'record_helper',
'record_quick_edit',

View File

@ -1,22 +1,21 @@
import json
from urllib.parse import urljoin
import secrets
import string
from base64 import b64encode
from flask import (
Blueprint, g, request, abort, current_app, make_response, jsonify,
)
from urllib.parse import urljoin
from flask import (Blueprint, g, request, abort, current_app, make_response, jsonify)
from flask_login import current_user
from .base import csrf
from ..models.base import db
from ..models import (
User, Domain, DomainUser, Account, AccountUser, History, Setting, ApiKey,
Role,
from ..decorators import (
api_basic_auth, api_can_create_domain, is_json, apikey_auth,
apikey_can_create_domain, apikey_can_remove_domain,
apikey_is_admin, apikey_can_access_domain, apikey_can_configure_dnssec,
api_role_can, apikey_or_basic_auth,
callback_if_request_body_contains_key, allowed_record_types, allowed_record_ttl
)
from ..lib import utils, helper
from ..lib.schema import (
ApiKeySchema, DomainSchema, ApiPlainKeySchema, UserSchema, AccountSchema,
UserDetailedSchema,
)
from ..lib.errors import (
StructuredException,
DomainNotExists, DomainAlreadyExists, DomainAccessForbidden,
@ -26,15 +25,15 @@ from ..lib.errors import (
UserCreateFail, UserCreateDuplicate, UserUpdateFail, UserDeleteFail,
UserUpdateFailEmail, InvalidAccountNameException
)
from ..decorators import (
api_basic_auth, api_can_create_domain, is_json, apikey_auth,
apikey_can_create_domain, apikey_can_remove_domain,
apikey_is_admin, apikey_can_access_domain, apikey_can_configure_dnssec,
api_role_can, apikey_or_basic_auth,
callback_if_request_body_contains_key, allowed_record_types, allowed_record_ttl
from ..lib.schema import (
ApiKeySchema, DomainSchema, ApiPlainKeySchema, UserSchema, AccountSchema,
UserDetailedSchema,
)
import secrets
import string
from ..models import (
User, Domain, DomainUser, Account, AccountUser, History, Setting, ApiKey,
Role,
)
from ..models.base import db
api_bp = Blueprint('api', __name__, url_prefix='/api/v1')
apilist_bp = Blueprint('apilist', __name__, url_prefix='/')
@ -56,10 +55,10 @@ def get_user_domains():
.outerjoin(Account, Domain.account_id == Account.id) \
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
.filter(
db.or_(
DomainUser.user_id == current_user.id,
AccountUser.user_id == current_user.id
)).all()
db.or_(
DomainUser.user_id == current_user.id,
AccountUser.user_id == current_user.id
)).all()
return domains
@ -71,10 +70,10 @@ def get_user_apikeys(domain_name=None):
.outerjoin(Account, Domain.account_id == Account.id) \
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
.filter(
db.or_(
DomainUser.user_id == User.id,
AccountUser.user_id == User.id
)
db.or_(
DomainUser.user_id == User.id,
AccountUser.user_id == User.id
)
) \
.filter(User.id == current_user.id)
@ -167,12 +166,7 @@ def handle_request_is_not_json(err):
def before_request():
# Check site is in maintenance mode
maintenance = Setting().get('maintenance')
if (
maintenance and current_user.is_authenticated and
current_user.role.name not in [
'Administrator', 'Operator'
]
):
if (maintenance and current_user.is_authenticated and current_user.role.name not in ['Administrator', 'Operator']):
return make_response(
jsonify({
"status": False,
@ -224,14 +218,13 @@ def api_login_create_zone():
history = History(msg='Add domain {0}'.format(
data['name'].rstrip('.')),
detail=json.dumps(data),
created_by=current_user.username,
domain_id=domain_id)
detail=json.dumps(data),
created_by=current_user.username,
domain_id=domain_id)
history.add()
if current_user.role.name not in ['Administrator', 'Operator']:
current_app.logger.debug(
"User is ordinary user, assigning created domain")
current_app.logger.debug("User is ordinary user, assigning created domain")
domain = Domain(name=data['name'].rstrip('.'))
domain.update()
domain.grant_privileges([current_user.id])
@ -299,9 +292,9 @@ def api_login_delete_zone(domain_name):
history = History(msg='Delete domain {0}'.format(
utils.pretty_domain_name(domain_name)),
detail='',
created_by=current_user.username,
domain_id=domain_id)
detail='',
created_by=current_user.username,
domain_id=domain_id)
history.add()
except Exception as e:
@ -326,14 +319,14 @@ def api_generate_apikey():
if 'domains' not in data:
domains = []
elif not isinstance(data['domains'], (list, )):
elif not isinstance(data['domains'], (list,)):
abort(400)
else:
domains = [d['name'] if isinstance(d, dict) else d for d in data['domains']]
if 'accounts' not in data:
accounts = []
elif not isinstance(data['accounts'], (list, )):
elif not isinstance(data['accounts'], (list,)):
abort(400)
else:
accounts = [a['name'] if isinstance(a, dict) else a for a in data['accounts']]
@ -385,8 +378,7 @@ def api_generate_apikey():
user_domain_list = [item.name for item in user_domain_obj_list]
current_app.logger.debug("Input domain list: {0}".format(domain_list))
current_app.logger.debug(
"User domain list: {0}".format(user_domain_list))
current_app.logger.debug("User domain list: {0}".format(user_domain_list))
inter = set(domain_list).intersection(set(user_domain_list))
@ -539,14 +531,14 @@ def api_update_apikey(apikey_id):
if 'domains' not in data:
domains = None
elif not isinstance(data['domains'], (list, )):
elif not isinstance(data['domains'], (list,)):
abort(400)
else:
domains = [d['name'] if isinstance(d, dict) else d for d in data['domains']]
if 'accounts' not in data:
accounts = None
elif not isinstance(data['accounts'], (list, )):
elif not isinstance(data['accounts'], (list,)):
abort(400)
else:
accounts = [a['name'] if isinstance(a, dict) else a for a in data['accounts']]
@ -963,9 +955,7 @@ def api_delete_account(account_id):
account = account_list[0]
else:
abort(404)
current_app.logger.debug(
f'Deleting Account {account.name}'
)
current_app.logger.debug(f'Deleting Account {account.name}')
# Remove account association from domains first
if len(account.domains) > 0:
@ -1047,7 +1037,7 @@ def api_remove_account_user(account_id, user_id):
user_list = User.query.join(AccountUser).filter(
AccountUser.account_id == account_id,
AccountUser.user_id == user_id,
).all()
).all()
if not user_list:
abort(404)
if not account.remove_user(user):
@ -1123,9 +1113,9 @@ def api_zone_forward(server_id, zone_id):
history = History(msg='{0} zone {1} record of {2}'.format(
rrset_data['changetype'].lower(), rrset_data['type'],
rrset_data['name'].rstrip('.')),
detail=json.dumps(data),
created_by=g.apikey.description,
domain_id=Domain().get_id_by_name(zone_id.rstrip('.')))
detail=json.dumps(data),
created_by=g.apikey.description,
domain_id=Domain().get_id_by_name(zone_id.rstrip('.')))
history.add()
elif request.method == 'DELETE':
history = History(msg='Deleted zone {0}'.format(zone_id.rstrip('.')),
@ -1192,17 +1182,13 @@ def api_get_zones(server_id):
return jsonify(domain_schema.dump(domain_obj_list)), 200
else:
resp = helper.forward_request()
if (
g.apikey.role.name not in ['Administrator', 'Operator']
and resp.status_code == 200
):
if (g.apikey.role.name not in ['Administrator', 'Operator'] and resp.status_code == 200):
domain_list = [d['name']
for d in domain_schema.dump(g.apikey.domains)]
accounts_domains = [d.name for a in g.apikey.accounts for d in a.domains]
allowed_domains = set(domain_list + accounts_domains)
current_app.logger.debug("Account domains: {}".format(
'/'.join(accounts_domains)))
current_app.logger.debug("Account domains: {}".format('/'.join(accounts_domains)))
content = json.dumps([i for i in json.loads(resp.content)
if i['name'].rstrip('.') in allowed_domains])
return content, resp.status_code, resp.headers.items()
@ -1223,6 +1209,7 @@ def api_server_config_forward(server_id):
resp = helper.forward_request()
return resp.content, resp.status_code, resp.headers.items()
# The endpoint to synchronize Domains in background
@api_bp.route('/sync_domains', methods=['GET'])
@apikey_or_basic_auth
@ -1231,6 +1218,7 @@ def sync_domains():
domain.update()
return 'Finished synchronization in background', 200
@api_bp.route('/health', methods=['GET'])
@apikey_auth
def health():
@ -1244,7 +1232,8 @@ def health():
try:
domain.get_domain_info(domain_to_query.name)
except Exception as e:
current_app.logger.error("Health Check - Failed to query authoritative server for domain {}".format(domain_to_query.name))
current_app.logger.error(
"Health Check - Failed to query authoritative server for domain {}".format(domain_to_query.name))
return make_response("Down", 503)
return make_response("Up", 200)

View File

@ -89,14 +89,14 @@ def domain(domain_name):
# - Find a way to make it consistent, or
# - Only allow one comment for that case
if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'):
pretty_v6 = Setting().get('pretty_ipv6_ptr')
for r in rrsets:
if r['type'] in records_allow_to_edit:
r_name = r['name'].rstrip('.')
# If it is reverse zone and pretty_ipv6_ptr setting
# is enabled, we reformat the name for ipv6 records.
if Setting().get('pretty_ipv6_ptr') and r[
'type'] == 'PTR' and 'ip6.arpa' in r_name and '*' not in r_name:
if pretty_v6 and r['type'] == 'PTR' and 'ip6.arpa' in r_name and '*' not in r_name:
r_name = dns.reversename.to_address(
dns.name.from_text(r_name))

View File

@ -581,7 +581,7 @@ def get_azure_groups(uri):
def authenticate_user(user, authenticator, remember=False):
login_user(user, remember=remember)
signin_history(user.username, authenticator, True)
if Setting().get('otp_force') and Setting().get('otp_field_enabled') and not user.otp_secret:
if Setting().get('otp_force') and Setting().get('otp_field_enabled') and not user.otp_secret and session['authentication_type'] not in ['OAuth']:
user.update_profile(enable_otp=True)
user_id = current_user.id
prepare_welcome_user(user_id)