"""Test for the Request module.""" import time from unittest.mock import MagicMock from unittest.mock import NonCallableMagicMock from unittest.mock import patch import uuid import pytest import requests from owsrequest import __version__ as version from owsrequest import auth from owsrequest import hmac from owsrequest import model from owsrequest import request from owsrequest import service from owsrequest.config import logger from owsrequest.constants import errors from owsrequest.constants import headers from owsrequest.constants.environment import PROD_ENVIRONMENT from owsrequest.constants.environment import QA_ENVIRONMENT VALID_HMAC_PACKAGES = [ ('application/service_name:937d4ea4ab7f585e69d7ab62e907beec16343360', '2abe4052-1d19-11e6-b5e7-f45c89a67659', 'GET', '', '/path', 'wj4Yr8Y53vGW6hXLc9V2LELXTCeDN8'), ('application/service_name:937d4ea4ab7f585e69d7ab62e907beec16343360', '2abe4052-1d19-11e6-b5e7-f45c89a67659', 'GET', '', '/path?', 'wj4Yr8Y53vGW6hXLc9V2LELXTCeDN8'), ('lambda_app/service_name:937d4ea4ab7f585e69d7ab62e907beec16343360', '2abe4052-1d19-11e6-b5e7-f45c89a67659', 'GET', '', '/path?', 'wj4Yr8Y53vGW6hXLc9V2LELXTCeDN8')] successful_response = NonCallableMagicMock(status_code=200) non_timeout_error_response = NonCallableMagicMock(status_code=400) timeout_error_response = NonCallableMagicMock(status_code=408) @pytest.mark.no_owsrequest_patch @pytest.mark.parametrize(( 'environment', 'expected_authorization', 'receiver_service_name', 'jwt_cached', 'uwsgi_cache_enabled', 'is_jwt_forwarded', 'isDaemon', 'isScheduler' ), [ ( PROD_ENVIRONMENT, 'Bearer jwt_token', 'ows-jwt-service', False, False, False, True, False ), ( PROD_ENVIRONMENT, 'application/ows-jwt-service:0d8ee49a284cd0223684cf2c3784da88484a45f0', 'ows-jwt-service', False, False, False, True, False ), ( PROD_ENVIRONMENT, 'application/ows-jwt-service:0d8ee49a284cd0223684cf2c3784da88484a45f0', 'ows-jwt-service', False, False, False, False, True ), ( PROD_ENVIRONMENT, 'Bearer jwt_token', 'ows-jwt-service', True, True, True, False, False ), ( PROD_ENVIRONMENT, 'Bearer jwt_token', 'ows-jwt-service', True, True, True, False, False ), ( PROD_ENVIRONMENT, 'Bearer jwt_token', 'ows-jwt-service', False, True, True, False, False ), ( PROD_ENVIRONMENT, 'application/ows-non-jwt-service:' '0d8ee49a284cd0223684cf2c3784da88484a45f0', 'ows-non-jwt-service', False, True, False, False, False ), ( QA_ENVIRONMENT, 'application/ows-non-jwt-service:' '0d8ee49a284cd0223684cf2c3784da88484a45f0', 'ows-non-jwt-service', False, True, False, False, False ), ( 'dev', None, 'ows-any-service', False, True, False, False, False ), ( PROD_ENVIRONMENT, 'Bearer jwt_token', 'ows-jwt-service', True, True, False, False, False ), ( QA_ENVIRONMENT, 'Bearer jwt_token', 'ows-jwt-service', True, True, False, False, False ), ( PROD_ENVIRONMENT, 'application/ows-jwt-service:0d8ee49a284cd0223684cf2c3784da88484a45f0', 'ows-jwt-service', False, True, False, False, False ), ( QA_ENVIRONMENT, 'application/ows-jwt-service:0d8ee49a284cd0223684cf2c3784da88484a45f0', 'ows-jwt-service', False, True, False, False, False ), ( PROD_ENVIRONMENT, 'application/ows-non-jwt-service:' '0d8ee49a284cd0223684cf2c3784da88484a45f0', 'ows-non-jwt-service', False, False, False, False, False ), ( QA_ENVIRONMENT, 'application/ows-non-jwt-service:' '0d8ee49a284cd0223684cf2c3784da88484a45f0', 'ows-non-jwt-service', False, False, False, False, False ), ]) @pytest.mark.parametrize('path', ['/path']) @pytest.mark.parametrize('protocol', ['https']) @pytest.mark.parametrize(( 'confirm_authorization_responses', 'expected_process_response', 'send_call_count', ), [ ( [successful_response], successful_response, 1, ), ( [non_timeout_error_response], non_timeout_error_response, 1, ), ( [timeout_error_response, successful_response], successful_response, 2, ), ( [*[timeout_error_response] * 2, successful_response], successful_response, 3, ), ( [*[timeout_error_response] * 3, successful_response], successful_response, 4, ), ( [*[timeout_error_response] * 4, successful_response], successful_response, 5, ), ]) @pytest.mark.parametrize('client_header', [{'user-agent': 'supplied'}, {}, {}, {}, {}, {}]) def test_processing_request( monkeypatch, path, environment, protocol, confirm_authorization_responses, expected_process_response, send_call_count, expected_authorization, receiver_service_name, jwt_cached, uwsgi_cache_enabled, is_jwt_forwarded, isDaemon, isScheduler, client_header ): """Test processing the request. Please note: the secret is hardcoded along with the correlation id. Those are the two pieces of data that will constantly for the same request. """ service_name = receiver_service_name sender_service_name = 'application' correlation_id = '97dd5280-1c3b-11e6-a000-f45c89a67659' expected_service_url = get_expected_service_url(environment, service_name) # dev environment should only use http expected_url = request.URL.format( domain=expected_service_url, path=path, protocol=protocol) if uwsgi_cache_enabled: cache_mock = MagicMock() if receiver_service_name == 'ows-non-jwt-service': cache_mock.has = MagicMock(return_value=False) else: i = send_call_count jwt_token = [[receiver_service_name]] jwt_check = [True, False] monkeypatch.setattr(logger, 'info', MagicMock()) if is_jwt_forwarded: jwt_check = [True] jwt_token = [[receiver_service_name]] elif jwt_cached: jwt_check = [True, True] jwt_token = [[receiver_service_name], 'jwt_token', 'jwt_token'] for count in range(i): jwt_check.extend(jwt_check) jwt_token.extend(jwt_token) cache_mock.has.side_effect = jwt_check cache_mock.get.side_effect = jwt_token monkeypatch.setattr(time, 'sleep', MagicMock(spec=time.sleep)) monkeypatch.setattr( requests, 'Session', MagicMock(spec=requests.Session), ) (requests .Session .return_value .__enter__ .return_value .send .side_effect) = confirm_authorization_responses monkeypatch.setattr( hmac, 'create_secret', lambda: '6gwQGjBoHpIybSl5zQQPsVpn0GR1hr') monkeypatch.setattr( model, 'save_authorization', MagicMock(spec=model.save_authorization)) if uwsgi_cache_enabled: monkeypatch.setattr( auth, 'get_uwsgi_cache_object', MagicMock(return_value=cache_mock)) authorization_header = None if isDaemon: authorization_header = expected_authorization if is_jwt_forwarded: authorization_header = expected_authorization if isScheduler: authorization_header = expected_authorization process_response = request.process( sender_service_name, environment, 'GET', service_name, path, correlation_id=correlation_id, protocol=protocol, uwsgi_cache_enabled=uwsgi_cache_enabled, authorization_header=authorization_header, isDaemon=isDaemon, isScheduler=isScheduler) # Send should have been called one time. assert ( requests .Session .return_value .__enter__ .return_value .send .call_count) == send_call_count # Verifying the information in the prepared request prepared_request = ( requests .Session .return_value .__enter__ .return_value .send .call_args[0][0]) assert prepared_request.headers.get('Authorization') == ( expected_authorization) assert prepared_request.headers.get('Correlation-Id') == correlation_id assert prepared_request.headers.get(headers.ORCHARD_REQUESTOR_SERVICE) == ( sender_service_name) assert prepared_request.headers.get('User-Agent') == f'owsrequest/{version}' assert prepared_request.url == request.clean_path_url(expected_url) assert expected_process_response == process_response if not jwt_cached and service_name == 'ows-jwt-service' \ and not is_jwt_forwarded and isDaemon is False and isScheduler is False: assert logger.info.called def get_expected_service_url(environment, service_name): """Return expected service_url for environment.""" if environment != 'dev': return service.SERVICE_URL_SPEC.format( environment=environment if environment in (QA_ENVIRONMENT, PROD_ENVIRONMENT) else 'dev', service_name=service_name, domain=service.SERVICE_DEFAULT_DOMAIN) else: return service.SERVICE_URL_SPEC.format( environment=QA_ENVIRONMENT, service_name=service_name, domain=service.SERVICE_DEFAULT_DOMAIN) @pytest.mark.parametrize(( 'receiver_service_name', 'jwt_cached', 'uwsgi_cache_enabled' ), [ ( 'ows-non-jwt-service', False, True ), ( 'ows-jwt-service', True, True ), ( 'ows-jwt-service', False, True ), ( 'ows-jwt-service', False, False ) ]) @patch('requests.Session.send') @pytest.mark.no_owsrequest_patch def test_processing_request_hmac_with_different_methods( send, monkeypatch, receiver_service_name, jwt_cached, uwsgi_cache_enabled): """HMAC must always change on a different method.""" monkeypatch.setattr( model, 'save_authorization', MagicMock(spec=model.save_authorization)) correlation_id = '97dd5280-1c3b-11e6-a000-f45c89a67659' authorizations = set() cache_mock = MagicMock() for method in ['get', 'post', 'put', 'head', 'options', 'delete']: if uwsgi_cache_enabled: if receiver_service_name == 'ows-non-jwt-service': cache_mock.has = MagicMock(return_value=False) else: jwt_token = [[receiver_service_name]] jwt_check = [True, False] if jwt_cached: jwt_check = [True, True] jwt_token = [ [receiver_service_name], 'jwt_token', 'jwt_token'] cache_mock.has.side_effect = jwt_check cache_mock.get.side_effect = jwt_token monkeypatch.setattr( auth, 'get_uwsgi_cache_object', MagicMock(return_value=cache_mock)) request.process( 'application', PROD_ENVIRONMENT, 'GET', receiver_service_name, '/path', correlation_id=correlation_id, uwsgi_cache_enabled=uwsgi_cache_enabled) authorization = send.call_args[0][0].headers.get('Authorization') if jwt_cached and len(authorizations) > 0: assert authorization in authorizations else: assert authorization not in authorizations authorizations.add(authorization) @pytest.mark.parametrize(( 'receiver_service_name', 'jwt_cached', 'uwsgi_cache_enabled' ), [ ( 'ows-non-jwt-service', False, True ), ( 'ows-jwt-service', True, True ), ( 'ows-jwt-service', False, True ), ( 'ows-jwt-service', False, False ) ]) @patch('requests.Session.send') @pytest.mark.no_owsrequest_patch def test_processing_request_hmac_with_different_cid( send, monkeypatch, receiver_service_name, jwt_cached, uwsgi_cache_enabled): """HMAC must always change on a different correlation id.""" monkeypatch.setattr( model, 'save_authorization', MagicMock(spec=model.save_authorization)) authorizations = set() cache_mock = MagicMock() for i in range(10): if uwsgi_cache_enabled: if receiver_service_name == 'ows-non-jwt-service': cache_mock.has = MagicMock(return_value=False) else: jwt_token = [[receiver_service_name]] jwt_check = [True, False] if jwt_cached: jwt_check = [True, True] jwt_token = [ [receiver_service_name], 'jwt_token', 'jwt_token'] cache_mock.has.side_effect = jwt_check cache_mock.get.side_effect = jwt_token monkeypatch.setattr( auth, 'get_uwsgi_cache_object', MagicMock(return_value=cache_mock)) request.process('application', PROD_ENVIRONMENT, 'GET', receiver_service_name, '/path', uwsgi_cache_enabled=uwsgi_cache_enabled) authorization = send.call_args[0][0].headers.get('Authorization') if jwt_cached and len(authorizations) > 0: assert authorization in authorizations else: assert authorization not in authorizations authorizations.add(authorization) @pytest.mark.no_owsrequest_patch def test_processing_request_with_wrong_path(): """Path should always start with a /. Not providing the path will throw an exception. The exception is intended as this will only happen in dev environment. """ with pytest.raises(AssertionError): request.process('application', PROD_ENVIRONMENT, 'GET', 'service_name', 'path') def test_create_authorization(monkeypatch): """Test creating an authorization.""" monkeypatch.setattr( model, 'save_authorization', MagicMock(spec=model.save_authorization)) application = 'application' service_name = 'service_name' environment = QA_ENVIRONMENT current_request = requests.Request( 'GET', url='http://domain/path', data='data').prepare() current_hmac = request.create_authorization( application, environment, service_name, current_request, str(uuid.uuid1())) assert current_hmac.startswith('{}/{}:'.format(application, service_name)) args = model.save_authorization.call_args[1] assert args.get('environment') == environment assert args.get('sender') == application assert args.get('recipient') == service_name assert args.get('hmac') assert len(args.get('secret')) == hmac.SECRET_SIZE @pytest.mark.parametrize('hmac_package', VALID_HMAC_PACKAGES) def test_confirm_authorization(monkeypatch, hmac_package): """Test confirm an authorization.""" (authorization, correlation_id, request_method, request_body, request_path_url, secret) = hmac_package monkeypatch.setattr( model, 'get_authorization', MagicMock( return_value={ 'authorization': authorization.split('/')[-1], 'sender': authorization.split('/')[0], 'created_at': time.time(), 'secret': secret }, spec=model.get_authorization)) assert request.confirm_authorization( QA_ENVIRONMENT, authorization, correlation_id, request_method, request_body, request_path_url) @pytest.mark.parametrize('hmac_package', VALID_HMAC_PACKAGES) def test_confirm_authorization_on_empty_dynamodb(monkeypatch, hmac_package): """Test confirm an authorization on empty fetch from dynamodb.""" (authorization, correlation_id, request_method, request_body, request_path_url, secret) = hmac_package monkeypatch.setattr( model, 'get_authorization', MagicMock( return_value={}, spec=model.get_authorization)) # expects to fail confirm_response = request.confirm_authorization( QA_ENVIRONMENT, authorization, correlation_id, request_method, request_body, request_path_url) assert not confirm_response assert confirm_response.status == 401 assert confirm_response.errors.get( 'code') == errors.UNAUTHORIZED_CODE assert confirm_response.errors.get( 'message') == errors.UNAUTHORIZED_MESSAGE.get( errors.AUTH_NOT_FOUND) @pytest.mark.parametrize('hmac_package', VALID_HMAC_PACKAGES) def test_confirm_authorization_on_expired_token(monkeypatch, hmac_package): """Test confirming authorization on expired.""" (authorization, correlation_id, request_method, request_body, request_path_url, secret) = hmac_package current_time = time.time() monkeypatch.setattr(time, 'time', MagicMock( return_value=current_time)) expiration_interval = request.EXPIRES_AFTER if authorization.split('/')[0].find('lambda') == 0: expiration_interval = request.LAMBDA_EXPIRES_AFTER created_at = current_time - expiration_interval - 1 expiration_time = created_at + expiration_interval expired_by_in_unix_time = current_time - expiration_time monkeypatch.setattr( model, 'get_authorization', MagicMock( return_value={ 'authorization': authorization.split('/')[-1], 'sender': authorization.split('/')[0], 'created_at': created_at, 'secret': secret }, spec=model.get_authorization)) # This is a valid request, just the token has expired. confirm_response = request.confirm_authorization( QA_ENVIRONMENT, authorization, correlation_id, request_method, request_body, request_path_url) assert not confirm_response assert confirm_response.status == 408 assert confirm_response.errors.get('code') == errors.UNAUTHORIZED_CODE assert confirm_response.errors.get( 'message') == errors.UNAUTHORIZED_MESSAGE.get( errors.AUTH_EXPIRED).format( created_at=created_at, current_time=current_time, request_time=current_time, expired_by_in_unix_time=expired_by_in_unix_time) @pytest.mark.parametrize('hmac_package', VALID_HMAC_PACKAGES) def test_confirm_authorization_on_different_sender(monkeypatch, hmac_package): """Test confirming authorization on different sender.""" (authorization, correlation_id, request_method, request_body, request_path_url, secret) = hmac_package monkeypatch.setattr( model, 'get_authorization', MagicMock( return_value={ 'authorization': authorization.split('/')[-1], 'sender': 'different_sender', 'created_at': time.time(), 'secret': secret }, spec=model.get_authorization)) # This is a valid request, just the token has expired. confirm_response = request.confirm_authorization( QA_ENVIRONMENT, authorization, correlation_id, request_method, request_body, request_path_url) assert not confirm_response assert confirm_response.status == 401 assert confirm_response.errors.get('code') == errors.UNAUTHORIZED_CODE assert confirm_response.errors.get( 'message') == errors.UNAUTHORIZED_MESSAGE.get( errors.SENDER_MISMATCH) @pytest.mark.parametrize('hmac_package', VALID_HMAC_PACKAGES) def test_confirm_authorization_on_different_hmac(monkeypatch, hmac_package): """Test confirming authorization on different hmac.""" (authorization, correlation_id, request_method, request_body, request_path_url, secret) = hmac_package monkeypatch.setattr(hmac, 'calculate', MagicMock(return_value='HMAC')) monkeypatch.setattr( model, 'get_authorization', MagicMock( return_value={ 'authorization': authorization.split('/')[-1], 'sender': authorization.split('/')[0], 'created_at': time.time(), 'secret': secret }, spec=model.get_authorization)) # This is a valid request, just the token has expired. confirm_response = request.confirm_authorization( QA_ENVIRONMENT, authorization, correlation_id, request_method, request_body, request_path_url) assert not confirm_response assert confirm_response.errors.get('code') == errors.UNAUTHORIZED_CODE assert confirm_response.errors.get( 'message') == errors.UNAUTHORIZED_MESSAGE.get( errors.HMAC_MISMATCH) def test_normalize_authorization(): """Test normalizing an authorizatio.""" result = request.normalize_authorization('sender/recipient:hmac') assert result.hmac == 'hmac' assert result.recipient == 'recipient' assert result.sender == 'sender' def test_clean_path_url(): """Test cleaning the url path.""" assert request.clean_path_url('?') == '' assert request.clean_path_url('??') == '' assert request.clean_path_url('/path/') == '/path/'