mobuchowski commented on code in PR #75860:
URL: https://github.com/apache/airflow/pull/75860#discussion_r4403595374


##########
providers/openlineage/src/airflow/providers/openlineage/utils/utils.py:
##########
@@ -951,8 +953,13 @@ def _include_fields(self):
                     "nor is defined as an attrs class."
                 )
             for field, val in obj_fields.items():
+                # Keys that aren't identifiers are set by libraries rather 
than being attributes, e.g. the
+                # unpicklable ``__wire|<class>|<method>`` caches of 
``methodtools.lru_cache``.
+                if not field.isidentifier():
+                    continue
                 if field not in self._fields and field not in self.excludes 
and field not in self.renames:
-                    setattr(self, field, val)
+                    # Any attribute may hold live Airflow objects, even nested 
ones that can't be pickled
+                    setattr(self, field, 
_convert_to_json_safe(self._cast_basic_types(val), field))

Review Comment:
   `OpenLineageRedactor._redact` sends anything that is JSON-serializable and 
has a `__dict__` to a branch that only redacts `item.__dict__`, then returns 
the item. A dict subclass always has `__dict__`, and after 
`self.__dict__.clear()` it's always empty, so that branch never masks anything 
for info objects. Before this change, `TaskInfoComplete` almost always held at 
least one non-JSON value (even a plain `BashOperator` has a function among its 
attributes), so it failed `is_json_serializable` and fell through to 
`SecretsMasker._redact`. That method walks the dict items and masks registered 
secrets and sensitive key names. Now that every value is JSON-native, 
`TaskInfoComplete` always takes the branch that masks nothing.
   
   I reproduced it with `mask_secret("s3cr3tvalue")`, a 
`BashOperator(bash_command="curl -u user:s3cr3tvalue …")` and an operator 
attribute holding a `Connection(password="s3cr3tvalue")`, then ran 
`OpenLineageRedactor.from_masker(_secrets_masker()).redact(...)`:
   
   | | `bash_command` | `conn` |
   |---|---|---|
   | base (`27c76244`) | `curl -u user:*** …` | `Connection(..., 
password='***', ...)` |
   | previous head (`a3692d6e`) | `curl -u user:*** …` | `Connection(..., 
password='***', ...)` |
   | this head (`9c22a0a8`) | `curl -u user:s3cr3tvalue …` | `Connection(..., 
password='s3cr3tvalue', ...)` |
   
   So with `include_full_task_info` on, rendered templated fields and secrets 
held on the operator are emitted in plain text. A plain `BashOperator`, with no 
extra attribute, leaks its rendered `bash_command` too. The `default=str` 
conversion makes this worse: values the client's `Serde` used to emit as 
`<<non-serializable: Connection>>` are now emitted as their full `repr`, and 
attrs classes such as `Connection` include every field in it.
   
   One-line fix I tested (the rest of the openlineage unit tests still pass): 
don't send dicts to the attribute-walking branch, so the base masker walks 
their items:
   ```python
   if is_json_serializable(item) and hasattr(item, "__dict__") and not 
isinstance(item, dict):
   ```
   A test that redacts a `TaskInfoComplete` with a masked secret in a templated 
field would guard against this regressing again.



-- 
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