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

add support for git credential helpers #976

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

dtrifiro
Copy link
Contributor

@dtrifiro dtrifiro commented Jun 7, 2022

  • 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, refactor it to use CredentialHelper with store helper.

Also see https://www.git-scm.com/docs/gitcredentials

fixes #873

@codecov-commenter
Copy link

codecov-commenter commented Jun 7, 2022

Codecov Report

Merging #976 (ba0fa0c) into master (ba0fa0c) will not change coverage.
The diff coverage is n/a.

❗ Current head ba0fa0c differs from pull request most recent head 452bc9a. Consider uploading reports for the commit 452bc9a to get more accurate results

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

dulwich/client.py Outdated Show resolved Hide resolved
@dtrifiro dtrifiro force-pushed the feature/credential-helper branch 3 times, most recently from 7c6a29f to 452bc9a Compare June 8, 2022 12:10
@dtrifiro dtrifiro marked this pull request as ready for review June 10, 2022 13:29
@jelmer jelmer self-requested a review June 12, 2022 23:09
@dtrifiro
Copy link
Contributor Author

Would appreciate if you could take a look @pmrowla

dulwich/credentials.py Outdated Show resolved Hide resolved
Comment on lines 94 to 202
for line in res.stdout.decode("UTF-8").strip().split("\n"):
try:
key, value = line.split("=")
credentials[key] = value
except ValueError:
continue
Copy link
Contributor

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.

Copy link
Contributor Author

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:

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.

Copy link
Contributor

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)

dulwich/credentials.py Outdated Show resolved Hide resolved
@dtrifiro dtrifiro force-pushed the feature/credential-helper branch 2 times, most recently from a0aaf5a to 5068244 Compare June 15, 2022 16:42
dulwich/client.py Outdated Show resolved Hide resolved
@dtrifiro dtrifiro force-pushed the feature/credential-helper branch 2 times, most recently from 357cfd7 to 5e58402 Compare June 16, 2022 10:36
@dtrifiro

This comment was marked as outdated.

self.credentials = self.get_credentials(url)
try:
credentials = next(self.credentials)
except StopIteration:
Copy link
Owner

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


argv = self.command.split(" ")
if not os.path.isabs(argv[0]):
cmd = ["git", f"credential-{argv[0]}"]
Copy link
Owner

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.

Copy link
Contributor

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

Copy link
Contributor Author

@dtrifiro dtrifiro Jun 20, 2022

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

dulwich/credentials.py Outdated Show resolved Hide resolved
Copy link
Owner

@jelmer jelmer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

@dtrifiro dtrifiro force-pushed the feature/credential-helper branch 2 times, most recently from 159278f to f3ed010 Compare June 20, 2022 14:18
@dtrifiro dtrifiro requested a review from jelmer June 20, 2022 15:30
@dtrifiro dtrifiro force-pushed the feature/credential-helper branch 2 times, most recently from 1705fbf to e9cca29 Compare June 28, 2022 07:22
@dtrifiro
Copy link
Contributor Author

dtrifiro commented Jun 28, 2022

Hey @jelmer, this is ready for review btw 🙂

@oslobowl
Copy link

Hi guys :) Do you know when this branch will be merged, approximately? Thanks a lot :)

Copy link
Owner

@jelmer jelmer left a 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.

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)
Copy link
Owner

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?

(parsed.port and valid_url.port != parsed.port),
(
parsed.path
and not valid_url.path.rstrip("/").startswith(parsed.path.rstrip("/"))
Copy link
Owner

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 ?

Copy link
Contributor Author

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 Show resolved Hide resolved

from dulwich.config import ConfigDict, StackedConfig, get_xdg_config_home_path

if TYPE_CHECKING:
Copy link
Owner

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(
Copy link
Owner

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.

Copy link
Contributor Author

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):
Copy link
Owner

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?

Copy link
Contributor Author

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.

@dtrifiro
Copy link
Contributor Author

Sorry for sitting on this for so long

Hi @jelmer, thanks for taking the time to review this 🙂

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.

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 get_credentials_from_helpers to explicitly how the configs sections and values are being used. The only other method to get credentials is get_credentials_from_store which, as far as I can tell, is unused. We could either refactor this or get rid of it (as I proposed in another comment).

dtrifiro added a commit to dtrifiro/scmrepo that referenced this pull request Sep 6, 2022
dtrifiro added a commit to dtrifiro/scmrepo that referenced this pull request Sep 6, 2022
dtrifiro added a commit to dtrifiro/scmrepo that referenced this pull request Sep 6, 2022
dtrifiro added a commit to dtrifiro/scmrepo that referenced this pull request Sep 6, 2022
efiop pushed a commit to iterative/scmrepo that referenced this pull request Sep 7, 2022
@dtrifiro dtrifiro requested a review from jelmer October 7, 2022 12:31
@dtrifiro
Copy link
Contributor Author

Hey @jelmer, just a friendly ping: this is ready for review 🙂

@jelmer
Copy link
Owner

jelmer commented Oct 16, 2022

sorry about that, looking at it again now

@jelmer
Copy link
Owner

jelmer commented Oct 19, 2022

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.

jelmer pushed a commit that referenced this pull request Oct 23, 2022
- add misc url-matching functions

Extracted from #976
@jelmer
Copy link
Owner

jelmer commented Oct 23, 2022

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.

@efiop
Copy link
Contributor

efiop commented Oct 26, 2022

Hey @jelmer , thanks for taking a look! 🙏 FYI: @dtrifiro broke his clavicle/collarbone and won't be able to reply for a few weeks. If there is anything he needs to address, he'll get back to it once he's recovered.

@jelmer
Copy link
Owner

jelmer commented Oct 26, 2022

Hey @jelmer , thanks for taking a look! 🙏 FYI: @dtrifiro broke his clavicle/collarbone and won't be able to reply for a few weeks. If there is anything he needs to address, he'll get back to it once he's recovered.

Ah, thanks - good to know. I hope his recovery goes well!

@dtrifiro
Copy link
Contributor Author

Hey @jelmer, sorry for keeping this hanging so long.

I just rebased and added a new commit to update the get_credentials_from_store to use CredentialHelper.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for credential helpers
8 participants