2020import logging
2121from typing import Annotated
2222
23- from fastapi import APIRouter , Depends , HTTPException , Path , Request , status
23+ from fastapi import APIRouter , Depends , HTTPException , Path , Query , Request , status
24+ from sqlalchemy import func , select
2425
26+ from airflow .api_fastapi .common .db .common import SessionDep
2527from airflow .api_fastapi .execution_api .datamodels .variable import (
28+ VariableKeysResponse ,
2629 VariablePostBody ,
2730 VariableResponse ,
2831)
@@ -54,17 +57,55 @@ async def has_variable_access(
5457 return True
5558
5659
57- router = APIRouter (
58- responses = {status .HTTP_404_NOT_FOUND : {"description" : "Variable not found" }},
59- dependencies = [Depends (has_variable_access )],
60- )
60+ router = APIRouter ()
6161
6262log = logging .getLogger (__name__ )
6363
6464
65+ # /keys must be declared before /{variable_key:path} so the static path is
66+ # matched first; otherwise the catch-all path param would swallow it.
67+ # has_variable_access is applied per-route below (not at router level) because
68+ # it requires a variable_key path parameter that /keys does not have.
69+ @router .get (
70+ "/keys" ,
71+ responses = {
72+ status .HTTP_401_UNAUTHORIZED : {"description" : "Unauthorized" },
73+ },
74+ )
75+ def get_variable_keys (
76+ session : SessionDep ,
77+ team_name : Annotated [str | None , Depends (get_team_name_dep )] = None ,
78+ prefix : Annotated [str | None , Query ()] = None ,
79+ limit : Annotated [int , Query (ge = 1 , le = 10_000 )] = 1000 ,
80+ offset : Annotated [int , Query (ge = 0 )] = 0 ,
81+ ) -> VariableKeysResponse :
82+ """
83+ Get Airflow Variable keys, optionally filtered by prefix.
84+
85+ .. note::
86+ This endpoint deliberately bypasses the per-variable ``has_variable_access``
87+ check, since access scoping requires a specific variable key. Any authenticated
88+ task within a team can therefore enumerate every variable key in that team —
89+ including keys for variables it would not be allowed to read. This is consistent
90+ with Airflow's security model (workers within a deployment trust each other),
91+ but the asymmetry between key enumeration and value access is intentional.
92+ """
93+ stmt = select (Variable .key ).order_by (Variable .key )
94+ if prefix is not None :
95+ stmt = stmt .where (Variable .key .startswith (prefix , autoescape = True ))
96+ if team_name is not None :
97+ stmt = stmt .where (Variable .team_name == team_name )
98+
99+ total_entries = session .scalar (select (func .count ()).select_from (stmt .subquery ())) or 0
100+ keys = session .scalars (stmt .offset (offset ).limit (limit )).all ()
101+ return VariableKeysResponse (keys = list (keys ), total_entries = total_entries )
102+
103+
65104@router .get (
66105 "/{variable_key:path}" ,
106+ dependencies = [Depends (has_variable_access )],
67107 responses = {
108+ status .HTTP_404_NOT_FOUND : {"description" : "Variable not found" },
68109 status .HTTP_401_UNAUTHORIZED : {"description" : "Unauthorized" },
69110 status .HTTP_403_FORBIDDEN : {"description" : "Task does not have access to the variable" },
70111 },
@@ -90,8 +131,10 @@ def get_variable(
90131
91132@router .put (
92133 "/{variable_key:path}" ,
134+ dependencies = [Depends (has_variable_access )],
93135 status_code = status .HTTP_201_CREATED ,
94136 responses = {
137+ status .HTTP_404_NOT_FOUND : {"description" : "Variable not found" },
95138 status .HTTP_401_UNAUTHORIZED : {"description" : "Unauthorized" },
96139 status .HTTP_403_FORBIDDEN : {"description" : "Task does not have access to the variable" },
97140 },
@@ -108,8 +151,10 @@ def put_variable(
108151
109152@router .delete (
110153 "/{variable_key:path}" ,
154+ dependencies = [Depends (has_variable_access )],
111155 status_code = status .HTTP_204_NO_CONTENT ,
112156 responses = {
157+ status .HTTP_404_NOT_FOUND : {"description" : "Variable not found" },
113158 status .HTTP_401_UNAUTHORIZED : {"description" : "Unauthorized" },
114159 status .HTTP_403_FORBIDDEN : {"description" : "Task does not have access to the variable" },
115160 },
0 commit comments