From dae9dedf211cf73a695bd553b30fc89cc0afe9a1 Mon Sep 17 00:00:00 2001 From: Francois Blackburn Date: Mon, 15 Jul 2024 15:43:40 -0400 Subject: [PATCH 1/2] validate and share tenant_uuid from verify_token with autodetect why: will remove a query to wazo-auth when Wazo-Tenant is present --- xivo/flask/auth_verifier.py | 7 +++++-- xivo/flask/headers.py | 4 ++++ xivo/tenant_flask_helpers.py | 5 +++++ xivo/tenant_helpers.py | 10 ++++++---- xivo/tests/test_tenant_flask_helpers.py | 18 ++++++++++++++++++ 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/xivo/flask/auth_verifier.py b/xivo/flask/auth_verifier.py index 92ca930..93d282f 100644 --- a/xivo/flask/auth_verifier.py +++ b/xivo/flask/auth_verifier.py @@ -10,7 +10,7 @@ from ..auth_verifier import AuthVerifierHelpers from ..http_exceptions import InvalidTokenAPIException, Unauthorized from ..tenant_flask_helpers import auth_client, token -from .headers import extract_token_id_from_header +from .headers import extract_tenant_id_from_header, extract_token_id_from_header R = TypeVar('R') @@ -33,7 +33,7 @@ def wrapper(*args: Any, **kwargs: Any) -> R | None: self.set_token_extractor(func) token_uuid = token.uuid required_acl = self.helpers.extract_required_acl(func, kwargs) - tenant_uuid = None # FIXME: Logic not implemented + tenant_uuid = extract_tenant_id_from_header() or None self.helpers.validate_token( auth_client, @@ -41,6 +41,9 @@ def wrapper(*args: Any, **kwargs: Any) -> R | None: required_acl, tenant_uuid, ) + + g.verified_tenant_uuid = tenant_uuid + return func(*args, **kwargs) return wrapper diff --git a/xivo/flask/headers.py b/xivo/flask/headers.py index 4149204..171084d 100644 --- a/xivo/flask/headers.py +++ b/xivo/flask/headers.py @@ -15,3 +15,7 @@ def extract_token_id_from_query_string() -> str: def extract_token_id_from_query_or_header() -> str: return extract_token_id_from_query_string() or extract_token_id_from_header() + + +def extract_tenant_id_from_header() -> str: + return request.headers.get('Wazo-Tenant', '') diff --git a/xivo/tenant_flask_helpers.py b/xivo/tenant_flask_helpers.py index 2800af6..97b8e51 100644 --- a/xivo/tenant_flask_helpers.py +++ b/xivo/tenant_flask_helpers.py @@ -78,6 +78,11 @@ def autodetect(cls: type[Self], include_query: bool = False) -> Self: else: logger.debug('Found tenant "%s" from header', tenant.uuid) + verified_tenant_uuid = g.get('verified_tenant_uuid') + if verified_tenant_uuid and tenant and verified_tenant_uuid == tenant.uuid: + logger.debug('Tenant already validated by Flask verify_token') + return cls(uuid=tenant.uuid) + if not tenant: tenant = cls.from_token(token) logger.debug('Found tenant "%s" from token', tenant.uuid) diff --git a/xivo/tenant_helpers.py b/xivo/tenant_helpers.py index 69e3e05..350b285 100644 --- a/xivo/tenant_helpers.py +++ b/xivo/tenant_helpers.py @@ -13,7 +13,10 @@ try: from flask import request - from .flask.headers import extract_token_id_from_header + from .flask.headers import ( + extract_tenant_id_from_header, + extract_token_id_from_header, + ) except ImportError: pass @@ -71,9 +74,8 @@ def from_query(cls: type[Self]) -> Self: @classmethod def from_headers(cls: type[Self]) -> Self: - try: - tenant_uuid = request.headers['Wazo-Tenant'] - except KeyError: + tenant_uuid = extract_tenant_id_from_header() + if not tenant_uuid: raise InvalidTenant() return cls(uuid=tenant_uuid) diff --git a/xivo/tests/test_tenant_flask_helpers.py b/xivo/tests/test_tenant_flask_helpers.py index 4f7d82c..ce7b916 100644 --- a/xivo/tests/test_tenant_flask_helpers.py +++ b/xivo/tests/test_tenant_flask_helpers.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch from unittest.mock import sentinel as s +from ..tenant_flask_helpers import Tenant from ..tenant_flask_helpers import auth_client as auth_client_proxy @@ -27,3 +28,20 @@ def test_config_deleted(self, auth_client): expected_config = {'host': s.host} auth_client.assert_called_once_with(**expected_config) + + +class TestTenant(unittest.TestCase): + @patch('xivo.tenant_flask_helpers.AuthClient') + def test_autodetect_when_verified_tenant_uuid(self, auth_client): + g_mock = Mock() + g_data = {'verified_tenant_uuid': s.tenant} + g_mock.get.side_effect = lambda x: g_data[x] + request_mock = Mock() + request_mock.headers.get.return_value = s.tenant + request_mock.args = [] + + with patch('xivo.tenant_flask_helpers.g', g_mock): + with patch('xivo.flask.headers.request', request_mock): + tenant = Tenant.autodetect() + + assert tenant.uuid == s.tenant From edcded74143e00885b13e188d0130b2dd8b5cb9e Mon Sep 17 00:00:00 2001 From: Francois Blackburn Date: Tue, 16 Jul 2024 12:23:24 -0400 Subject: [PATCH 2/2] fix tests to mock right path --- xivo/flask/tests/test_auth_verifier.py | 11 ++++++++--- xivo/tests/test_tenant_helpers.py | 14 +++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/xivo/flask/tests/test_auth_verifier.py b/xivo/flask/tests/test_auth_verifier.py index 2b19476..3272a58 100644 --- a/xivo/flask/tests/test_auth_verifier.py +++ b/xivo/flask/tests/test_auth_verifier.py @@ -25,15 +25,20 @@ def test_verify_token_decorator(self): 'token_extractor': None, } mock_g.get.side_effect = lambda x: g_data[x] + tenant_extractor = Mock(return_value=s.tenant) @auth_verifier.verify_token @required_acl('foo') def decorated(): return s.result - with patch('xivo.flask.auth_verifier.g', mock_g): - with patch('xivo.tenant_flask_helpers.g', mock_g): - result = decorated() + with patch( + 'xivo.flask.auth_verifier.extract_tenant_id_from_header', + tenant_extractor, + ): + with patch('xivo.flask.auth_verifier.g', mock_g): + with patch('xivo.tenant_flask_helpers.g', mock_g): + result = decorated() assert result == s.result diff --git a/xivo/tests/test_tenant_helpers.py b/xivo/tests/test_tenant_helpers.py index 43f4a2f..6a36952 100644 --- a/xivo/tests/test_tenant_helpers.py +++ b/xivo/tests/test_tenant_helpers.py @@ -22,7 +22,7 @@ class TestTenantAutodetect(TestCase): @patch('xivo.tenant_helpers.Token') - @patch('xivo.tenant_helpers.request', spec={}) + @patch('xivo.flask.headers.request', spec={}) def test_given_no_token_when_autodetect_then_raise(self, request, Token): auth = Mock() Token.from_headers = Mock() @@ -35,7 +35,7 @@ def test_given_no_token_when_autodetect_then_raise(self, request, Token): ) @patch('xivo.tenant_helpers.Token') - @patch('xivo.tenant_helpers.request', spec={}) + @patch('xivo.flask.headers.request', spec={}) def test_given_token_no_tenant_when_autodetect_then_return_tenant_from_token( self, request, Token ): @@ -51,7 +51,7 @@ def test_given_token_no_tenant_when_autodetect_then_return_tenant_from_token( assert_that(result.uuid, equal_to(tenant)) @patch('xivo.tenant_helpers.Token') - @patch('xivo.tenant_helpers.request', spec={}) + @patch('xivo.flask.headers.request', spec={}) def test_given_token_and_tenant_when_autodetect_then_return_given_tenant( self, request, Token ): @@ -67,7 +67,7 @@ def test_given_token_and_tenant_when_autodetect_then_return_given_tenant( assert_that(result.uuid, equal_to(tenant)) @patch('xivo.tenant_helpers.Token') - @patch('xivo.tenant_helpers.request', spec={}) + @patch('xivo.flask.headers.request', spec={}) def test_given_token_unknown_tenant_and_user_in_tenant_when_autodetect_then_return_tenant( self, request, Token ): @@ -85,7 +85,7 @@ def test_given_token_unknown_tenant_and_user_in_tenant_when_autodetect_then_retu assert_that(result.uuid, equal_to(tenant)) @patch('xivo.tenant_helpers.Token') - @patch('xivo.tenant_helpers.request', spec={}) + @patch('xivo.flask.headers.request', spec={}) def test_given_token_unknown_tenant_and_user_not_in_tenant_when_autodetect_then_raise( self, request, Token ): @@ -112,13 +112,13 @@ def test_given_visible_tenants_called_twice_with_same_tenant(self): class TestTenantFromHeaders(TestCase): - @patch('xivo.tenant_helpers.request', spec={}) + @patch('xivo.flask.headers.request', spec={}) def test_given_no_tenant_when_from_headers_then_raise(self, request): request.headers = {} assert_that(calling(Tenant.from_headers), raises(InvalidTenant)) - @patch('xivo.tenant_helpers.request', spec={}) + @patch('xivo.flask.headers.request', spec={}) def test_given_tenant_when_from_headers_then_return_tenant(self, request): tenant = 'tenant' request.headers = {'Wazo-Tenant': tenant}