"""OWS Request. As mentioned in the tech design (see Readme), this library provides all the tools to create a full request that is sent from one microservice to another one. Based on the request, it will find the corresponding service url, it will sign the request with an HMAC. This library also enable authentication of an incoming request. More details are available in the Readme. """ from collections import namedtuple from os import environ import time import uuid from warnings import warn import backoff from owsresponse import response from owsresponse import status import requests from owsrequest import __version__ as version from owsrequest import auth from owsrequest import hmac from owsrequest import model from owsrequest import service from owsrequest.constants import auth as auth_constants from owsrequest.constants import environment as env from owsrequest.constants import errors from owsrequest.constants import headers Authorization = namedtuple('Authorization', ['sender', 'recipient', 'hmac']) AUTHORIZATION_FORMAT = '{sender}/{recipient}:{authorization_hmac}' EXPIRES_AFTER = 10 # Expiration time in seconds. LAMBDA_EXPIRES_AFTER = 10 # Expiration time in seconds for lambdas. URL = '{protocol}://{domain}{path}' DEV_URL = '{domain}{path}' SERVICE_DEFAULT_PROTOCOL = environ.get('SERVICE_DEFAULT_PROTOCOL', 'https') FORCE_HTTPS = environ.get('FORCE_HTTPS', '') OWSREQUEST_MAX_RETRIES = int(environ.get('OWSREQUEST_MAX_RETRIES', '5')) RETRY_STATUSES = ( status.REQUEST_TIMEOUT, status.BAD_GATEWAY, status.SERVICE_UNAVAILABLE, status.GATEWAY_TIMEOUT) @backoff.on_predicate( wait_gen=backoff.expo, jitter=backoff.full_jitter, predicate=lambda r: r.status_code in RETRY_STATUSES, max_tries=OWSREQUEST_MAX_RETRIES, ) def process( application, environment, method, service_name, path, correlation_id=None, protocol=SERVICE_DEFAULT_PROTOCOL, uwsgi_cache_enabled=False, authorization_header=None, islegacy=False, isDaemon=False, isScheduler=False, **options): """Create a Request. This method uses python-requests to create and serve an HTTP(S) Request to one of our microservice. First, it resolves the service name to the corresponding domain, then it creates the HMAC for the request, and finally it sends the request. Args: application (str): name of the current application. environment (str): the current environment. method (str): the http method (POST, GET, PUT, HEAD, OPTIONS). service_name (str): the service which will receive the request. path (str): the path the service is going to call. correlation_id (optional, str): the correlation id. protocol (optional, str): the protocol for the request. options (dict): additional options to pass to the request. uwsgi_cache_enabled (bool): if True, allow caching in uwsgi. authorization_header (str): authorization header for the request. islegacy (bool): True for php legacy service. Returns: Response: the Requests response. """ if path: assert path.startswith('/'), 'The path must start with a /' (service_environment, service_domain) = service.discover( service_name, environment, islegacy=islegacy) if service_environment == 'dev': protocol = 'http' if FORCE_HTTPS: protocol = 'https' url = URL.format(domain=service_domain, path=path, protocol=protocol) url = clean_path_url(url) session = requests.Session() request = requests.Request(method, url=url, **options).prepare() correlation_id = correlation_id or str(uuid.uuid1()) # Update the header to be compliant with our security and logging / tracing # policies. request.headers.update({'Correlation-Id': correlation_id}) # Send the name of the service that is originating the request. request.headers.update({headers.ORCHARD_REQUESTOR_SERVICE: application}) # This enables any local/dev applications to indicate the request # is for a dev environment. When the request is being authorized # by a qa microservice, it will allow this request to go through. # However, if the request is sent to a prod microservice, it will # still go through the verification process. if environment != env.DEV_ENVIRONMENT: if authorization_header and uwsgi_cache_enabled and \ auth.is_jwt_enabled_service(service_name): authorization = authorization_header elif uwsgi_cache_enabled and auth.is_jwt_cached( application, service_name): cache_obj = auth.get_uwsgi_cache_object() authorization = auth_constants.AUTHORIZATION_FORMAT.format( jwt_token=cache_obj.get('jwt_token')) elif (isDaemon or isScheduler) and authorization_header: authorization = authorization_header else: authorization = create_authorization( application, service_environment, service_name, request, correlation_id) request.headers.update(Authorization=authorization) # add a User Agent header for WAF access if 'User-Agent' not in request.headers: request.headers.update({'user-agent': f'owsrequest/{version}'}) with session as session: return session.send(request) def create_authorization( application, environment, service_name, request, correlation_id): """Create an authorization for the current request. Args: application (str): name of the application. environment (str): the environment of the request. service_name (str): destination service. request (requests.Request): the preparred request. correlation_id (str): the correlation id. Returns: str: the authorization information. """ warn('HMAC Authorization is being deprecated', PendingDeprecationWarning, stacklevel=2) secret = hmac.create_secret() authorization_hmac = hmac.calculate( secret, request.method, request.body, correlation_id, clean_path_url(request.path_url)) model.save_authorization( environment=environment, hmac=authorization_hmac, sender=application, recipient=service_name, secret=secret) authorization = AUTHORIZATION_FORMAT.format( sender=application, recipient=service_name, authorization_hmac=authorization_hmac) return authorization def confirm_authorization( environment, authorization, correlation_id, request_method, request_body, request_path_url): """Confirm Authorization. Args: environment (str): the environment. authorization (str): the authorization header. correlation_id (str): the correlation id of the headers. request_method (str): the method of the request (GET, POST, PUT, HEAD) request_body (str): the full request of the body (to calculate the HMAC). request_path_url (str): the request path (full path, including the GET params). Returns: bool: if the request is confirmed or not. """ warn('HMAC Authorization is being deprecated', PendingDeprecationWarning, stacklevel=2) request_time = time.time() # The first part of the token is the issuer {sender}/{recipient}:{hmac} request_authorization = normalize_authorization(authorization) authorization = model.get_authorization(environment, authorization) # Verify the authorization is valid, it has not yet expired and the issuer # is matching the one in the request. if not authorization: return response.create_error_response( code=errors.UNAUTHORIZED_CODE, message=errors.UNAUTHORIZED_MESSAGE.get( errors.AUTH_NOT_FOUND), status=status.UNAUTHORIZED) current_time = time.time() created_at = authorization.get('created_at', 0) expiration_interval = EXPIRES_AFTER if authorization.get('sender').find('lambda') == 0: expiration_interval = LAMBDA_EXPIRES_AFTER expiration_time = created_at + expiration_interval if expiration_time < current_time: expired_by_in_unix_time = current_time - float(expiration_time) expiration_message = errors.UNAUTHORIZED_MESSAGE.get( errors.AUTH_EXPIRED).format( expired_by_in_unix_time=expired_by_in_unix_time, created_at=created_at, request_time=request_time, current_time=current_time) return response.create_error_response( code=errors.UNAUTHORIZED_CODE, message=expiration_message, status=status.REQUEST_TIMEOUT) if authorization.get('sender') != request_authorization.sender: return response.create_error_response( code=errors.UNAUTHORIZED_CODE, message=errors.UNAUTHORIZED_MESSAGE.get( errors.SENDER_MISMATCH), status=status.UNAUTHORIZED) # Calculate the HMAC to compare it against the request. request_hmac = hmac.calculate( authorization.get('secret'), request_method, request_body, correlation_id, clean_path_url(request_path_url)) if request_authorization.hmac == request_hmac: return response.Response() else: return response.create_error_response( code=errors.UNAUTHORIZED_CODE, message=errors.UNAUTHORIZED_MESSAGE[errors.HMAC_MISMATCH], status=status.UNAUTHORIZED) def normalize_authorization(string): """Normalize authorization from a string. Args: string (str): the string from the header. The format, as defined in the tech design is {sender}/{recipient}:{hmac} Returns: Authorization: the authorization object. """ warn('HMAC Authorization is being deprecated', PendingDeprecationWarning, stacklevel=2) sender, rest = string.split('/') recipient, hmac = rest.split(':') return Authorization(sender, recipient, hmac) def clean_path_url(url): """Clean the path url. Some frameworks (like flask) have a different behavior when it comes to urls. It adds automatically the `?` even if no params are provided. This method takes care of cleaning any trailing question amrk. Args: string (str): url to clean. """ while url.endswith('?'): url = url[:-1] return url