This is an automated email from the ASF dual-hosted git repository.

shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 2e4a4b21e02 Invalidate cached Microsoft Graph request adapter on 403 
Forbidden (#73601)
2e4a4b21e02 is described below

commit 2e4a4b21e02ea7a833df9e1f8c0d1dbbb3859c33
Author: David Blain <[email protected]>
AuthorDate: Wed Sep 23 21:53:08 2026 +0200

    Invalidate cached Microsoft Graph request adapter on 403 Forbidden (#73601)
---
 .../providers/microsoft/azure/hooks/msgraph.py     |  4 ++-
 .../unit/microsoft/azure/hooks/test_msgraph.py     | 39 ++++++++++++++++++----
 2 files changed, 35 insertions(+), 8 deletions(-)

diff --git 
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/msgraph.py
 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/msgraph.py
index b9a74f0f97b..0fc2de4403c 100644
--- 
a/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/msgraph.py
+++ 
b/providers/microsoft/azure/src/airflow/providers/microsoft/azure/hooks/msgraph.py
@@ -189,7 +189,9 @@ class DefaultResponseHandler(ResponseHandler):
             status_code = HTTPStatus(resp.status_code)
             if status_code == HTTPStatus.BAD_REQUEST:
                 raise AirflowBadRequest(message)
-            if status_code == HTTPStatus.UNAUTHORIZED:
+            if status_code in {HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN}:
+                # Power BI reports an expired access token as 403 Forbidden 
with error code
+                # ``TokenExpired`` rather than 401, so both must evict the 
cached request adapter.
                 raise PermissionError(message)
             if status_code == HTTPStatus.NOT_FOUND:
                 raise AirflowNotFoundException(message)
diff --git 
a/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_msgraph.py 
b/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_msgraph.py
index 1d0fa23b697..8ddf8de6dea 100644
--- a/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_msgraph.py
+++ b/providers/microsoft/azure/tests/unit/microsoft/azure/hooks/test_msgraph.py
@@ -745,8 +745,20 @@ class TestKiotaRequestAdapterHook:
             credential.close.assert_awaited_once()
 
     @pytest.mark.asyncio
-    async def 
test_send_request_invalidates_cache_and_raises_on_unauthorized(self):
-        """send_request evicts the cached adapter, closes it, and re-raises 
when Microsoft Graph returns 401."""
+    @pytest.mark.parametrize(
+        "error",
+        [
+            pytest.param("401 Unauthorized", id="unauthorized"),
+            pytest.param("403 Forbidden: TokenExpired", 
id="forbidden-token-expired"),
+        ],
+    )
+    async def 
test_send_request_invalidates_cache_and_raises_on_permission_error(self, error: 
str):
+        """
+        send_request evicts the cached adapter, closes it, and re-raises when 
the API rejects the token.
+
+        Microsoft Graph reports an expired token as 401, Power BI as 403 with 
error code TokenExpired;
+        the response handler maps both to PermissionError.
+        """
         with patch_hook():
             hook = KiotaRequestAdapterHook(conn_id="msgraph_api")
 
@@ -754,15 +766,13 @@ class TestKiotaRequestAdapterHook:
             adapter._http_client = Mock(spec=AsyncClient, is_closed=False)
             adapter._authentication_provider = 
mock_authentication_provider(closed=False)
             adapter.base_url = "https://graph.microsoft.com/v1.0";
-            adapter.send_no_response_content_async = AsyncMock(
-                side_effect=PermissionError("401 Unauthorized")
-            )
-            hook.cached_request_adapters[hook.conn_id] = (hook.api_version, 
adapter)
+            adapter.send_no_response_content_async = 
AsyncMock(side_effect=PermissionError(error))
+            hook.cached_request_adapters[hook.conn_id] = ("v1.0", adapter)
 
             access_token_provider = 
adapter._authentication_provider.access_token_provider
             credential = access_token_provider._credentials._credential
 
-            with pytest.raises(PermissionError, match="401 Unauthorized"):
+            with pytest.raises(PermissionError, match=error):
                 await hook.run(url="users")
 
             adapter.send_no_response_content_async.assert_called_once()
@@ -952,6 +962,21 @@ class TestResponseHandler:
         with pytest.raises(PermissionError):
             
asyncio.run(DefaultResponseHandler().handle_response_async(response, None))
 
+    def test_handle_response_async_when_forbidden(self):
+        """Power BI reports an expired access token as 403 Forbidden with 
error code TokenExpired."""
+        body = {
+            "error": {
+                "code": "TokenExpired",
+                "message": "Access token has expired, resubmit with a new 
access token",
+            }
+        }
+        response = mock_json_response(403, body)
+
+        with pytest.raises(PermissionError) as exc_info:
+            
asyncio.run(DefaultResponseHandler().handle_response_async(response, None))
+
+        assert exc_info.value.args == (body,)
+
     def test_handle_response_async_when_not_found(self):
         response = mock_json_response(404, {})
 

Reply via email to