From 07f0d215a7d0969d45dd36127f737cffe87d6476 Mon Sep 17 00:00:00 2001 From: zoeller-freinet <86965592+zoeller-freinet@users.noreply.github.com> Date: Mon, 6 Dec 2021 22:38:16 +0100 Subject: [PATCH] PDNS-API: factor in 'dnssec_admins_only' basic setting (#1055) `GET cryptokeys/{cryptokey_id}` returns the private key, which justifies that the setting is honored in this case. --- powerdnsadmin/__init__.py | 2 ++ powerdnsadmin/decorators.py | 52 ++++++++++++++++++++++++++++++++++--- powerdnsadmin/routes/api.py | 29 ++++++++++++++++++++- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/powerdnsadmin/__init__.py b/powerdnsadmin/__init__.py index 98690c2..c70b273 100755 --- a/powerdnsadmin/__init__.py +++ b/powerdnsadmin/__init__.py @@ -55,6 +55,8 @@ def create_app(config=None): csrf.exempt(routes.api.api_list_account_users) csrf.exempt(routes.api.api_add_account_user) csrf.exempt(routes.api.api_remove_account_user) + csrf.exempt(routes.api.api_zone_cryptokeys) + csrf.exempt(routes.api.api_zone_cryptokey) # Load config from env variables if using docker if os.path.exists(os.path.join(app.root_path, 'docker_config.py')): diff --git a/powerdnsadmin/decorators.py b/powerdnsadmin/decorators.py index 90c2f0d..e2a35bb 100644 --- a/powerdnsadmin/decorators.py +++ b/powerdnsadmin/decorators.py @@ -192,6 +192,24 @@ def is_json(f): return decorated_function +def callback_if_request_body_contains_key(callback, http_methods=[], keys=[]): + """ + If request body contains one or more of specified keys, call + :param callback + """ + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + check_current_http_method = not http_methods or request.method in http_methods + if (check_current_http_method and + set(request.get_json(force=True).keys()).intersection(set(keys)) + ): + callback(*args, **kwargs) + return f(*args, **kwargs) + return decorated_function + return decorator + + def api_role_can(action, roles=None, allow_self=False): """ Grant access if: @@ -304,15 +322,18 @@ def apikey_is_admin(f): def apikey_can_access_domain(f): + """ + Grant access if: + - user has Operator role or higher, or + - user has explicitly been granted access to domain + """ @wraps(f) def decorated_function(*args, **kwargs): - apikey = g.apikey if g.apikey.role.name not in ['Administrator', 'Operator']: - domains = apikey.domains zone_id = kwargs.get('zone_id').rstrip(".") - domain_names = [item.name for item in domains] + domain_names = [item.name for item in g.apikey.domains] - accounts = apikey.accounts + accounts = g.apikey.accounts accounts_domains = [domain.name for a in accounts for domain in a.domains] allowed_domains = set(domain_names + accounts_domains) @@ -324,6 +345,29 @@ def apikey_can_access_domain(f): return decorated_function +def apikey_can_configure_dnssec(http_methods=[]): + """ + Grant access if: + - user is in Operator role or higher, or + - dnssec_admins_only is off + """ + def decorator(f=None): + @wraps(f) + def decorated_function(*args, **kwargs): + check_current_http_method = not http_methods or request.method in http_methods + + if (check_current_http_method and + g.apikey.role.name not in ['Administrator', 'Operator'] and + Setting().get('dnssec_admins_only') + ): + msg = "ApiKey #{0} does not have enough privileges to configure dnssec" + current_app.logger.error(msg.format(g.apikey.id)) + raise DomainAccessForbidden(message=msg) + return f(*args, **kwargs) if f else None + return decorated_function + return decorator + + def apikey_auth(f): @wraps(f) def decorated_function(*args, **kwargs): diff --git a/powerdnsadmin/routes/api.py b/powerdnsadmin/routes/api.py index c96e7f4..4fce368 100644 --- a/powerdnsadmin/routes/api.py +++ b/powerdnsadmin/routes/api.py @@ -28,8 +28,9 @@ from ..lib.errors import ( 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_is_admin, apikey_can_access_domain, apikey_can_configure_dnssec, api_role_can, apikey_or_basic_auth, + callback_if_request_body_contains_key, ) import secrets import string @@ -1024,6 +1025,28 @@ def api_remove_account_user(account_id, user_id): return '', 204 +@api_bp.route( + '/servers//zones//cryptokeys', + methods=['GET', 'POST']) +@apikey_auth +@apikey_can_access_domain +@apikey_can_configure_dnssec(http_methods=['POST']) +def api_zone_cryptokeys(server_id, zone_id): + resp = helper.forward_request() + return resp.content, resp.status_code, resp.headers.items() + + +@api_bp.route( + '/servers//zones//cryptokeys/', + methods=['GET', 'PUT', 'DELETE']) +@apikey_auth +@apikey_can_access_domain +@apikey_can_configure_dnssec() +def api_zone_cryptokey(server_id, zone_id, cryptokey_id): + resp = helper.forward_request() + return resp.content, resp.status_code, resp.headers.items() + + @api_bp.route( '/servers//zones//', methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) @@ -1039,6 +1062,9 @@ def api_zone_subpath_forward(server_id, zone_id, subpath): @apikey_auth @apikey_can_access_domain @apikey_can_remove_domain(http_methods=['DELETE']) +@callback_if_request_body_contains_key(apikey_can_configure_dnssec()(), + http_methods=['PUT'], + keys=['dnssec', 'nsec3param']) def api_zone_forward(server_id, zone_id): resp = helper.forward_request() if not Setting().get('bg_domain_updates'): @@ -1072,6 +1098,7 @@ def api_zone_forward(server_id, zone_id): history.add() return resp.content, resp.status_code, resp.headers.items() + @api_bp.route('/servers/', methods=['GET', 'PUT']) @apikey_auth @apikey_is_admin