enh: Enforce Record Restrictions in API (#1089)

Co-authored-by: Tom <tom@tom.com>
This commit is contained in:
RGanor 2022-06-18 15:20:49 +03:00 committed by GitHub
parent 83d2f3c791
commit 81f158d9bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 78 additions and 5 deletions

View File

@ -5,9 +5,10 @@ from flask import g, request, abort, current_app, Response
from flask_login import current_user from flask_login import current_user
from .models import User, ApiKey, Setting, Domain, Setting from .models import User, ApiKey, Setting, Domain, Setting
from .lib.errors import RequestIsNotJSON, NotEnoughPrivileges from .lib.errors import RequestIsNotJSON, NotEnoughPrivileges, RecordTTLNotAllowed, RecordTypeNotAllowed
from .lib.errors import DomainAccessForbidden, DomainOverrideForbidden from .lib.errors import DomainAccessForbidden, DomainOverrideForbidden
def admin_role_required(f): def admin_role_required(f):
""" """
Grant access if user is in Administrator role Grant access if user is in Administrator role
@ -384,6 +385,60 @@ def apikey_can_configure_dnssec(http_methods=[]):
return decorated_function return decorated_function
return decorator return decorator
def allowed_record_types(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if request.method == 'GET':
return f(*args, **kwargs)
if g.apikey.role.name in ['Administrator', 'Operator']:
return f(*args, **kwargs)
records_allowed_to_edit = Setting().get_records_allow_to_edit()
content = request.get_json()
try:
for record in content['rrsets']:
if 'type' not in record:
raise RecordTypeNotAllowed()
if record['type'] not in records_allowed_to_edit:
current_app.logger.error(f"Error: Record type not allowed: {record['type']}")
raise RecordTypeNotAllowed(message=f"Record type not allowed: {record['type']}")
except (TypeError, KeyError) as e:
raise e
return f(*args, **kwargs)
return decorated_function
def allowed_record_ttl(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not Setting().get('enforce_api_ttl'):
return f(*args, **kwargs)
if request.method == 'GET':
return f(*args, **kwargs)
if g.apikey.role.name in ['Administrator', 'Operator']:
return f(*args, **kwargs)
allowed_ttls = Setting().get_ttl_options()
allowed_numeric_ttls = [ ttl[0] for ttl in allowed_ttls ]
content = request.get_json()
try:
for record in content['rrsets']:
if 'ttl' not in record:
raise RecordTTLNotAllowed()
if record['ttl'] not in allowed_numeric_ttls:
current_app.logger.error(f"Error: Record TTL not allowed: {record['ttl']}")
raise RecordTTLNotAllowed(message=f"Record TTL not allowed: {record['ttl']}")
except (TypeError, KeyError) as e:
raise e
return f(*args, **kwargs)
return decorated_function
def apikey_auth(f): def apikey_auth(f):
@wraps(f) @wraps(f)

View File

@ -170,7 +170,6 @@ class UserUpdateFailEmail(StructuredException):
self.message = message self.message = message
self.name = name self.name = name
class UserDeleteFail(StructuredException): class UserDeleteFail(StructuredException):
status_code = 500 status_code = 500
@ -178,3 +177,19 @@ class UserDeleteFail(StructuredException):
StructuredException.__init__(self) StructuredException.__init__(self)
self.message = message self.message = message
self.name = name self.name = name
class RecordTypeNotAllowed(StructuredException):
status_code = 400
def __init__(self, name=None, message="Record type not allowed or does not present"):
StructuredException.__init__(self)
self.message = message
self.name = name
class RecordTTLNotAllowed(StructuredException):
status_code = 400
def __init__(self, name=None, message="Record TTL not allowed or does not present"):
StructuredException.__init__(self)
self.message = message
self.name = name

View File

@ -28,7 +28,7 @@ class Setting(db.Model):
'allow_user_create_domain': False, 'allow_user_create_domain': False,
'allow_user_remove_domain': False, 'allow_user_remove_domain': False,
'allow_user_view_history': False, 'allow_user_view_history': False,
'delete_sso_accounts': False, 'delete_sso_accounts': False,
'bg_domain_updates': False, 'bg_domain_updates': False,
'enable_api_rr_history': True, 'enable_api_rr_history': True,
'site_name': 'PowerDNS-Admin', 'site_name': 'PowerDNS-Admin',
@ -110,6 +110,7 @@ class Setting(db.Model):
'oidc_oauth_email': 'email', 'oidc_oauth_email': 'email',
'oidc_oauth_account_name_property': '', 'oidc_oauth_account_name_property': '',
'oidc_oauth_account_description_property': '', 'oidc_oauth_account_description_property': '',
'enforce_api_ttl': False,
'forward_records_allow_edit': { 'forward_records_allow_edit': {
'A': True, 'A': True,
'AAAA': True, 'AAAA': True,

View File

@ -1268,7 +1268,7 @@ def setting_basic():
'allow_user_create_domain', 'allow_user_remove_domain', 'allow_user_view_history', 'bg_domain_updates', 'site_name', 'allow_user_create_domain', 'allow_user_remove_domain', 'allow_user_view_history', 'bg_domain_updates', 'site_name',
'session_timeout', 'warn_session_timeout', 'ttl_options', 'session_timeout', 'warn_session_timeout', 'ttl_options',
'pdns_api_timeout', 'verify_ssl_connections', 'verify_user_email', 'pdns_api_timeout', 'verify_ssl_connections', 'verify_user_email',
'delete_sso_accounts', 'otp_field_enabled', 'custom_css', 'enable_api_rr_history', 'max_history_records', 'otp_force', 'deny_domain_override' 'delete_sso_accounts', 'otp_field_enabled', 'custom_css', 'enable_api_rr_history', 'max_history_records', 'otp_force', 'deny_domain_override', 'enforce_api_ttl'
] ]
return render_template('admin_setting_basic.html', settings=settings) return render_template('admin_setting_basic.html', settings=settings)

View File

@ -30,7 +30,7 @@ from ..decorators import (
apikey_can_create_domain, apikey_can_remove_domain, apikey_can_create_domain, apikey_can_remove_domain,
apikey_is_admin, apikey_can_access_domain, apikey_can_configure_dnssec, apikey_is_admin, apikey_can_access_domain, apikey_can_configure_dnssec,
api_role_can, apikey_or_basic_auth, api_role_can, apikey_or_basic_auth,
callback_if_request_body_contains_key, callback_if_request_body_contains_key, allowed_record_types, allowed_record_ttl
) )
import secrets import secrets
import string import string
@ -1079,6 +1079,8 @@ def api_zone_subpath_forward(server_id, zone_id, subpath):
@api_bp.route('/servers/<string:server_id>/zones/<string:zone_id>', @api_bp.route('/servers/<string:server_id>/zones/<string:zone_id>',
methods=['GET', 'PUT', 'PATCH', 'DELETE']) methods=['GET', 'PUT', 'PATCH', 'DELETE'])
@apikey_auth @apikey_auth
@allowed_record_types
@allowed_record_ttl
@apikey_can_access_domain @apikey_can_access_domain
@apikey_can_remove_domain(http_methods=['DELETE']) @apikey_can_remove_domain(http_methods=['DELETE'])
@callback_if_request_body_contains_key(apikey_can_configure_dnssec()(), @callback_if_request_body_contains_key(apikey_can_configure_dnssec()(),