Skip to content

Commit 93861d0

Browse files
pgpxPaul Martin
and
Paul Martin
authoredNov 25, 2024
Fix handling of osxkeychain addtional keys (#391) (#392)
* Fix handling of osxkeychain addtional keys (#391) git-credential-helper v2.45 < v2.47.0 output additional garbage bytes after the username. git-credential-helper >= v2.46.0 outputs additional `capability[]` and `state` keys. * Fix typing of credentials Dict --------- Co-authored-by: Paul Martin <paul.martin@telekom.com>
1 parent a38304b commit 93861d0

File tree

2 files changed

+73
-2
lines changed

2 files changed

+73
-2
lines changed
 

‎src/scmrepo/git/credentials.py

+33-2
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,29 @@ def get(self, credential: "Credential", **kwargs) -> "Credential":
173173
if res.stderr:
174174
logger.debug(res.stderr)
175175

176-
credentials = {}
176+
credentials: dict[str, Any] = {}
177177
for line in res.stdout.splitlines():
178178
try:
179179
key, value = line.split("=", maxsplit=1)
180-
credentials[key] = value
180+
# Only include credential values that are used in the Credential
181+
# constructor.
182+
# Other values may be returned by the subprocess, but they must be
183+
# ignored.
184+
# e.g. osxkeychain credential helper >= 2.46.0 can return
185+
# `capability[]` and `state`)
186+
if key in [
187+
"protocol",
188+
"host",
189+
"path",
190+
"username",
191+
"password",
192+
"password_expiry_utc",
193+
"url",
194+
]:
195+
# Garbage bytes were output from git-credential-osxkeychain from
196+
# 2.45.0 to 2.47.0:
197+
# https://github.com/git/git/commit/6c3c451fb6e1c3ca83f74e63079d4d0af01b2d69
198+
credentials[key] = _strip_garbage_bytes(value)
181199
except ValueError:
182200
continue
183201
if not credentials:
@@ -265,6 +283,19 @@ def get_matching_commands(
265283
)
266284

267285

286+
def _strip_garbage_bytes(s: str) -> str:
287+
"""
288+
Garbage (random) bytes were output from git-credential-osxkeychain from
289+
2.45.0 to 2.47.0 so must be removed.
290+
https://github.com/git/git/commit/6c3c451fb6e1c3ca83f74e63079d4d0af01b2d69
291+
:param s: string that might contain garbage/random bytes
292+
:return str: The string with the garbage bytes removed
293+
"""
294+
# Assume that any garbage bytes begin with a 0-byte
295+
zero = s.find(chr(0))
296+
return s[0:zero] if zero >= 0 else s
297+
298+
268299
class _CredentialKey(NamedTuple):
269300
protocol: str
270301
host: Optional[str]

‎tests/test_credentials.py

+40
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import io
22
import os
3+
import random
34

45
import pytest
56

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

4748

49+
def test_subprocess_ignore_unexpected_credential_keys(git_helper, mocker):
50+
git_helper.use_http_path = True
51+
run = mocker.patch(
52+
"subprocess.run",
53+
# Simulate git-credential-osxkeychain (version >=2.45)
54+
return_value=mocker.Mock(
55+
stdout="username=foo\npassword=bar\ncapability[]=state\nstate[]=osxkeychain:seen=1"
56+
),
57+
)
58+
creds = git_helper.get(Credential(protocol="https", host="foo.com", path="foo.git"))
59+
assert run.call_args.args[0] == ["git-credential-foo", "get"]
60+
assert (
61+
run.call_args.kwargs.get("input")
62+
== "protocol=https\nhost=foo.com\npath=foo.git\n"
63+
)
64+
assert creds == Credential(username="foo", password="bar")
65+
66+
67+
def test_subprocess_strip_trailing_garbage_bytes(git_helper, mocker):
68+
"""Garbage bytes were output from git-credential-osxkeychain from 2.45.0 to 2.47.0
69+
so must be removed
70+
https://github.com/git/git/commit/6c3c451fb6e1c3ca83f74e63079d4d0af01b2d69"""
71+
git_helper.use_http_path = True
72+
run = mocker.patch(
73+
"subprocess.run",
74+
# Simulate git-credential-osxkeychain (version 2.45), assuming initial 0-byte
75+
return_value=mocker.Mock(
76+
stdout=f"username=foo\npassword=bar{chr(0)}{random.randbytes(15)}"
77+
),
78+
)
79+
creds = git_helper.get(Credential(protocol="https", host="foo.com", path="foo.git"))
80+
assert run.call_args.args[0] == ["git-credential-foo", "get"]
81+
assert (
82+
run.call_args.kwargs.get("input")
83+
== "protocol=https\nhost=foo.com\npath=foo.git\n"
84+
)
85+
assert creds == Credential(username="foo", password="bar")
86+
87+
4888
def test_subprocess_get_failed(git_helper, mocker):
4989
from subprocess import CalledProcessError
5090

0 commit comments

Comments
 (0)