This is an automated email from the ASF dual-hosted git repository.
o-nikolas 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 4744db9eca9 Warn when AwsDuckDBHook ignores the AWS connection's
credentials (#76639)
4744db9eca9 is described below
commit 4744db9eca920580aa3b1f3b335c3bdf50e2dc2d
Author: Sean Ghaeli <[email protected]>
AuthorDate: Mon Nov 9 14:12:15 2026 -0700
Warn when AwsDuckDBHook ignores the AWS connection's credentials (#76639)
With the default credential_chain strategy, keys, a role or a profile in
the AWS connection are never used: DuckDB resolves credentials on its own.
That is easy to miss, since every other AWS hook uses them.
When credential_strategy is not set and the AWS connection has
credentials, log a warning that points at config (to use them) and
credential_chain (to silence it). The default and the secret are
unchanged.
---
providers/amazon/docs/operators/duckdb.rst | 4 ++
.../airflow/providers/amazon/aws/hooks/duckdb.py | 28 ++++++++++++++
.../tests/unit/amazon/aws/hooks/test_duckdb.py | 44 +++++++++++++++++++++-
3 files changed, 75 insertions(+), 1 deletion(-)
diff --git a/providers/amazon/docs/operators/duckdb.rst
b/providers/amazon/docs/operators/duckdb.rst
index 2afea6a11a1..d50ff94f6e9 100644
--- a/providers/amazon/docs/operators/duckdb.rst
+++ b/providers/amazon/docs/operators/duckdb.rst
@@ -168,6 +168,10 @@ DuckDB how to authenticate, and the
``credential_strategy`` parameter selects ho
hook_params={"aws_conn_id": "aws_static_keys", "credential_strategy":
"config"},
)
+If ``credential_strategy`` is not set and the AWS connection has credentials
(keys, a ``role_arn`` or
+a ``profile_name``), the hook logs a warning, because ``credential_chain``
does not use them. Set it to
+``config`` to use them, or to ``credential_chain`` to silence the warning.
+
Non-default S3 endpoints
========================
diff --git a/providers/amazon/src/airflow/providers/amazon/aws/hooks/duckdb.py
b/providers/amazon/src/airflow/providers/amazon/aws/hooks/duckdb.py
index d9c9e87296f..2c6f02b663b 100644
--- a/providers/amazon/src/airflow/providers/amazon/aws/hooks/duckdb.py
+++ b/providers/amazon/src/airflow/providers/amazon/aws/hooks/duckdb.py
@@ -73,6 +73,9 @@ class AwsDuckDBHook(DuckDBHook):
running inside DuckDB cannot see, or when talking to an S3-compatible
endpoint with its own
credentials. Prefer ``credential_chain`` where it works.
+ When ``credential_strategy`` is not set and the AWS connection has
credentials (keys, a
+ ``role_arn`` or a ``profile_name``), the hook warns that
``credential_chain`` does not use them.
+
Either strategy is better than none: DuckDB's ``httpfs`` extension has its
own HTTP client which
reads credentials from the standard ``AWS_*`` environment variables but
does **not** implement the
ECS container credential provider, so an ambient task role is invisible to
it and S3 access fails
@@ -145,6 +148,7 @@ class AwsDuckDBHook(DuckDBHook):
self._s3_endpoint_url = s3_endpoint_url
self._secret_name = secret_name
self._aws_identity_looked_up = False
+ self._ignored_credentials_checked = False
@property
def aws_conn_id(self) -> str | None:
@@ -210,6 +214,7 @@ class AwsDuckDBHook(DuckDBHook):
self.log.debug("credential_strategy is 'none'; not creating a
DuckDB S3 secret.")
return
+ self._warn_if_connection_credentials_are_ignored()
self._validate_identifier(self.secret_name, "secret")
parts = ["TYPE s3", *self._provider_clauses(strategy)]
@@ -241,6 +246,29 @@ class AwsDuckDBHook(DuckDBHook):
self._log_aws_identity(strategy, region, endpoint_url)
+ def _warn_if_connection_credentials_are_ignored(self) -> None:
+ """Warn once per hook when ``credential_chain`` leaves the AWS
connection's credentials unused."""
+ if self._ignored_credentials_checked:
+ return
+ self._ignored_credentials_checked = True
+ explicit = self._credential_strategy or
self.connection_extra.get("credential_strategy")
+ if explicit or self.aws_conn_id is None:
+ return
+ # profile_name counts: boto3 resolves it like every other AWS hook,
DuckDB's chain would not.
+ conn_config = self.aws_hook.conn_config
+ if (
+ conn_config.aws_access_key_id
+ or conn_config.aws_session_token
+ or conn_config.role_arn
+ or conn_config.profile_name
+ ):
+ self.log.warning(
+ "The AWS connection %r has credentials, but DuckDB is using
its own credential chain, so "
+ "they are not used. Set credential_strategy to 'config' to use
them, or to "
+ "'credential_chain' to silence this warning.",
+ self.aws_conn_id,
+ )
+
def _log_aws_identity(
self, strategy: CredentialStrategy, region: str | None, endpoint_url:
str | None
) -> None:
diff --git a/providers/amazon/tests/unit/amazon/aws/hooks/test_duckdb.py
b/providers/amazon/tests/unit/amazon/aws/hooks/test_duckdb.py
index c64aa0e1f62..7ffc9a4ba86 100644
--- a/providers/amazon/tests/unit/amazon/aws/hooks/test_duckdb.py
+++ b/providers/amazon/tests/unit/amazon/aws/hooks/test_duckdb.py
@@ -26,6 +26,7 @@ from botocore.exceptions import EndpointConnectionError
from airflow.models.connection import Connection
from airflow.providers.amazon.aws.hooks.duckdb import AwsDuckDBHook
+from airflow.providers.amazon.aws.utils.connection_wrapper import
AwsConnectionWrapper
from airflow.providers.duckdb.hooks.duckdb import DuckDBHook
from airflow.providers.duckdb.operators.duckdb import
DuckDBExecuteQueryOperator
@@ -34,6 +35,7 @@ AWS_BASE_HOOK =
"airflow.providers.amazon.aws.hooks.duckdb.AwsBaseHook"
STS_HOOK = "airflow.providers.amazon.aws.hooks.duckdb.StsHook"
ARN = "arn:aws:sts::123456789012:assumed-role/airflow-worker/session"
S3_VPC_ENDPOINT =
"https://bucket.vpce-1a2b3c4d-5e6f.s3.us-east-1.vpce.amazonaws.com"
+AWS_KEYS = {"login": "AKIA", "password": "s"}
try:
from airflow.sdk.exceptions import AirflowNotFoundException
@@ -47,7 +49,7 @@ def aws_base_hook():
with mock.patch(AWS_BASE_HOOK) as patched:
instance = patched.return_value
instance.region_name = "us-west-2"
- instance.conn_config.endpoint_url = None
+ instance.conn_config = AwsConnectionWrapper(conn=None)
credentials = instance.get_credentials.return_value
credentials.access_key = "AKIAEXAMPLE"
credentials.secret_key = "secret"
@@ -138,6 +140,36 @@ class TestAwsDuckDBHookCredentials:
assert "PROVIDER credential_chain" in statement
assert "KEY_ID" not in statement
+ @pytest.mark.parametrize(
+ ("aws_conn", "hook_kwargs", "warns"),
+ [
+ pytest.param(AWS_KEYS, {}, True, id="keys"),
+ pytest.param({"extra": '{"aws_session_token": "t"}'}, {}, True,
id="session-token"),
+ pytest.param({"extra": '{"role_arn": "arn:aws:iam::1:role/r"}'},
{}, True, id="role"),
+ pytest.param({"extra": '{"profile_name": "p"}'}, {}, True,
id="profile"),
+ pytest.param({"extra": '{"region_name": "eu-west-1"}'}, {}, False,
id="no-credentials"),
+ pytest.param(None, {}, False, id="missing-connection"),
+ pytest.param(AWS_KEYS, {"aws_conn_id": None}, False,
id="no-aws-conn"),
+ pytest.param(AWS_KEYS, {"credential_strategy":
"credential_chain"}, False, id="chain"),
+ pytest.param(AWS_KEYS, {"credential_strategy": "config"}, False,
id="config"),
+ ],
+ )
+ def test_warns_when_the_aws_connection_credentials_are_not_used(
+ self, aws_base_hook, aws_conn, hook_kwargs, warns
+ ):
+ conn = Connection(conn_id="aws_default", conn_type="aws", **aws_conn)
if aws_conn else None
+ aws_base_hook.conn_config = AwsConnectionWrapper(conn)
+ hook = AwsDuckDBHook(**hook_kwargs)
+ with mock.patch.object(AwsDuckDBHook, "log") as mock_log:
+ statement = secret_statement(hook)
+ secret_statement(hook) # As a second get_conn() would: the
warning is once per hook.
+ assert mock_log.warning.call_args_list == ([mock.call(mock.ANY,
"aws_default")] if warns else [])
+ if "credential_strategy" not in hook_kwargs: # The secret is the same
as before the warning.
+ assert statement == (
+ "CREATE OR REPLACE SECRET airflow_aws "
+ "(TYPE s3, PROVIDER credential_chain, REFRESH 'auto', REGION
'us-west-2');"
+ )
+
def test_credential_chain_does_not_resolve_credentials_itself(self,
aws_base_hook):
"""The point of credential_chain: nothing secret is materialised into
the SQL statement."""
secret_statement(AwsDuckDBHook())
@@ -263,6 +295,16 @@ class TestAwsDuckDBHookConnectionOverrides:
with mock.patch(GET_CONNECTION,
return_value=self.duckdb_connection(credential_strategy="none")):
assert secret_statement(AwsDuckDBHook()) == ""
+ def test_credential_strategy_on_the_connection_silences_the_warning(self,
aws_base_hook):
+ aws_base_hook.conn_config =
AwsConnectionWrapper(Connection(conn_type="aws", **AWS_KEYS))
+ connection =
self.duckdb_connection(credential_strategy="credential_chain")
+ with (
+ mock.patch(GET_CONNECTION, return_value=connection),
+ mock.patch.object(AwsDuckDBHook, "log") as log,
+ ):
+ secret_statement(AwsDuckDBHook())
+ log.warning.assert_not_called()
+
def test_region_can_be_set_on_the_connection(self, aws_base_hook):
with mock.patch(GET_CONNECTION,
return_value=self.duckdb_connection(region_name="ap-south-1")):
assert AwsDuckDBHook().get_region_name() == "ap-south-1"