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

potiuk 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 9101aa66754 Stop exposing trigger kwargs in the REST API response 
(#67868)
9101aa66754 is described below

commit 9101aa66754af052ba5010a5fce9f8fa4140e6c7
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sun Jun 14 04:10:25 2026 +0200

    Stop exposing trigger kwargs in the REST API response (#67868)
    
    TriggerResponse.kwargs returned the decrypted trigger keyword arguments
    verbatim (as a stringified Python dict). Those kwargs can contain
    credentials a deferred operator hands to its trigger (an API key, a token),
    so the field leaked secrets.
    
    Rather than removing the field (a breaking schema change for API consumers),
    keep it but always return it empty as "{}" -- the same value an empty-kwargs
    trigger already produced under the previous str() serialization. The field
    is now marked `deprecated` in the response schema so consumers are nudged
    off it, while retained for backwards compatibility. The triggerer still
    decrypts and uses the real kwargs at runtime; only the API representation
    is emptied.
    
    A plain `Field(deprecated=True)` is used rather than a deprecated computed
    field, so response serialization stays warning-free.
---
 airflow-core/newsfragments/67868.bugfix.rst        |  1 +
 .../api_fastapi/core_api/datamodels/trigger.py     | 22 ++++++++-
 .../api_fastapi/core_api/openapi/_private_ui.yaml  |  1 +
 .../core_api/openapi/v2-rest-api-generated.yaml    |  1 +
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts |  3 +-
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  3 ++
 .../core_api/datamodels/test_trigger.py            | 56 ++++++++++++++++++++++
 7 files changed, 84 insertions(+), 3 deletions(-)

diff --git a/airflow-core/newsfragments/67868.bugfix.rst 
b/airflow-core/newsfragments/67868.bugfix.rst
new file mode 100644
index 00000000000..55535ed4b4f
--- /dev/null
+++ b/airflow-core/newsfragments/67868.bugfix.rst
@@ -0,0 +1 @@
+The ``kwargs`` field of trigger objects returned by the REST API (for example 
in the ``trigger`` of a task-instance response) no longer exposes the decrypted 
trigger keyword arguments. Those kwargs can contain credentials a deferred 
operator hands to its trigger (an API key, a token, …), so the field is now 
always returned empty, as ``"{}"``. The field is retained in the response 
schema for backwards compatibility — and is now marked ``deprecated`` there so 
consumers are nudged off it —  [...]
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/trigger.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/trigger.py
index 9b67ff9dc17..1fbaef6ac92 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/trigger.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/trigger.py
@@ -19,11 +19,26 @@ from __future__ import annotations
 from datetime import datetime
 from typing import Annotated
 
-from pydantic import BeforeValidator, ConfigDict
+from pydantic import BeforeValidator, ConfigDict, Field
 
 from airflow.api_fastapi.core_api.base import BaseModel
 
 
+def _remove_kwargs(_: object) -> str:
+    """
+    Return empty trigger kwargs for API responses.
+
+    Trigger ``kwargs`` may contain sensitive values (for example credentials a 
deferred
+    operator hands to its trigger -- an API key, a token), so they are never 
exposed through
+    the REST API. The field is kept in the response schema for backwards 
compatibility -- so
+    existing API consumers do not break on a missing property -- but it is 
always returned
+    empty, as ``"{}"`` (the stringified empty dict, matching the string format 
the field has
+    always used). The triggerer still decrypts and uses the real kwargs at 
runtime; only the
+    API representation is emptied.
+    """
+    return "{}"
+
+
 class TriggerResponse(BaseModel):
     """Trigger serializer for responses."""
 
@@ -31,7 +46,10 @@ class TriggerResponse(BaseModel):
 
     id: int
     classpath: str
-    kwargs: Annotated[str, BeforeValidator(str)]
+    # Deprecated: always emptied (see ``_remove_kwargs``). Marked deprecated 
in the schema so
+    # consumers are nudged off it. ``deprecated`` only warns on direct 
attribute access, not
+    # during model serialization, so our own response rendering stays 
warning-free.
+    kwargs: Annotated[str, BeforeValidator(_remove_kwargs), 
Field(deprecated=True)]
     created_date: datetime
     queue: str | None
     triggerer_id: int | None
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index ee7d0beb815..e110e9b2510 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -3984,6 +3984,7 @@ components:
         kwargs:
           type: string
           title: Kwargs
+          deprecated: true
         created_date:
           type: string
           format: date-time
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index 5e28d7be8ce..6d6dcb687ad 100644
--- 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++ 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -16500,6 +16500,7 @@ components:
         kwargs:
           type: string
           title: Kwargs
+          deprecated: true
         created_date:
           type: string
           format: date-time
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 8db0fd81208..608d2d3eef5 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -7651,7 +7651,8 @@ export const $TriggerResponse = {
         },
         kwargs: {
             type: 'string',
-            title: 'Kwargs'
+            title: 'Kwargs',
+            deprecated: true
         },
         created_date: {
             type: 'string',
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index fd34703d468..0c05afccbfe 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -1887,6 +1887,9 @@ export type TriggerDAGRunPostBody = {
 export type TriggerResponse = {
     id: number;
     classpath: string;
+    /**
+     * @deprecated
+     */
     kwargs: string;
     created_date: string;
     queue: string | null;
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/datamodels/test_trigger.py 
b/airflow-core/tests/unit/api_fastapi/core_api/datamodels/test_trigger.py
new file mode 100644
index 00000000000..62cfc35df7f
--- /dev/null
+++ b/airflow-core/tests/unit/api_fastapi/core_api/datamodels/test_trigger.py
@@ -0,0 +1,56 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from datetime import datetime
+
+import pytest
+
+from airflow.api_fastapi.core_api.datamodels.trigger import TriggerResponse
+
+
+class _Trigger:
+    """Stand-in for the ``Trigger`` ORM object a ``TriggerResponse`` is built 
from."""
+
+    id = 1
+    classpath = "airflow.providers.standard.triggers.temporal.DateTimeTrigger"
+    created_date = datetime(2024, 1, 1)
+    queue = None
+    triggerer_id = None
+
+    def __init__(self, kwargs):
+        self.kwargs = kwargs
+
+
+class TestTriggerResponse:
+    @pytest.mark.parametrize(
+        "kwargs",
+        [
+            pytest.param({"api_key": "super-secret", "polling_interval": 30}, 
id="sensitive-values"),
+            pytest.param({}, id="already-empty"),
+        ],
+    )
+    def test_kwargs_are_always_empty(self, kwargs):
+        """Trigger kwargs may hold credentials, so the API always returns them 
empty as ``"{}"``."""
+        response = TriggerResponse.model_validate(_Trigger(kwargs), 
from_attributes=True)
+
+        # Read through the serialized output (the API representation) rather 
than the attribute,
+        # which would emit the field's deprecation warning.
+        dumped = response.model_dump()
+        assert dumped["kwargs"] == "{}"
+        # The schema must remain a string for backwards compatibility.
+        assert isinstance(dumped["kwargs"], str)

Reply via email to