From 03ba5684d553256b7b6eae099fbab6939e34fbdb Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Fri, 5 Jun 2026 01:22:39 +0200 Subject: [PATCH] Redact trigger kwargs in the REST API response 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, so the field type and OpenAPI schema are unchanged. The triggerer still decrypts and uses the real kwargs at runtime; only the API representation is emptied. --- airflow-core/newsfragments/67868.bugfix.rst | 1 + .../core_api/datamodels/trigger.py | 17 +++++- .../core_api/datamodels/test_trigger.py | 53 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 airflow-core/newsfragments/67868.bugfix.rst create mode 100644 airflow-core/tests/unit/api_fastapi/core_api/datamodels/test_trigger.py diff --git a/airflow-core/newsfragments/67868.bugfix.rst b/airflow-core/newsfragments/67868.bugfix.rst new file mode 100644 index 0000000000000..e2e519014ad42 --- /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 the triggerer still decrypts and uses the real kwargs at runtime — only the API representation is emptied. 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 9b67ff9dc17e7..b24f48d7f9e82 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 @@ -24,6 +24,21 @@ from airflow.api_fastapi.core_api.base import BaseModel +def _redact_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,7 @@ class TriggerResponse(BaseModel): id: int classpath: str - kwargs: Annotated[str, BeforeValidator(str)] + kwargs: Annotated[str, BeforeValidator(_redact_kwargs)] created_date: datetime queue: str | None triggerer_id: int | None 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 0000000000000..f72742e0bdb49 --- /dev/null +++ b/airflow-core/tests/unit/api_fastapi/core_api/datamodels/test_trigger.py @@ -0,0 +1,53 @@ +# 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_redacted_to_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) + + assert response.kwargs == "{}" + # The schema must remain a string for backwards compatibility. + assert isinstance(response.kwargs, str)