ephraimbuddy commented on code in PR #64105:
URL: https://github.com/apache/airflow/pull/64105#discussion_r4062472723


##########
providers/git/src/airflow/providers/git/bundles/git.py:
##########
@@ -317,6 +322,47 @@ def _clone_bare_repo_if_required(self) -> None:
                 shutil.rmtree(self.bare_repo_path)
             raise
 
+    def _sync_bare_repo_remote_url(self) -> None:
+        """
+        Re-point the bare repo's origin at the current repo url.
+
+        Called standalone from the ``_initialize`` fast paths that skip 
cloning and never
+        reach ``_clone_bare_repo_if_required``, so a bundle that takes one 
would otherwise
+        keep a credentialed origin url in ``bare/config`` forever. This is 
best-effort: a
+        bundle that can still be served from disk must not fail to initialize 
because its
+        bare repo is unreadable.
+        """
+        try:
+            if not self.bare_repo_path.exists():
+                return
+            bare_repo = Repo(self.bare_repo_path)
+            try:
+                self._rewrite_bare_repo_origin(bare_repo)
+            finally:
+                bare_repo.close()
+        except Exception as e:
+            # Deliberately broad: opening the repo raises anything from 
``configparser`` on a
+            # truncated config to ``GitError`` on an unsafe remote url, and 
the fast paths this
+            # runs ahead of never needed the bare repo at all.
+            self._log.warning(
+                "Could not rewrite the bare repository origin, a credential 
may remain in "
+                "cleartext in the bundle's bare/config",
+                bare_repo_path=self.bare_repo_path,
+                exc=e,
+            )
+
+    def _rewrite_bare_repo_origin(self, bare_repo: Repo) -> None:
+        if "origin" not in bare_repo.remotes:
+            return
+        origin = bare_repo.remotes.origin
+        repo_url = str(self.repo_url)
+        # Bundles cloned before credentials moved to a credential helper 
embedded ``user:token``
+        # here, so the token sits in cleartext in ``<bundle>/bare/config`` 
where any Dag author
+        # on the Dag processor can read it. Rewriting origin is what removes 
it from those bundles.
+        if origin.url != repo_url:
+            self._log.info("Updating bare repository remote url", 
bare_repo_path=self.bare_repo_path)
+            origin.set_url(repo_url)

Review Comment:
   For a relative `repo_url` such as `path/to/repo`, Git stores an absolute 
origin during cloning. This overwrites it with the relative path, so the 
following fetch resolves it inside the bare repository and fails on both 
attempts. Please preserve Git’s resolved local origin or normalize local paths 
before rewriting, and add a regression test using a relative source path



##########
providers/git/src/airflow/providers/git/hooks/git.py:
##########
@@ -242,73 +247,91 @@ def _ensure_github_app_token(self) -> None:
             )
             self.user_name, self.auth_token, self.github_app_token_exp = 
self._get_github_app_token()
 
+    def _strip_embedded_credentials(self) -> tuple[str | None, str | None]:
+        """Take any ``user:password@`` out of the repo url and return what it 
held."""
+        if not isinstance(self.repo_url, str) or not 
self.repo_url.startswith(("http://";, "https://";)):
+            return None, None
+        scheme, separator, rest = self.repo_url.partition("://")
+        authority, slash, path = rest.partition("/")
+        userinfo, at_sign, host = authority.rpartition("@")
+        user, _, password = userinfo.partition(":")
+        # A bare ``user@`` holds no secret, so leave those urls exactly as the 
connection wrote
+        # them; anything git clones from a stripped url would lose the 
username for nothing.
+        if not at_sign or not password:
+            return None, None
+        self.repo_url = f"{scheme}{separator}{host}{slash}{path}"
+        return unquote(user) or None, unquote(password)
+
+    def _extract_credential_scope(self) -> str:
+        """Return the ``<scheme>://<host>[:port]`` git matches a credential 
config against."""
+        scheme, _, rest = str(self.repo_url).partition("://")
+        host = rest.partition("/")[0].rpartition("@")[2]
+        return f"{scheme}://{host}" if host else ""
+
     @contextlib.contextmanager
-    def _github_app_askpass_env(self) -> Generator[None]:
-        if not self.auth_token:
+    def _token_credential_env(self) -> Generator[None]:
+        """Hand the token to git through a credential helper scoped to the 
repository's host."""
+        # Credential helpers only serve http(s); an SSH connection that 
happens to carry a
+        # password would gain nothing from one.
+        if not self.auth_token or not 
str(self.repo_url).startswith(("http://";, "https://";)):
             yield
             return
 
-        token = shlex.quote(self.auth_token)
-        with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=True) 
as askpass_script:
-            askpass_script.write(
-                "#!/bin/sh\n"
-                'case "$1" in\n'
-                "  *Username*) echo x-access-token;;\n"
-                f"  *Password*) echo {token};;\n"
-                f"  *) echo {token};;\n"
-                "esac\n"
-            )
-            askpass_script.flush()
-            os.chmod(askpass_script.name, stat.S_IRWXU)
+        scope = self._extract_credential_scope()
+        if not scope:
+            yield
+            return
+
+        with tempfile.TemporaryDirectory() as helper_dir:
+            helper_path = os.path.join(helper_dir, "credential-helper.sh")
+            # git matches the configured scope against the url it parsed, then 
hands the helper
+            # structured fields on stdin. A submodule elsewhere never reaches 
this helper, and no
+            # part of the decision depends on the wording of a human-readable 
prompt.
+            # Written and closed before git runs: Linux refuses to exec a file 
that is still
+            # open for writing, which git surfaces as "cannot exec: Text file 
busy".
+            with open(helper_path, "w") as helper_script:
+                helper_script.write(
+                    r"""#!/bin/sh
+cat > /dev/null
+[ "$1" = get ] || exit 0
+printf 'username=%s\npassword=%s\n' "$AIRFLOW_GIT_USER" "$AIRFLOW_GIT_TOKEN"
+"""
+                )
+            os.chmod(helper_path, stat.S_IRWXU)
 
-            old_askpass = os.environ.get("GIT_ASKPASS")
-            old_lc_all = os.environ.get("LC_ALL")
-            old_terminal_prompt = os.environ.get("GIT_TERMINAL_PROMPT")
+            # Append to any GIT_CONFIG_* the deployment already exports rather 
than replacing it.
+            try:
+                index = int(os.environ.get("GIT_CONFIG_COUNT", "0"))
+            except ValueError:
+                index = 0
+            # System/global config loads before env-supplied config, so a 
deployment-wide
+            # `credential.helper` would otherwise answer `get` first and our 
token would never
+            # be used. git also invokes every helper on `approve`, so a 
`store` helper would
+            # persist it to ~/.git-credentials. Reset the scope to empty first 
(git help credentials).
+            values = {
+                "GIT_CONFIG_COUNT": str(index + 2),
+                f"GIT_CONFIG_KEY_{index}": f"credential.{scope}.helper",
+                f"GIT_CONFIG_VALUE_{index}": "",
+                f"GIT_CONFIG_KEY_{index + 1}": f"credential.{scope}.helper",
+                f"GIT_CONFIG_VALUE_{index + 1}": helper_path,

Review Comment:
   Git interprets `credential.helper` through a shell, so this unquoted path 
breaks token authentication when `TMPDIR` contains spaces. Please use `!` 
followed by `shlex.quote(helper_path)` and cover a temporary directory 
containing spaces; quoting without `!` makes Git treat it as a named helper



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