Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix handling of osxkeychain addtional keys (#391) #392

Merged
merged 2 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions src/scmrepo/git/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,29 @@ def get(self, credential: "Credential", **kwargs) -> "Credential":
if res.stderr:
logger.debug(res.stderr)

credentials = {}
credentials: dict[str, Any] = {}
for line in res.stdout.splitlines():
try:
key, value = line.split("=", maxsplit=1)
credentials[key] = value
# Only include credential values that are used in the Credential
# constructor.
# Other values may be returned by the subprocess, but they must be
# ignored.
# e.g. osxkeychain credential helper >= 2.46.0 can return
# `capability[]` and `state`)
if key in [
"protocol",
"host",
"path",
"username",
"password",
"password_expiry_utc",
"url",
]:
# Garbage bytes were output from git-credential-osxkeychain from
# 2.45.0 to 2.47.0:
# https://github.com/git/git/commit/6c3c451fb6e1c3ca83f74e63079d4d0af01b2d69
credentials[key] = _strip_garbage_bytes(value)
except ValueError:
continue
if not credentials:
Expand Down Expand Up @@ -265,6 +283,19 @@ def get_matching_commands(
)


def _strip_garbage_bytes(s: str) -> str:
"""
Garbage (random) bytes were output from git-credential-osxkeychain from
2.45.0 to 2.47.0 so must be removed.
https://github.com/git/git/commit/6c3c451fb6e1c3ca83f74e63079d4d0af01b2d69
:param s: string that might contain garbage/random bytes
:return str: The string with the garbage bytes removed
"""
# Assume that any garbage bytes begin with a 0-byte
zero = s.find(chr(0))
return s[0:zero] if zero >= 0 else s


class _CredentialKey(NamedTuple):
protocol: str
host: Optional[str]
Expand Down
40 changes: 40 additions & 0 deletions tests/test_credentials.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io
import os
import random

import pytest

Expand Down Expand Up @@ -45,6 +46,45 @@ def test_subprocess_get_use_http_path(git_helper, mocker):
assert creds == Credential(username="foo", password="bar")


def test_subprocess_ignore_unexpected_credential_keys(git_helper, mocker):
git_helper.use_http_path = True
run = mocker.patch(
"subprocess.run",
# Simulate git-credential-osxkeychain (version >=2.45)
return_value=mocker.Mock(
stdout="username=foo\npassword=bar\ncapability[]=state\nstate[]=osxkeychain:seen=1"
),
)
creds = git_helper.get(Credential(protocol="https", host="foo.com", path="foo.git"))
assert run.call_args.args[0] == ["git-credential-foo", "get"]
assert (
run.call_args.kwargs.get("input")
== "protocol=https\nhost=foo.com\npath=foo.git\n"
)
assert creds == Credential(username="foo", password="bar")


def test_subprocess_strip_trailing_garbage_bytes(git_helper, mocker):
"""Garbage bytes were output from git-credential-osxkeychain from 2.45.0 to 2.47.0
so must be removed
https://github.com/git/git/commit/6c3c451fb6e1c3ca83f74e63079d4d0af01b2d69"""
git_helper.use_http_path = True
run = mocker.patch(
"subprocess.run",
# Simulate git-credential-osxkeychain (version 2.45), assuming initial 0-byte
return_value=mocker.Mock(
stdout=f"username=foo\npassword=bar{chr(0)}{random.randbytes(15)}"
),
)
creds = git_helper.get(Credential(protocol="https", host="foo.com", path="foo.git"))
assert run.call_args.args[0] == ["git-credential-foo", "get"]
assert (
run.call_args.kwargs.get("input")
== "protocol=https\nhost=foo.com\npath=foo.git\n"
)
assert creds == Credential(username="foo", password="bar")


def test_subprocess_get_failed(git_helper, mocker):
from subprocess import CalledProcessError

Expand Down
Loading