"""PDP authorization utilities.""" from functools import wraps from http import HTTPStatus from typing import Any, Callable from ddtrace import tracer from flask import g, request from jwtauth.exceptions import JWTAuthError from owsrequest import flask_request from owsresponse import response from owsresponse.adaptors.flask import flaskify from python_pdp_sdk.resource_getters import base from payment import config from payment.constants import error from payment.constants.error import ( ERROR_NO_VALID_IDENTITY_IN_CONTEXT, ERROR_NOT_PERMITTED_IDENTITY, ) @tracer.wrap() def authorize_resource( resource_id: int | str, resource_type: str, action: str = 'view', **kwargs: Any, ) -> bool: """Authorize a resource with empty attributes.""" authorized = config.authorization_backend.is_authorized( action=action, resource_id=resource_id, resource_type=resource_type, resource_getter=base.ForwardKwargsGetter(), **kwargs, ) if not authorized: g.log.warning( error.ERROR_CODE_AUTHORIZATION, resources={ 'identity_id': g.request_context.jwt_identity_id, 'resource_id': resource_id, 'resource_type': resource_type, 'auth_response': authorized, }, ) return False return True def check_access( func_or_resource_type: str | Callable, action: str | None = None, resource_id: str | int | None = None, resource_id_param: str | None = None, tenant_type: str | None = None, ) -> Callable: """Decorator to check access via standalone access rules or specified resource. Use cases: 1. Checks access rules using flask_request.verify_rules_access_standalone. Returns a 403 Unauthorized response if access is denied. 2. Uses authorize_resource to check if the user has access to the specified resource. Returns a 403 Unauthorized response if access is denied. Usage: @check_access def my_endpoint(): ... @check_access('resource_type', 'view', '0') def my_endpoint(): ... @check_access('resource_type', 'view', None, 'resource_id') def my_endpoint(resource_id): ... """ # bare decorator use case if callable(func_or_resource_type): func = func_or_resource_type @wraps(func) def wrapper(*args, **kwargs): access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) return func(*args, **kwargs) wrapper.__check_access__ = True return wrapper # configurable decorator use case def decorator(func: Callable) -> Callable: resource_type = func_or_resource_type @wraps(func) def wrapper(*args, **kwargs): access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: if resource_id is not None: check_resource_id = resource_id elif kwargs is not None and resource_id_param is not None: check_resource_id = kwargs.get(resource_id_param) else: check_resource_id = ( '0' # default ID for all or nothing PDP resource types ) if check_resource_id is None: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Resource ID not provided', status=403, ) ) extra_kwargs = {} if tenant_type: extra_kwargs['id_to_uuid_exchange_tenant'] = { 'tenant_type': tenant_type, 'tenant_id': check_resource_id, } authorized = authorize_resource( resource_id=check_resource_id, resource_type=resource_type, action=action, **extra_kwargs, ) if not authorized: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) return func(*args, **kwargs) wrapper.__check_access__ = True return wrapper return decorator def check_jwt_identity(identity_list: list[str]) -> Callable: """Decorator function which checks if the identity of the user making the request matches the expected identities for the endpoint. .. code-block:: python @app.route("/two") @check_jwt_identity([ "e7b019ea-1829-47f2-9642-cb4647065949", "f9435d76-221d-4570-ab2f-2f0e887c593f" ]) def index(): return "Ok!" :param identity_list: Identities, of user(s) allowed to access the endpoint """ def decorator(func: Callable) -> Callable: @wraps(func) def wrapper(*args, **kwargs): if config.Config.ENVIRONMENT == config.Config.DEV_ENVIRONMENT: g.log.warning( f'Disabled check_jwt_identity for development environment.' ) return func(*args, **kwargs) try: # we already have jwt_identity in context because it was already # authorized in before_request hook mechanism using m2m machinery jwt_identity = g.request_context.jwt_identity_id if not jwt_identity: raise JWTAuthError(ERROR_NO_VALID_IDENTITY_IN_CONTEXT) if jwt_identity not in identity_list: raise JWTAuthError( ERROR_NOT_PERMITTED_IDENTITY.format(jwt_identity=jwt_identity) ) except JWTAuthError as auth_error: return ( {'error': auth_error.message}, HTTPStatus.UNAUTHORIZED, ) return func(*args, **kwargs) wrapper.__check_access__ = True return wrapper return decorator