potiuk commented on code in PR #72355:
URL: https://github.com/apache/airflow/pull/72355#discussion_r4015125842


##########
shared/configuration/src/airflow_shared/configuration/parser.py:
##########
@@ -1602,16 +1602,35 @@ def getjson(
         self, section: str, key: str, fallback=None, **kwargs
     ) -> dict | list | str | int | float | None:
         """
-        Return a config value parsed from a JSON string.
+        Return a config value parsed from a JSON string or JSON file.
 
-        ``fallback`` is *not* JSON parsed but used verbatim when no config 
value is given.
+        If no direct config value is given for ``key``, attempts to read from 
a file path
+        specified by ``key + '_file'``.
+
+        ``fallback`` is *not* JSON parsed but used verbatim when no config 
value or file is given.
         """
         try:
             data = self.get(section=section, key=key, fallback=None, 
_extra_stacklevel=1, **kwargs)
         except (NoSectionError, NoOptionError):
             data = None
 
         if data is None or data == "":
+            try:

Review Comment:
   This is the placement the dev-list thread landed on differently. Quoting it:
   
   > I don't think we should limit it to just `getjson` though as your PR 
currently does. The feature should live at the higher level that `_cmd` and 
`_secret` live with all the same checks and allowlisting that those abide by.
   
   > There is no particular reason to limit it to .json only, I can clearly see 
the case where you want to read string or boolean values from files named by 
the variables.
   
   Concretely: a `_get_file_option()` sibling of `_get_cmd_option()` 
(`parser.py:1154`) and `_get_secret_option()` (`parser.py:1172`), wired into 
the same `get()` lookup chain those two are resolved in (around 
`parser.py:1244` / `parser.py:1270`). The suffix then returns the file's 
contents as the raw string value, every typed accessor (`get`, `getboolean`, 
`getint`, `getfloat`, `getjson`) picks it up for free, and `getjson` needs no 
change at all.
   
   Full thread: https://lists.apache.org/thread/fpzwkdjxl1trs4q4hqbgj5w7vwz0kvyv



##########
shared/configuration/src/airflow_shared/configuration/parser.py:
##########
@@ -1602,16 +1602,35 @@ def getjson(
         self, section: str, key: str, fallback=None, **kwargs
     ) -> dict | list | str | int | float | None:
         """
-        Return a config value parsed from a JSON string.
+        Return a config value parsed from a JSON string or JSON file.
 
-        ``fallback`` is *not* JSON parsed but used verbatim when no config 
value is given.
+        If no direct config value is given for ``key``, attempts to read from 
a file path
+        specified by ``key + '_file'``.
+
+        ``fallback`` is *not* JSON parsed but used verbatim when no config 
value or file is given.
         """
         try:
             data = self.get(section=section, key=key, fallback=None, 
_extra_stacklevel=1, **kwargs)
         except (NoSectionError, NoOptionError):
             data = None
 
         if data is None or data == "":
+            try:
+                file_path = self.get(
+                    section=section, key=key + "_file", fallback=None, 
_extra_stacklevel=1, **kwargs
+                )
+            except (NoSectionError, NoOptionError):
+                file_path = None
+
+            if file_path:

Review Comment:
   This is the "all the same checks and allowlisting" half of the dev-list 
request, and it is easy to miss, so calling it out on its own. Both siblings 
open with the same guard:
   
   ```python
   def _get_cmd_option(self, section: str, key: str):
       """Get config option from command execution."""
       fallback_key = key + "_cmd"
       if (section, key) in self.sensitive_config_values:
   ```
   
   `sensitive_config_values` is not only an opt-in list — it also drives 
masking (`parser.py:638`) and is what `as_dict()` keys its `include_cmds` / 
`include_secret` expansion off. This branch consults no such set, so any 
`[section] anykey_file` is opened and read, and the resolved value participates 
in none of that machinery.
   
   To be clear on severity: this is **not** a privilege-escalation issue. 
Anyone who can write `airflow.cfg` or set `AIRFLOW__*` is a Deployment Manager 
who already has full control, which the security model puts out of scope. It 
matters for consistency and masking behaviour. Moving the feature into the 
generic chain resolves it by construction.



##########
shared/configuration/src/airflow_shared/configuration/parser.py:
##########
@@ -1602,16 +1602,35 @@ def getjson(
         self, section: str, key: str, fallback=None, **kwargs
     ) -> dict | list | str | int | float | None:
         """
-        Return a config value parsed from a JSON string.
+        Return a config value parsed from a JSON string or JSON file.
 
-        ``fallback`` is *not* JSON parsed but used verbatim when no config 
value is given.
+        If no direct config value is given for ``key``, attempts to read from 
a file path
+        specified by ``key + '_file'``.
+
+        ``fallback`` is *not* JSON parsed but used verbatim when no config 
value or file is given.
         """
         try:
             data = self.get(section=section, key=key, fallback=None, 
_extra_stacklevel=1, **kwargs)
         except (NoSectionError, NoOptionError):
             data = None
 
         if data is None or data == "":
+            try:
+                file_path = self.get(
+                    section=section, key=key + "_file", fallback=None, 
_extra_stacklevel=1, **kwargs
+                )
+            except (NoSectionError, NoOptionError):
+                file_path = None
+
+            if file_path:
+                try:
+                    with open(file_path) as f:
+                        return json.load(f)
+                except Exception as e:

Review Comment:
   `except Exception` is wider than it needs to be here — it also turns genuine 
programming errors (a non-string path yielding `TypeError`, say) into an 
`AirflowConfigException`. The string branch a few lines below narrows to 
`JSONDecodeError`; `except (OSError, ValueError)` would match that convention 
and still cover both file-IO and JSON-decode failures, since `JSONDecodeError` 
subclasses `ValueError`.



##########
shared/configuration/tests/configuration/test_parser.py:
##########
@@ -338,6 +338,49 @@ def test_getjson_fallback(self, fallback):
 
         assert test_conf.getjson("test", "json", fallback=fallback) == fallback
 
+    def test_getjson_from_file(self, tmp_path):

Review Comment:
   These three cover the happy path, the env-var path and invalid JSON. The two 
branches most likely to regress are not covered:
   
   1. **Precedence** — both `key` and `key_file` set. The direct value should 
win; that is the `data is None or data == ""` condition doing real work, and 
nothing pins it down.
   2. **Missing file** — `key_file` pointing at a path that does not exist. 
Today that raises rather than returning `fallback`. That is a defensible 
choice, but it is currently neither documented in the docstring nor tested, so 
it is easy to change by accident.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to