-
-
Notifications
You must be signed in to change notification settings - Fork 402
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
add support for git credential helpers #976
Conversation
c3e09d6
to
eeea755
Compare
Codecov Report
@@ Coverage Diff @@
## master #976 +/- ##
=======================================
Coverage 85.05% 85.05%
=======================================
Files 93 93
Lines 22901 22901
Branches 3359 3359
=======================================
Hits 19479 19479
Misses 2957 2957
Partials 465 465 📣 Codecov can now indicate which changes are the most critical in Pull Requests. Learn more |
eeea755
to
4df8f7d
Compare
7c6a29f
to
452bc9a
Compare
Would appreciate if you could take a look @pmrowla |
dulwich/credentials.py
Outdated
for line in res.stdout.decode("UTF-8").strip().split("\n"): | ||
try: | ||
key, value = line.split("=") | ||
credentials[key] = value | ||
except ValueError: | ||
continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should be making assumptions about text encodings here, and just handle credentials as Dict[bytes, bytes]
rather than [str, str]
.
Text encoding for HTTP basic auth username/password is technically undefined (https://www.rfc-editor.org/rfc/rfc7617#appendix-B.3), and depending on the credential manager/system locale settings/etc, I think it's possible that the returned credentials here may not be utf-8 or ascii.
Then later we can just do b64encode(username + b':' + password)
where the username/pw are already bytes objects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed and corrected.
Note though that when username/password kwargs are provided to Urllib3HttpGitClient()
, we use urllib3.utils.make_headers
:
Line 2187 in bb83067
basic_auth = urllib3.util.make_headers(basic_auth=credentials) |
which encodes the basic_auth
string as latin-1
:
https://github.com/urllib3/urllib3/blob/972e9f02cd219892701fa0a1129dda7f81dac535/src/urllib3/util/request.py#L101-L104
In practice though, Imost of the time user/pass will be provided as kwargs when using from_parsedurl
and in this case username/password are already decoded through urlparse
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When using creds from a credential manager we should be generating the basic auth header ourselves. (We should be following the RFC, not urllib3)
452bc9a
to
f90afc7
Compare
a0aaf5a
to
5068244
Compare
357cfd7
to
5e58402
Compare
This comment was marked as outdated.
This comment was marked as outdated.
dulwich/client.py
Outdated
self.credentials = self.get_credentials(url) | ||
try: | ||
credentials = next(self.credentials) | ||
except StopIteration: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
won't this keep retrying upon a 404?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should not, there's a test for that here:
https://github.com/dtrifiro/dulwich/blob/f3ed0104e17c3b9c74952fe62f401008cb590579/dulwich/tests/test_client.py#L1205
dulwich/credentials.py
Outdated
|
||
argv = self.command.split(" ") | ||
if not os.path.isabs(argv[0]): | ||
cmd = ["git", f"credential-{argv[0]}"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dulwich is meant to be independent of C Git; please don't introduce dependencies on C git.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify the intention here isn't to introduce any dependency on C Git, the expected behavior for 3rd party credential helpers is to follow the executable naming conventions set by C Git:
Generally speaking, rule (3) above is the simplest for users to specify. Authors of credential helpers should make an effort to assist their users by naming their program "git-credential-$NAME", and putting it in the $PATH or $GIT_EXEC_PATH during installation, which will allow a user to enable it with git config credential.helper $NAME.
Although that does mean we probably should just use git-credential-{name}
instead of relying on git credential-{name}
@dtrifiro
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree.
The rationale for using git credential-foo
instead of git-credential-foo
here was that (I'm assuming) most users will be using cgit provided credential helpers (such as git-credential-store
, git-credential-osxkeychain
, git-credential-gnome-keyring
, etc), which are in GIT_EXEC_PATH
(which is not in PATH
). Using git
made sure that GIT_EXEC_PATH
was also checked.
Pushed a patch which fixes the issue: if git-credential-foo
is not in PATH
and git
cli is available, GIT_EXEC_PATH
is retrieved using git --exec-path
and we check if git-credential-foo
is available in GIT_EXEC_PATH
. If git
cli is not available, nothing is done, and calling subprocess.run
will just raise FileNotFoundError
(which will result in CredentialNotFound
exception).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.
159278f
to
f3ed010
Compare
1705fbf
to
e9cca29
Compare
Hey @jelmer, this is ready for review btw 🙂 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for sitting on this for so long; I'm a bit concerned about how this makes the interface for authentication more complicated. Ideally there should just be one pythonic interface for authentication, with perhaps one implementation of that that can interact with C git.
dulwich/credentials.py
Outdated
def match_partial_url(valid_url: "ParseResult", partial_url: str) -> bool: | ||
"""matches a parsed url with a partial url (no scheme/netloc)""" | ||
if "://" not in partial_url: | ||
parsed = urlparse("scheme://" + partial_url) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be an error instead?
dulwich/credentials.py
Outdated
(parsed.port and valid_url.port != parsed.port), | ||
( | ||
parsed.path | ||
and not valid_url.path.rstrip("/").startswith(parsed.path.rstrip("/")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't this consider http://example.com/bla to match http://example.com/blah ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, fixed.
dulwich/credentials.py
Outdated
|
||
from dulwich.config import ConfigDict, StackedConfig, get_xdg_config_home_path | ||
|
||
if TYPE_CHECKING: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for this as we're already importing urllib.parse
return cls(command.decode(encoding)) | ||
|
||
|
||
def get_credentials_from_store( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally we'd just have one pythonic interface that's implemented by the static credentials files, the helper class and any other python code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved get_credentials_from_store
from client
to here since it seemed the right place for it, but it is dead code. I would remove this if that's ok with you.
|
||
def get_credentials_from_helper(base_url: str, config) -> Tuple[bytes, bytes]: | ||
"""Retrieves credentials for the given url from git credential helpers""" | ||
if isinstance(config, StackedConfig): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this need to peek under the covers, can't it just use the config API?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given an URL, git loops though the available configs and matches urls in credentials sections with the provided URL, which is something we cannot do with the config API.
For example with a config like this:
[credential "https://github.com/jelmer/dulwich"]
helper = githubhelper
[credential]
helper = generichelper
Running
config.get((b"credential", b"https://github.com))
would return generichelper
instead of githubhelper
, which is not what we want.
I refactored this a bit so that we use urlmatch_credential_sections
to cycle though config sections that match the URL.
This could be extracted to become a config API method, although I'm not sure if there's any other use cases where one needs to match URLs in sections, apart from the credential helpers scenario.
f1343d2
to
a60bdb1
Compare
Hi @jelmer, thanks for taking the time to review this 🙂
I'm not sure I understand what you're proposing here, but below you'll find my thoughts. Please let me know if I misunderstood and/or if there's any other parts you find complicated you can point me to, so I can have another look and take action. With my last push I streamlined |
a60bdb1
to
c2200c5
Compare
Temporary patch while waiting for jelmer/dulwich#976
Temporary patch while waiting for jelmer/dulwich#976
Temporary patch while waiting for jelmer/dulwich#976
Temporary patch while waiting for jelmer/dulwich#976
Temporary patch while waiting for jelmer/dulwich#976
Hey @jelmer, just a friendly ping: this is ready for review 🙂 |
sorry about that, looking at it again now |
Haven't forgotten about this, will probably put forward a PR with a subset of your changes later this week. |
- add misc url-matching functions Extracted from #976
Sorry again for the long delay. As a means of trying to move this forward, I've at least cherry-pick the config-related bits of this PR that are non-controversial. |
c2200c5
to
7bdc74a
Compare
Hey @jelmer, sorry for keeping this hanging so long. I just rebased and added a new commit to update the If you have any other concerns and/or suggestions that would help this get merged, I'd greatly appreciate that. Thanks! |
- add `CredentialHelper` class to run credential helper commands and retrieve values - add `get_credentials_from_helper` to retrieve credentials from helpers defined in provided config - moved (unused) `get_credentials_from_store` to `credentials.py` Also see https://www.git-scm.com/docs/gitcredentials fixes jelmer#873
7bdc74a
to
c9e2d47
Compare
CredentialHelper
class to run credential helper commands and retrieve valuesget_credentials_from_helper
to retrieve credentials from helpers defined in provided configget_credentials_from_store
tocredentials.py
, refactor it to useCredentialHelper
withstore
helper.Also see https://www.git-scm.com/docs/gitcredentials
fixes #873