potiuk commented on code in PR #66575:
URL: https://github.com/apache/airflow/pull/66575#discussion_r3264957811


##########
task-sdk/src/airflow/sdk/execution_time/secrets/execution_api.py:
##########
@@ -43,6 +43,27 @@ def get_conn_value(self, conn_id: str, team_name: str | None 
= None) -> str | No
         """
         raise NotImplementedError("Use get_connection instead")
 
+    def _raise_if_authz_denied(self, msg, *, resource: str, key: str) -> None:
+        """
+        Raise PermissionError on an explicit deny response from the Execution 
API.
+
+        Returning None on a 401/403 would let the secrets-backend dispatcher
+        fall through to a less-restrictive backend (e.g. 
EnvironmentVariablesBackend
+        which performs no authorization checks). The Execution API explicitly
+        denied this request — we must not silently route around that decision.
+        Other ErrorResponse types (NOT_FOUND, transient API_SERVER_ERROR,
+        GENERIC_ERROR) keep the existing fallthrough behaviour so the
+        not-found-here path remains usable.
+        """
+        from airflow.sdk.exceptions import ErrorType
+        from airflow.sdk.execution_time.comms import ErrorResponse
+
+        if isinstance(msg, ErrorResponse) and msg.error == 
ErrorType.PERMISSION_DENIED:

Review Comment:
   Good catch — confirmed and fixed. Introduced 
`AirflowSecretsBackendAccessDenied(PermissionError)` in 
`airflow.sdk.exceptions` and threaded `except 
AirflowSecretsBackendAccessDenied: raise` ahead of the catch-all `except 
Exception:` in:
   
   - `task-sdk/src/airflow/sdk/execution_time/context.py` — `_get_connection`, 
`_async_get_connection`, `_get_variable`
   - `airflow-core/src/airflow/models/connection.py` — 
`get_connection_from_secrets`
   - `airflow-core/src/airflow/models/variable.py` — `get_variable_from_secrets`
   
   Went with a distinct subclass (rather than special-casing the bare 
`PermissionError`) so an incidental `OSError`-family `PermissionError` from 
inside an unrelated backend isn't mis-treated as an authoritative authz deny. 
Inheriting from `PermissionError` keeps any existing `except PermissionError:` 
caller working.
   
   Also added `TestDispatcherRefusesFallbackOnDeny` that pins the end-to-end 
behaviour — see reply to the test-coverage comment below.
   
   ---
   Drafted-by: Claude Code (Opus 4.7); reviewed by @potiuk before posting



##########
task-sdk/src/airflow/sdk/execution_time/secrets/execution_api.py:
##########
@@ -43,6 +43,27 @@ def get_conn_value(self, conn_id: str, team_name: str | None 
= None) -> str | No
         """
         raise NotImplementedError("Use get_connection instead")
 
+    def _raise_if_authz_denied(self, msg, *, resource: str, key: str) -> None:

Review Comment:
   Agreed — PR body updated. The default chain `[Env, ExecutionAPI]` has 
nothing after `ExecutionAPISecretsBackend` for the dispatcher to fall through 
to; the fall-through-leak scenario requires a non-default reordering or a 
custom backend explicitly placed after `ExecutionAPISecretsBackend`. Fix is 
still warranted because (a) some users do reorder, and (b) silently downgrading 
an authoritative deny to "next backend, please" is wrong on principle.
   
   ---
   Drafted-by: Claude Code (Opus 4.7); reviewed by @potiuk before posting



##########
task-sdk/tests/task_sdk/execution_time/test_secrets.py:
##########
@@ -120,6 +120,70 @@ def test_get_conn_value_not_implemented(self):
         with pytest.raises(NotImplementedError, match="Use get_connection 
instead"):
             backend.get_conn_value("test_conn")
 
+    def test_get_connection_raises_on_permission_denied(self, 
mock_supervisor_comms):

Review Comment:
   Added `TestDispatcherRefusesFallbackOnDeny` with three end-to-end tests 
covering `_get_connection`, `_get_variable`, and `_async_get_connection`. Each 
one inserts a spy backend AFTER `ExecutionAPISecretsBackend` in the chain and 
asserts both:
   
   1. `AirflowSecretsBackendAccessDenied` reaches the caller.
   2. The downstream spy backend is **never** called — `assert_not_called()`.
   
   These would have failed on the previous revision precisely because the outer 
`except Exception:` swallowed the deny.
   
   ---
   Drafted-by: Claude Code (Opus 4.7); reviewed by @potiuk before posting



##########
task-sdk/tests/task_sdk/execution_time/test_secrets.py:
##########
@@ -120,6 +120,70 @@ def test_get_conn_value_not_implemented(self):
         with pytest.raises(NotImplementedError, match="Use get_connection 
instead"):
             backend.get_conn_value("test_conn")
 
+    def test_get_connection_raises_on_permission_denied(self, 
mock_supervisor_comms):
+        """An explicit deny from the Execution API must raise, not fall 
through.
+
+        Returning None on a 401/403 would let the secrets-backend dispatcher
+        fall through to a less-restrictive backend (e.g. 
EnvironmentVariablesBackend).
+        """
+        from airflow.sdk.exceptions import ErrorType
+        from airflow.sdk.execution_time.comms import ErrorResponse

Review Comment:
   Hoisted `ErrorType` / `ErrorResponse` / `ConnectionResult` / 
`VariableResult` to the module top — applied to the older tests in the file as 
well.
   
   ---
   Drafted-by: Claude Code (Opus 4.7); reviewed by @potiuk before posting



##########
task-sdk/tests/task_sdk/execution_time/test_secrets.py:
##########
@@ -120,6 +120,70 @@ def test_get_conn_value_not_implemented(self):
         with pytest.raises(NotImplementedError, match="Use get_connection 
instead"):
             backend.get_conn_value("test_conn")
 
+    def test_get_connection_raises_on_permission_denied(self, 
mock_supervisor_comms):
+        """An explicit deny from the Execution API must raise, not fall 
through.
+
+        Returning None on a 401/403 would let the secrets-backend dispatcher
+        fall through to a less-restrictive backend (e.g. 
EnvironmentVariablesBackend).
+        """
+        from airflow.sdk.exceptions import ErrorType
+        from airflow.sdk.execution_time.comms import ErrorResponse

Review Comment:
   Done — all hoisted to module top.
   
   ---
   Drafted-by: Claude Code (Opus 4.7); reviewed by @potiuk before posting



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to