"""Request based helpers methods.""" from ddtrace import tracer from fastapi import HTTPException from fastapi.requests import Request from jwtauth.utils import jwt_auth_enabled_for_env from owsrequest.rules import EndpointRulesValidator from moneyhub.config import Config from moneyhub.connectors.ows_permissions import get_resources_for_profile from moneyhub.constants.constants import ACCOUNT_PATH_PARAM from moneyhub.constants.constants import DEFAULT_USER_TYPE from moneyhub.constants.constants import Environment from moneyhub.constants.constants import JWTKeys from moneyhub.constants.constants import LAMBDA_PROFILE_ID_PROD from moneyhub.constants.constants import LAMBDA_PROFILE_ID_QA from moneyhub.constants.constants import PAGINATION_TYPE_STANDARD from moneyhub.constants.constants import TOKEN from moneyhub.constants.constants import WILD_CARD from moneyhub.constants.error import ERROR_MESSAGE_FORBIDDEN from moneyhub.constants.error import FORBIDDEN_URL_ACCESS from moneyhub.constants.error import MISSING_JSON_BODY from moneyhub.constants.error import MISSING_TOKEN from moneyhub.utils.logger import get_logger from moneyhub.utils.request_context import g def extract_json_body(payload: object) -> dict: """Get json body from the request else throw an error if its empty. Arg: payload (object): Request body parameters. Returns: dict: Request body parameters. """ request_body = payload.model_dump(exclude_unset=True) if not request_body: raise HTTPException(status_code=400, detail=MISSING_JSON_BODY) return request_body @tracer.wrap('verify_rules_access function') def verify_rules_access( request: Request, validator: EndpointRulesValidator, log_only: bool = True, exclude_paths: list | None = None ): """Verify access to the endpoint with YML rules file. Args: request (request): flask request proxy validator (EndpointRulesValidator): rules validator object log_only (bool): only log access errors without blocking exclude_paths (list): list of paths to exclude from the access check """ if not jwt_auth_enabled_for_env(Config.ENVIRONMENT): return # Skip roles check for health endpoints if exclude_paths and request.url.path in exclude_paths: return # unpack JWT token token = g().token if token is None: raise HTTPException(status_code=403, detail=MISSING_TOKEN) profile_id = token[JWTKeys.PROFILE_ID] profile_type = token[JWTKeys.PROFILE_TYPE] profile_roles = token[JWTKeys.ROLES] # Check access rules for frontend->ows-grass and graphql-product requests. has_access = validator.has_access( path=request.url.path, method=request.method, profile=profile_type, roles=profile_roles ) if not has_access: if not log_only: raise HTTPException(status_code=403, detail=ERROR_MESSAGE_FORBIDDEN) # Check user's ownership of the account if ACCOUNT_PATH_PARAM in request.url.path: if log_only: return account_id = int((request.url.path.split('account/', )[1]).split('/')[0]) subaccount_id = ( int(request.query_params['subaccount_id']) if 'is_subaccount' in request.query_params and 'subaccount_id' in request.query_params else None ) if not profile_has_access_to_resource(profile_type, profile_id, account_id, subaccount_id): logger = get_logger(request) logger.info( f'profile: {profile_id}, does not have access to account: {account_id} (subaccount: {subaccount_id})') # noqa: Q003, E501 raise HTTPException(status_code=403, detail=FORBIDDEN_URL_ACCESS) def profile_has_access_to_resource( profile_type: str, profile_id: int, account_id: int, subaccount_id: int | None = None ) -> bool: """Check whether the profile has access to a given resource (account/subaccount). Args: profile_type (str): Type of the profile to check profile_id (int): ID of the profile to check account_id (int): ID of the account to check subaccount_id (int): (Optional) ID of the subaccount to check Returns: bool: Whether the profile has access """ resources = get_resources_for_profile(profile_type, profile_id) return any( (resource['type'] == 'Vendor' and (resource['vendorId'] == account_id or resource['vendorId'] == WILD_CARD)) # noqa: E501 or (subaccount_id and resource['type'] == 'Subaccount' and resource['id'] == subaccount_id) for resource in resources ) @tracer.wrap('extract token function') def extract_token(request: Request) -> dict | None: """Extract raw token from request and return either a user token or machine token dictionary. Args: request (Request): the request being processed Returns: dict: a compressed token dictionary. """ if TOKEN not in request.scope: return None token = request.scope[TOKEN] if JWTKeys.PROFILE_META in token: profiles: list = token[JWTKeys.PROFILE_META] profile = list(filter( lambda p: p[JWTKeys.PROFILE_TYPE] in ['AbacusProfile', 'MoneyhubProfile'], profiles )) profile_dict = profile[0] if profile else profiles[0] profile_id = profile_dict[JWTKeys.PROFILE_ID] profile_type = profile_dict[JWTKeys.PROFILE_TYPE] profile_roles = profile_dict[JWTKeys.ROLES] orchard_identity_id = token[JWTKeys.USER_META][JWTKeys.ORCHARD_IDENTITY_ID] else: profile_id = ( LAMBDA_PROFILE_ID_PROD if Config.ENVIRONMENT == Environment.PROD else LAMBDA_PROFILE_ID_QA ) profile_type = DEFAULT_USER_TYPE profile_roles = ['administrator'] orchard_identity_id = 'system_id' moneyhub_token = { JWTKeys.PROFILE_ID.value: profile_id, JWTKeys.PROFILE_TYPE.value: profile_type, JWTKeys.ROLES.value: profile_roles, JWTKeys.ORCHARD_IDENTITY_ID.value: orchard_identity_id } # Get the active span current_span = tracer.current_span() if current_span: # add token to span current_span.set_tag('request.token', moneyhub_token) return moneyhub_token def create_paginated_response(items: list, total_records: int) -> dict: """ Create a response containing paginated item. Args: items (list): paginated list of objects. total_records (int): total items. Returns: Response: object containing the items and pagination info """ message = { 'items': items, 'pagination': { 'pagination_type': PAGINATION_TYPE_STANDARD, 'total_records': total_records, }, } return message