vatsrahul1001 commented on code in PR #72650:
URL: https://github.com/apache/airflow/pull/72650#discussion_r4306172279


##########
airflow-core/src/airflow/utils/cli.py:
##########
@@ -137,6 +137,297 @@ def wrapper(*args, **kwargs):
     return action_logging
 
 
+MASKED_VALUE = "*" * 8
+
+# Options whose value is a secret on its own, keyed by the destination 
argparse stores the
+# parsed value under. Masking is driven by what is present in the parsed 
namespace, so ``-p``
+# is only treated as a password on commands where it parses into ``password`` 
(``api-server -p``
+# parses into ``port`` and is left alone).
+_SECRET_OPTIONS: dict[str, tuple[str, ...]] = {
+    "password": ("-p", "--password"),
+    "conn_password": ("--conn-password",),
+    "basic_auth": ("-A", "--basic-auth"),
+}
+# Options whose value is JSON that may hold secrets under sensitive keys.
+_JSON_OPTIONS: dict[str, tuple[str, ...]] = {
+    "conn_json": ("--conn-json",),
+    "conn_extra": ("--conn-extra",),
+    "conf": ("-c", "--conf"),
+    "dag_run_conf": ("--dag-run-conf",),
+    "task_params": ("-t", "--task-params"),
+    "env_vars": ("--env-vars",),
+}
+# Options whose value is a URI that may hold secrets in its credentials or 
query.
+_URI_OPTIONS: dict[str, tuple[str, ...]] = {
+    "conn_uri": ("--conn-uri",),
+    "broker_api": ("-a", "--broker-api"),
+}
+# Short options of the commands that take one of the short options above, as
+# ``(boolean flags, options taking a value)``, so a cluster such as 
``-vpVALUE`` is walked the
+# way argparse reads it. Keep in sync with the command definitions in 
``cli_config`` and the
+# FAB and Celery providers. Commands missing here are masked conservatively.
+_SHORT_OPTION_ARITY: dict[tuple[str, str], tuple[str, str]] = {

Review Comment:
   Optional: this table is hand-synced with the command definitions, and drift 
would silently change what gets masked. A test that checks the entries against 
the real parsers in `cli_config` would catch that.



##########
airflow-core/src/airflow/utils/cli.py:
##########
@@ -154,59 +445,42 @@ def _build_metrics(func_name, namespace):
     sub_commands_to_check_for_sensitive_fields = {"users", "connections"}
     sub_commands_to_check_for_sensitive_key = {"variables"}
     sensitive_fields = {"-p", "--password", "--conn-password"}
+    # The long spellings name a password on every subcommand, including ones 
contributed by
+    # providers (``keycloak-auth-manager ... --password``), so they are masked 
everywhere. ``-p``
+    # is only a password on the subcommands above; elsewhere it is e.g. 
``--port``.
+    always_sensitive_fields = {"--password", "--conn-password"}
     full_command = list(sys.argv)
     sub_command = full_command[1] if len(full_command) > 1 else None
     # For cases when value under sub_commands_to_check_for_sensitive_key have 
sensitive info
     if sub_command in sub_commands_to_check_for_sensitive_key:
-        key = full_command[-2] if len(full_command) > 3 else None
-        if key and _secrets_masker().should_hide_value_for_key(key):
-            # Mask the sensitive value since key contain sensitive keyword
-            full_command[-1] = "*" * 8
-    elif sub_command in sub_commands_to_check_for_sensitive_fields:
+        # Identify the value from the parsed command rather than from its 
position in
+        # argv. The key and value are not reliably the last two arguments: any 
supported
+        # trailing option displaces them (``variables set KEY VALUE 
--description TEXT``
+        # ends in ``--description TEXT``), which left the value recorded in 
clear.
+        key = getattr(namespace, "key", None)
+        value = getattr(namespace, "value", None)
+        if key and value and _secrets_masker().should_hide_value_for_key(key):
+            full_command = [MASKED_VALUE if arg == value else arg for arg in 
full_command]
+    else:
+        fields = (
+            sensitive_fields
+            if sub_command in sub_commands_to_check_for_sensitive_fields
+            else always_sensitive_fields
+        )
         for idx, command in enumerate(full_command):
-            if command in sensitive_fields:
+            if command in fields:
                 # For cases when password is passed as "--password xyz" (with 
space between key and value)
-                full_command[idx + 1] = "*" * 8
+                if idx + 1 < len(full_command):
+                    full_command[idx + 1] = MASKED_VALUE
             else:
                 # For cases when password is passed as "--password=xyz" (with 
'=' between key and value)
-                for sensitive_field in sensitive_fields:
+                for sensitive_field in fields:
                     if command.startswith(f"{sensitive_field}="):
-                        full_command[idx] = f"{sensitive_field}={'*' * 8}"
-
-    # handle conn-json and conn-uri separately as it requires different 
handling
-    if "--conn-json" in full_command:
-        import json
-
-        json_index = full_command.index("--conn-json") + 1
-        conn_json = json.loads(full_command[json_index])
-        for k in conn_json:
-            if k and _secrets_masker().should_hide_value_for_key(k):
-                conn_json[k] = "*" * 8
-        full_command[json_index] = json.dumps(conn_json)
-
-    if "--conn-uri" in full_command:
-        from urllib.parse import urlparse, urlunparse
-
-        uri_index = full_command.index("--conn-uri") + 1
-        conn_uri = full_command[uri_index]
-        parsed_uri = urlparse(conn_uri)
-        netloc = parsed_uri.netloc
-        if parsed_uri.password:
-            password = "*" * 8
-            netloc = f"{parsed_uri.username}:{password}@{parsed_uri.hostname}"
-            if parsed_uri.port:
-                netloc += f":{parsed_uri.port}"
-
-        full_command[uri_index] = urlunparse(
-            (
-                parsed_uri.scheme,
-                netloc,
-                parsed_uri.path,
-                parsed_uri.params,
-                parsed_uri.query,
-                parsed_uri.fragment,
-            )
-        )
+                        full_command[idx] = f"{sensitive_field}={MASKED_VALUE}"
+
+    # Options carrying secrets (on their own, or inside JSON and URIs) are 
masked by their
+    # parsed value, whatever spelling was used for them on the command line.
+    full_command = _mask_parsed_arguments(full_command, namespace)

Review Comment:
   `_build_metrics` runs before the `try` in the `action_logging` wrapper, so 
any unexpected exception from this masking aborts every CLI command. It would 
be safer to catch exceptions here and fall back to masking the whole recorded 
command, so a masking bug can neither break the command nor leak a secret.



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