From c89e2d2fff0e2287ddb67d92aed95e0536785d1b Mon Sep 17 00:00:00 2001 From: etvahala <36950815+etvahala@users.noreply.github.com> Date: Thu, 24 Oct 2024 00:57:34 +0300 Subject: [PATCH] Add API-key scope checking (#1837) Fixes # N/A The current documentation mentions that API-key security supports scopes: " The function should accept the following arguments: - apikey - required_scopes (optional) " However, the scopes were not passed to the checker. Changes proposed in this pull request: - Add missing parameter routing to ApiKeySecurityHandler - Add a unit test for API-key scopes --------- Co-authored-by: Robbe Sneyders --- connexion/security.py | 5 ++-- tests/decorators/test_security.py | 40 +++++++++++++++++++++++++++---- tests/test_operation2.py | 6 ++--- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/connexion/security.py b/connexion/security.py index e618f7ca9..2ad092a60 100644 --- a/connexion/security.py +++ b/connexion/security.py @@ -245,9 +245,10 @@ def get_fn(self, security_scheme, required_scopes): apikey_info_func, security_scheme["in"], security_scheme["name"], + required_scopes, ) - def _get_verify_func(self, api_key_info_func, loc, name): + def _get_verify_func(self, api_key_info_func, loc, name, required_scopes): check_api_key_func = self.check_api_key(api_key_info_func) def wrapper(request: ConnexionRequest): @@ -264,7 +265,7 @@ def wrapper(request: ConnexionRequest): if api_key is None: return NO_VALUE - return check_api_key_func(request, api_key) + return check_api_key_func(request, api_key, required_scopes=required_scopes) return wrapper diff --git a/tests/decorators/test_security.py b/tests/decorators/test_security.py index 2e1a99e7e..5ea9a6b8e 100644 --- a/tests/decorators/test_security.py +++ b/tests/decorators/test_security.py @@ -1,5 +1,5 @@ import json -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest import requests @@ -209,7 +209,7 @@ def apikey_info(apikey, required_scopes=None): security_handler_factory = ApiKeySecurityHandler() wrapped_func = security_handler_factory._get_verify_func( - apikey_info, "query", "auth" + apikey_info, "query", "auth", None ) request = ConnexionRequest(scope={"type": "http", "query_string": b"auth=foobar"}) @@ -225,7 +225,7 @@ def apikey_info(apikey, required_scopes=None): security_handler_factory = ApiKeySecurityHandler() wrapped_func = security_handler_factory._get_verify_func( - apikey_info, "header", "X-Auth" + apikey_info, "header", "X-Auth", None ) request = ConnexionRequest( @@ -235,6 +235,36 @@ def apikey_info(apikey, required_scopes=None): assert await wrapped_func(request) is not None +async def test_verify_apikey_scopes(): + def apikey_info(apikey, required_scopes=None): + if apikey == "admin foobar" and required_scopes == ["admin"]: + return {"sub": "foo"} + return None + + security_handler_factory = ApiKeySecurityHandler() + + scheme_apikey = { + "type": "apiKey", + "name": "x-auth", + "in": "header", + "scopes": {"admin"}, + } + + with patch.object( + security_handler_factory, + f"{security_handler_factory._resolve_func.__name__}", + return_value=apikey_info, + ) as mock_resolve_func: + wrapped_func = security_handler_factory.get_fn(scheme_apikey, ["admin"]) + mock_resolve_func.assert_called_once() + + request = ConnexionRequest( + scope={"type": "http", "headers": [[b"x-auth", b"admin foobar"]]} + ) + + assert await wrapped_func(request) == {"sub": "foo"} + + async def test_multiple_schemes(): def apikey1_info(apikey, required_scopes=None): if apikey == "foobar": @@ -249,10 +279,10 @@ def apikey2_info(apikey, required_scopes=None): security_handler_factory = SecurityHandlerFactory() apikey_security_handler_factory = ApiKeySecurityHandler() wrapped_func_key1 = apikey_security_handler_factory._get_verify_func( - apikey1_info, "header", "X-Auth-1" + apikey1_info, "header", "X-Auth-1", [] ) wrapped_func_key2 = apikey_security_handler_factory._get_verify_func( - apikey2_info, "header", "X-Auth-2" + apikey2_info, "header", "X-Auth-2", [] ) schemes = { "key1": wrapped_func_key1, diff --git a/tests/test_operation2.py b/tests/test_operation2.py index d5984ff71..c5086bca8 100644 --- a/tests/test_operation2.py +++ b/tests/test_operation2.py @@ -587,7 +587,7 @@ def test_no_token_info(): def test_multiple_security_schemes_and(): """Tests an operation with multiple security schemes in AND fashion.""" - def return_api_key_name(func, in_, name): + def return_api_key_name(func, in_, name, scopes): return name class MockApiKeyHandler(ApiKeySecurityHandler): @@ -610,8 +610,8 @@ class MockApiKeyHandler(ApiKeySecurityHandler): ) assert verify_api_key.call_count == 2 - verify_api_key.assert_any_call(math.ceil, "header", "X-Auth-1") - verify_api_key.assert_any_call(math.ceil, "header", "X-Auth-2") + verify_api_key.assert_any_call(math.ceil, "header", "X-Auth-1", []) + verify_api_key.assert_any_call(math.ceil, "header", "X-Auth-2", []) # Assert verify_multiple_schemes is called with mapping from scheme name # to result of security_handler_factory.verify_api_key() verify_multiple.assert_called_with({"key1": "X-Auth-1", "key2": "X-Auth-2"})