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)